Keen:Mod Code Structure Overview

From Medieval Engineers Wiki
Jump to navigation Jump to search

Hello engineers,

In this short, high-level tutorial we will be explaining some of the key architecture points of Medieval Engineers with regards to modding it. This guide will be a little technical, and will cover the various parts of the engine that we have.

This guide is meant for people with basic programming knowledge who have an interest in making custom code for Medieval Engineers.

Version: 0.4

Gameplay Architecture

The game is essentially built out of two different types of elements. Entities with Entity Components, and Session Components. These are, however, quite distinct elements of the engine.

Entities and Entity Components

Entities make up the bulk of the in-world objects in the game. The planet, buildings, players, basically anything you can see and interact with is an entity. It helps to think of it as an object. Then, the properties that describe an object, are the entity components. For example, if you attach a model component and a controller component to an entity, you have a character in the world that can be controlled.

By describing objects as a unique combination of components it makes it easily possible to re-use a lot of code to provide a toolset from which we can make new objects. For example, if you have a block entity, and attach an inventory component, you have a storage container like the wooden chest. Then, by adding a crafting component, you can turn it into a crafting table. If you want to go another step further, by adding the fuel component it becomes a furnace. Or you can add the stockpile component and it will make the inventory displayed visually like the dining table.

If you want to introduce a game feature that directly affects a specific object in the world, you would make an entity component.

Session Components

For gameplay elements that are not specific about any one object, we have session components. Session components exist exactly once per session. A session is basically a fancy name for the currently loaded game. With session components, we can store all kinds of general data such as who owns which part of the world, which grids have been abandoned and need to be decayed, etc.

If you wanted to introduce a game feature that does not directly describe any one object, a session component would be the way forward.

Data serialization

Data serialization happens in several ways, but they are quite similar. First, there is reading definitions from disk, then there is reading save data from disk, and finally, there is writing save data to disk.

Figure 2.1 will explain the relation of the elements to each other, and then we will describe each element in chapters 2.1, 2.2 and 2.3.

Fig 2.1: Overview of the components

Reading Definitions

Definitions are used to define the initial state of an entity- or session component. Typically this is data not describing any specific instance of the component, but all components of the same type. So for example, the animation speed of a door, or the inventory size of a wooden chest, would be described in the definition.

The actual object used for reading them from disk is called an ObjectBuilder, for example MyObjectBuilder_CraftingComponentDefinition. These object builders are written in a human-readable format, which is not necessarily ideal for the game to operate on, so what we commonly do is parse them into a definition object. Typically, they have the same name, just minus the MyObjectBuilder_ prefix, like MyCraftingComponentDefinition.

Typical data contained in an object builder are things like strings, integers, etc. Usually C#’s primitive data types, but a class containing only primitives is okay too. And their values are stored in a human-readable format, for example, angles are stored in degrees. Then, in the definition, we convert strings into either MyStringHash or MyStringId for performance reasons, angles go from degrees to radians, values are sanity checked (Making sure there is not a bad value) etc.

Finally, all definition objects are stored in the definition manager. This makes it possible for other systems to access the definition by type and subtype.

Reading save data

Now, just these definitions alone are not enough. We also have to have a mechanic for reading and writing the game’s state to disk when you save the game. For example, whether or not the door has been opened, the contents of the inventory, or the currently crafted item and its progress.

These are always written and read by the entity or session component responsible for them and are always in a human readable format. For these, we have the same object builder name, just minus the Definition postfix, like MyObjectBuilder_CraftingComponent.

These are typically stored in a human readable format, and directly processed by the component itself, rather than passed through a definition object.

Writing save data

We use the same object builder for writing data as we use for reading data, for the obvious reason that this way we ensure the data is identical. Again, this is written in a human readable format, and directly generated by the component itself, rather than passed through a definition object.

Warning: Mods with custom object builders
Once a world is saved with a mod with a custom object builder it is currently no longer possible to remove this mod from the world. The game will no longer be able to deserialize the data correctly! This is something we will look into resolving in the future.

Custom Data Types

We also have a couple of custom data types. These are used by Definitions or ObjectBuilders to help the game perform better.

MyStringId and MyStringHash

Rather than interacting with strings everywhere, we like to use MyStringId and MyStringHash instead. These classes are essentially wrappers around an indexing table, and they allow us to refer to strings as an integer instead.

There is a major difference between MyStringId and MyStringHash that you must be aware of: MyStringId always generates a unique number, while MyStringHash just generates a hash of the data. This means that it is possible for different strings to produce the same value with MyStringHash.

The main benefits of these classes is that they improve performance (they are indexed by integer, rather than actual string data), reduce memory usage and allocations (they are only created once, then re-used), and provide a way to transmit string data over the network without having to send strings, reducing the bandwidth usage (because MyStringHash is always computed identically on the server and clients).

The most important difference between MyStringId and MyStringHash is that MyStringId is not guaranteed identical across systems; the order of initialization makes a difference, while MyStringHash will always be the same for each system. This means that MyStringHash values can be sent across the network safely. However, you don’t want to use MyStringHash for everything because every usage of MyStringHash increases the chance of a hash collision happening.

MyStringId is generally used for strings that are shown to the player, while MyStringHash is used for internal data manipulation purposes.

These two are always used in Definitions, never in ObjectBuilders.

SerializableDefinitionId and MyDefinitionId

These structs provide an identifier for a definition by type and subtype. SerializableDefinitionId is used by ObjectBuilders (Human readable/serializable) while MyDefinitionId is used by Definitions (Computer readable).

In the sbc files, you will see these represented as: <Id Type="MyObjectBuilder_CubeBlock" Subtype="WoodBarrel" />

Fun fact: In the code, the Subtype is stored as a MyStringHash! :)

SerializableDefinitionId can be sent across the network, but MyDefinitionId cannot. There are implicit conversion operators available to turn one into the other and vice versa, so make sure you send the correct object!