ID System

Preface

Enter the Gungeon was not made with mod support in mind. As a consequence of this, elements of the game (items, enemies, etc.) are usually identified by hardcoded numbers.

This system of numeric IDs makes it difficult to handle multiple mods loaded at once, as it introduces the risk of collisions and is in general very inflexible.

Imagine that mod A adds an item and it just so happens to get a numeric ID of 500 - then, mod B is loaded, and it adds an item with the next available numeric ID - 501. You save and quit the game during a run where you found mod A's item - ID 500.

A day later, you decide that you want to continue playing, and so you start up the game. You load the game, and to your surprise, mod A's item has been replaced by mod B's. Why? Well, as it turns out, you've updated mod A to a broken version, causing it to fail loading. Mod B did load, though, and its item has been registered as the first available numeric ID - 500. The game isn't at fault here, as it saved your game with the item with ID 500 in your inventory. There was no way for the loading process to tell that anything went wrong.

If IDs were not assigned based on what's available but instead decided by the mod's author, an entirely new problem arises - nobody knows what they should use. Should you use 500? 5000? 99999? Surely nobody else would use 4812394, but can you be sure about that? And what if another mod does use the same ID as you? Should yours or theirs fail loading? Should the next one be assigned instead?

As you can see, numeric ID systems create a lot of problems when it comes to modding, and that is why Semi uses a system closely inspired by Minecraft's identifiers - an ID is a piece of text that specifies a name and an optional namespace.

ID Format

A basic identificator for a particular resource looks like this: namespace:key. Same as in that game, IDs that look like key are also valid - they will always resolve to the "default" namespace of the game - gungeon. This means that key and gungeon:key are exactly the same thing.

Idiomatically, a single mod has a single namespace where it can insert its entries (items, enemies, synergies, etc.) using a provided set of methods. Nothing stops a mod from having multiple namespaces, however that requires more direct use of the provided registries.

On top of the Minecraft-style ID system, Semi implements something called "explicit contextual IDs". As a bit of a backstory, there are multiple places in Semi mods where you will want to refer to items, synergies and other objects that come from your own mod. As expected, it would be a huge annoyance if you were forced to always prefix them with the ID of your mod. The initial solution to this was that certain methods would override the default behavior of the shorthand IDs mentioned above (ones without a namespace) - instead of being autofilled to the gungeon namespace, they'd instead be autofilled to your mod's namespace.

While this solved the issue of having to repeat the ID of your mod a lot and made it possible to change the ID during development very easily, it created another problem - the behavior of IDs was now highly inconsistent, and it was difficult to tell what happens when. Presented with a parameter like item_id in a method, there was no sure way to know that this would default to your namespace or to the gungeon namespace.

Explicit context IDs are a compromise for both issues. They're defined like this: @:name, with an @ symbol as the namespace. It adds only 2 extra static characters for referring to local IDs, but means that the behavior of default namespaces can be consistent - you can always expect the IDx to become gungeon:x, no matter where you type it. Context IDs are resolved by certain methods. If for some reason you accidentally pass a context ID to a method that does not resolve it, for example because it doesn't know what mod called it, the ID pool will throw an exception informing you of this.

ID Objects

Formerly Semi stored and used IDs as literal strings (text), i.e. the C# string type. Every place in the code that handled IDs called appropriate methods to verify that the ID was written correctly and to expand it if necessary (in the case of defaulted or context identifiers). This became quite cumbersome to maintain. It also meant that you could not tell whether a field was an ID or text used for some other purpose from the signature (parameters) of a method. Take this quite long method:RegisterItem(string id, string enc_icon_id, string sprite_template_id, string name_key, string short_desc_key, string long_desc_key)

The only way to know which of these arguments is an ID is to make educated guesses about the names of the parameters or to read the implementation of it. This can become a lot more difficult with more complicated methods.

The solution to this was quite simple - Semi uses special objects called ID to represent the IDs, not strings. These structs can be created from strings and turned into strings. They also give you automatic separation of namespace and name (key) without the need for any parsers and without even allocating additional memory on the heap - both of those fields are StringView objects which act like a string but actually reference a portion of an existing string.

IDs cast to strings implicitly - in other words, something like string str = some_id will work fine. In order to add additional clarity however, strings do not implicitly cast to IDs - you have to prepend them with (ID) like this: ID my_id = (ID)"abc". You can treat (ID)"blah:blah" as a sort of ID "literal", in the same way that 123 is a number literal and true is a boolean literal.

IDs are used primarily in ID Pools (registries), which are nothing more than glorified dictionaries (AKA hash maps). They contain a list of objects, all assigned unique ID structs.

Gungeon Entries

As Enter the Gungeon uses numeric IDs almost everywhere, and Semi aims to use string IDs almost everywhere, work has to be done to bridge this gap.

This is done through the use of ID maps, which are text files containing information that links numeric IDs and string IDs. These files are compiled into C# code that constructs appropriate ID pools and adds all the entries to the table one by one. This is very much an implementation detail of the mod loader, so it's not that important if you are only writing mods.

What is important, however, is to remember exactly the process of ID resolution mentioned in the earlier sections. Enter the Gungeon items, enemies and other ID-pooled entries always reside under the gungeon namespace. This namespace is usually locked, meaning that you cannot remove or add entries to it. Exceptions exist where the namespace is not locked, but this is usually a side effect of implementation details and doesn't affect anything.

The names for Gungeon-provided items are usually based on their ingame names with the english language selected, with a few exceptions notably including Ser Junkan (ingame name Junk) residing under gungeon:junkan or an inconsistency between the Alpha Bullet and Omega Bullets being resolved into gungeon:alpha_bullets and gungeon:omega_bullets. A list of Gungeon items and their IDs is available here: [TODO].

Available ID Pools

At the moment, only certain elements of the game have their own ID pools in Semi. Modification of the game's code itself to use the string ID system instead of the numeric one is also very sparse (though, for performance reasons it's actually a good thing to keep this to a minimum).

With the current API, there exist ID pools for the following areas of the game:

  • Items (this includes guns and consumables such as ammo packs)

  • Enemies (this includes companions)

  • Synergies

  • Localizations

  • Languages

Certain ID pools are provided by the current version of Semi but only hold content added by mods and not by the vanilla game. These are:

  • Sprite collections

  • Sprite templates

  • Animation templates

  • Audio tracks

Last updated