ConfigPositions – A Fusion of Configuration and Level Data
Most games in Minecraft at least need some configuration related to the specific levels and their structures. This configuration could, for example, be used to set areas of portals, positions of specific NPCs and their looks, some spawners and their designated mob type, frequency and amount, the bounds of an arena, blocks of a movable door, interactable blocks, … and a lot of other use cases.
This configuration is often stored in some external file (typically a YAML or JSON file) along with the absolute coordinates. If the map or feature needs to be loaded, the file is taken along with the level data, and the dynamic content is applied at the configured locations.
Problems of Existing Approaches
Creating this configuration is often a very tedious task that either involves manually reading the current coordinates (through the F3 debug menu or by selecting the specific block(s) in WorldEdit) or the development of some specialized setup wizard that always has to be kept in sync with the runtime plugin and configuration structure.
spawns:
lounge: world,863,68,-997,0,0
spectator: world,876,63,-997,90,0
exit: world,321,67,221,0,5
spawn1: world,873,58,-997,90,0
spawn2: world,863,58,-1007,0,0
spawn3: world,853,58,-997,-90,0
spawn4: world,863,58,-987,180,0
Traditional configuration data for locations typically looks something like this. Sometimes the fields are separated more cleanly or the Bukkit serialization API is used, but the result is similar. (Source: github.com/Eredrim/pvparena)
Maintaining this configuration becomes even more annoying. Not only can the configuration and level quickly get out of sync, there are also loads of other problems with this approach:
- The world receives another name or the structure is moved to another world.
- The configuration file becomes huge and it's hard to keep track of all the locations.
- Some locations should be removed and it's difficult to locate the corresponding entries in the configuration.
- The structure is modified and the original locations/areas no longer match the visuals or blocks of the new level data.
- The structure is moved within the same world.
- Original values were incorrectly extracted, leading to runtime errors.
These operations, along with many others can cause the positional configuration data to become faulty or are error prone in itself. Sadly, there is no widely adopted alternative. That's why we came up with ConfigPositions!
What Are ConfigPositions?
ConfigPositions are special entities that we place in our levels that store arbitrary (but well-defined) positional configurations. Their location in the world is important and they can hold any key-value data. They are present within the levels as real entities and are therefore not external. A ConfigPosition holds the following data structure (aside from its location in the world):
data class ConfigPosition(
type: String,
data: Map<String, String>
)
All config positions have this internal data structure. The location is taken from the entity itself and is therefore not part of the extra data, that we store in the entity.
Technically ConfigPositions are Text Displays with special NBT (Named Binary Tag) data and a dynamic but unified visual appearance. Each ConfigPosition is strictly required to contain a type
. The data
mapping is optional and may be omitted or empty. The type
is used to resolve specific ConfigPositions from the list and to differentiate what data can be expected from a specific configuration.
We use commands to create those ConfigPositions in the world and they come in different flavors. They can be used to express individual locations, areas, directional positions, interaction zones, and any other data that is bound to some kind of position. Since the data
mapping is dynamic, any arbitrary configuration can be attached and persisted within ConfigPositions.
When the ConfigPosition is added to the world (on each chunk load), we read the embedded data from its NBT and overwrite the visual appearance (background, text content, text highlight, order of values, visual offset, …) so that it is consistent across all ConfigPositions. Therefore, its visual appearance is completely detached from the embedded data within NBT.
This makes it easier to filter that we temporarily only want to see certain ConfigPositions or that all ConfigPositions should be visible through walls, that they should float 1 block higher than the configured position, or anything else.
{
"position": {
"x": 745.5,
"y": 30.0,
"z": 984.5
}
"type": "NPC_MAIN_WORLD_TUTORIAL",
"data": {
"villagerType": "SAVANNA",
"villagerProfession": "FLETCHER"
}
}
This is the information that can be extracted from the ConfigPosition above. While it still contains coordinates, the coordinates are relative to a dynamic origin and therefore always in sync with the real level.
When the level or structure is exported, all ConfigPositions are extracted and collected in a separate collection. But since we export all maps and structures in our own SHARD (Shard Highly Augmented Region Data) format, we can embed those extracted positions directly into the level. Separating the level and the entities at this final step comes with various advantages for performance, integrity, and chunk loading, which I'll elaborate on later in this article.
But even before we had our SHARD format, we were exporting ConfigPositions into a separate JSON file that we put next to the schematic or world file and that we would consider while loading the structure and initializing its features.
If we were to load a world with ConfigPositions (either traditionally or as a SHARD), we take the collection of ConfigPositions and search (for example) for a ConfigPosition of type NPC_MAIN_WORLD_TUTORIAL
. If there is no such ConfigPosition, we throw an error if this position is expected and shut down our server immediately. This is to prevent faulty configurations from being used.
But if there is at least one result, we take the very first occurrence and read the specified position. That position is relative to the origin of the exported structure. Therefore, we add the relative offset between the structure and the origin of the world (0,0,0)
to get the new absolute position.
So even though we're still handling position data, it is now relative and embedded within the structure itself. Therefore it can tolerate a shift within the world or the relevant structure. It is also always guaranteed to be the latest version, as it is always transferred along with the same version of the level data.
Now that we have our position, we take the other data from the ConfigPosition and use it to spawn an NPC (in our example, a villager). We apply the specified biome of the villager and its profession and are greeted by a new inhabitant of our world:
NPC_MAIN_WORLD_TUTORIAL
is taken and the corresponding NPC is spawned with the configured biome type and villager profession.The same procedure is applied for lists of ConfigPositions that have the same type. In that case, we take all results for a specific type
and execute the procedure for each of those configurations.
But we cannot only store single positions, we are also able to store entire areas. By storing the relative coordinates of the "min corner" (the corner that has the smallest coordinates on all three axes) in the data
section of ConfigPositions, we can define an area. The stored coordinates are relative to the position of the ConfigPosition itself, which also makes them dynamic for changes.
We do not need to store the "max corner" separately, as it can be constructed by taking the ConfigPosition and adding the inverse of the "min corner" to add, as the ConfigPosition is always in the center of the area. Those ConfigPositions for areas can be defined by selecting a zone with WorldEdit and executing a special command that converts everything and creates the ConfigPosition at the correct position.
There's also a special command flag to store the direction along the position. This direction can then be used to orient the spawned NPCs or objects or to determine the direction of a player spawn or boost pad.
Advantages of ConfigPositions
Now that I've elaborated on the technical details of ConfigPositions, let's dive into the advantages that our system offers, compared to the traditional approach of external configurations. Some of these may be specific to our use cases, while others can be applied universally:
They Are Visible
ConfigPositions are visible within the world, next to blocks and other entities. That makes it significantly easier to comprehend the underlying configuration and special locations can be considered when making changes to the map. They can be hidden/filtered at will, but are visible by default and contain all necessary information for the builders.
They Do Not Impair Building
ConfigPositions are click-through and don't die/remove on any normal operation. That makes them very robust and even blocks can be placed where they are. This way, builders don't have to be extra cautious in their vicinity and the world can still be modified at will, without first relocating the overlapping positions.
They Move With The World/Server
ConfigPositions are embedded into the level as real entities. That means there is no external configuration and they are not bound to a specific plugin, but rather the level itself. Therefore, they cannot be forgotten when moving the world folder or transferring the world to our production server.
They Are Affected By WorldEdit
ConfigPositions are affected by all WorldEdit operations and are therefore as natively supported as blocks are. So if a specific selection is copied or moved, all ConfigPositions are moved with the rest. And there's even support for mass-edit of contained ConfigPositions within a specific selection.
They Are Versatile
ConfigPositions are very flexible, even though their data fields are strictly defined. The same universal data structure can represent single positions, directions, areas, spawn points, and all other positional configurations. They are completely unopinionated on their meaning and the plugin that parses them decides what they represent, based on their type
.
They Can Contain Loads of Data
ConfigPositions have no real limit to the amount of data that gets attached to them. They could hold anything from only the type
to hundreds of data fields that further define this position. This allows us to even store entire skins in the data fields or complex data like equipment.
They Can Be Aligned To The Block Grid
ConfigPositions snap to the block grid. They can be placed either in the middle between 4 blocks, at the center of a block, on slabs, or freely without any snapping. This allows all positions to be configured and not just only block-based positions.
They Are Indexed On Export
ConfigPositions are extracted and indexed when the level data is exported. Therefore, ConfigPositions have zero impact on the runtime performance as no entities are exported. They are also indexed and can therefore be easily queried within the SHARD (or formerly a JSON file). This allows us to have maps like our Clashlands main world that holds more than 3500 ConfigPositions.
They Integrate Natively With Our SHARDs
ConfigPositions are natively supported by our SHARD system. That means, they are embedded into the data structure of the format and are stored within the level data. Therefore, they are really coupled to the level data and can be obtained and loaded as efficiently as possible.
They Can Be Visually Updated Dynamically
ConfigPositions store their data as NBT, which is native to Minecraft, and keep the data detached from their visual appearance. That allows us to dynamically update or change the display properties of ConfigPositions without affecting the stored data. So configured areas could be displayed as highlighted boxes, directions could be presented as arrows, and so on.
They Give Power To The Builders
ConfigPositions allow builders to decide where the best spot for a feature would be, how an NPC should be styled, in what direction something should be oriented, and what blocks should be affected by something. This avoids the typical ping-pong between game developers, game designers, and builders.
Use Cases For ConfigPositions
We have countless applications for ConfigPositions. For any positional configuration, we've switched to using ConfigPositions exclusively and have come up with many great features that make excellent use of them. Below are some examples of our current use cases for ConfigPositions, but as time moves on, there will probably be many more:
- Sounds: To mark the location and type of a specific sound or a pool of varying sounds to play there.
- NPCs: As the position for a static NPC with a specific rotation and (if it is a villager) some configuration on its appearance.
- Spawns: For the monster type, spawning frequency, group, and location of the individual spawns.
- Doors: To set the affected blocks that should be considered as "in" the door and the blocks that should be removed to open it.
- Rooms: The selection of a single room for Dungeon Explorer to quickly export each room individually.
- Leaderboards: The orientation, attachment, and location of the text of the leaderboard, relative to the display object of the board.
- Player spawns: The direction and locations where players should spawn or respawn in a specific area.
- Complex objects: To spawn complex objects that need to be assembled of multiple, moving, and interactable parts.
- Portals: As the definition of the area that should be considered to be "in" the portal and to spawn the corresponding particles.
- Interaction points: To set the size and position of interaction entities that can be used to interact with the world.
- Paste slots: To define where another structure should be embedded in the underlying structure and how much space is available.
They also come in especially handy for rapid prototyping. We don't have to develop some rigid configuration scheme or think about how we're gonna supply the configuration for a specific level anymore. Instead, we'll just toss in some ConfigPositions and can skip right to testing our features.
Was It Worth It?
It has certainly paid off to develop ConfigPositions and convert all configurations to our new format. We've been using ConfigPositions for 14 months now and it has saved us so much time! It took a while to get the tooling ready, and we had to adapt to our new server initialization logic, but now that we've gotten used to the new system, we see it as a powerful asset in our toolbox.
ConfigPositions are not our only productivity and efficiency improvement. We've also developed Contentplate, a configuration repository for dynamic configuration, aimed at our game designers. While ConfigPositions puts the power back into the hands of builders and improves their speed, Contentplate puts the game designers in charge and allows them to configure everything live, check the differences between our live and staging environments, experiment with the game balance, and apply those changes like in Git.
Finally, we've created our SHARD format that simplifies level management, improves the reliability and performance of level data merging and world loading, and simplifies our update migrations. In the future, we want to also release blog posts about our SHARD format and our Contentplate system.
Did You Like This Post?
If you are interested in working in a friendly environment with lots of interesting technologies, experimentation, and passionate other people, feel free to reach out to us! We're always looking for new team members. For contact information and an overview of our project, the skills you need to possess, and the problems you'll be facing, head over to our job posting!
We're constantly working on exciting stuff like this and would love you to take part in the development of JustChunks. If you're just interested in more JustChunks-related development or want to get in touch with the community, feel free to read our regular, german recaps here or hop on our discord server!
Our Discord server for JustChunks. Join our community, discuss our development or propose changes to features. If you'd like to talk with us, that is your point to go!