Adding an Item

Adding an item is a great way to familiarize yourself with the basics of making mods. Note: All of the solutions used in these guides are available on the SemiGuides repo.

Start off by opening your mod's solution.

Assuming you haven't done it yet, you might want to consider renaming the solution to your mod's name.

Make sure that you have your mod.yml all set up.

We will start off by creating an encounter icon for our item. This is the icon that shows up in the Ammonomicon. We technically could make it different to the actual item's sprite very easily, but for the purpose of this guide we won't.

Semi has a useful shortcut called RegisterEncounterIcon, which takes two parameters - the ID of our icon and a sprite path. It will automatically handle everything about the Ammonomicon sprite collection for us, and it's enough for our simple test item.

Note that RegisterEncounterIcon has to be ran in registering mode, meaning that we can only use it inside RegisterContent.

public override void RegisterContent() {
    Logger.Debug($"Registering content.");
    
    RegisterEncounterIcon("@:my_item_icon", "my_item.png");
}

I've used a context ID @:my_item_icon here, meaning that the real ID of this icon (actually a sprite definition) will end up being adding_an_item:my_item_icon (the adding_an_item comes from id inmod.yml). Context IDs are a great way to avoid having to type out long namespaces every time, and they make it very easy to see where IDs are used too.

Now that we have an encounter icon present, we will start with creating the actual item itself. We have to first create its "sprite template" - the object that the game will copy when it wants to spawn the item. To create that sprite template, we need a sprite collection, and for that sprite collection we want a sprite definition containing our image.

If we wanted to just create a new sprite definition from a single image, we'd use the CreateSpriteDefinition method. However, that'd load the whole image again. There's a nifty shortcut that we can use - RegisterEncounterIcon creates a SpriteDefinition under the hood, and returns it at the end of the method. We can then reuse this sprite definition.

So, we've got the sprite definition step handled. Now we need to register the collection. We use RegisterSpriteCollection for this, unsurprisingly. ID, and then a list of definitions.

public override void RegisterContent() {
    Logger.Debug($"Registering content.");
    
    var def = RegisterEncounterIcon("@:my_item_icon", "my_item.png");
    RegisterSpriteCollection(
        "@:my_item_coll",
        def
    );
}

Now we have a sprite collection. If we had more items, we could've put in all of their sprites into one collection, but we don't need that right now. It's time to register the sprite template - RegisterSpriteTemplate.

public override void RegisterContent() {
    Logger.Debug($"Registering content.");
    
    var def = RegisterEncounterIcon("@:my_item_icon", "my_item.png");
    RegisterSpriteCollection(
        "@:my_item_coll",
        def
    );
    RegisterSpriteTemplate(
        "@:my_item_sprite",
        "@:my_item_coll",
        "@:my_item_icon"
    );
}

RegisterSpriteTemplate is a bit more high level, in that it takes IDs as opposed to concrete references. This makes the code more obvious at first glance and makes it easier for mods to interact with eachother.

We've now created a sprite template, called my_item_sprite, that points to the collection my_item_coll's my_item_icon definition. We forgot about something, however - and that's the localization.

Gungeon supports multiple languages, and so naming an item isn't as simple as just giving it an English name. Even if you don't care about other languages, Semi allows mod authors to localize not only their own content but also content from other mods, so it's important that you play by the rules and use the systems present. Semi has its own localization system that is largely based off of Gungeon's one and works transparently with it, while working around some of EtG's localization system's flaws.

We will go ahead now and create an localization file for english. This is a simple text file where #X represents a key named X and all lines after that that don't start with # are considered the key's value. For example:

#SOMETHING
Something

Creates a key called SOMETHING with the value Something. Semi extends this basic format to give localization keys IDs, but we'll bring that up in a minute.

For now: minimalize that Visual Studio/Mono Develop window, and go into the mod folder in the solution's directory. Then enter resources. Create a new file, call it something like english_items.txt. In it write this:

#MY_ITEM_NAME
My Item
#MY_ITEM_SHORT_DESC
Very Cool Item
#MY_ITEM_LONG_DESC
This Item Is Very Cool
Yes

Congratulations! You've created a localization file. Wasn't so hard, was it?

Now we will get it set up so that Semi recognizes it. Localizations are registered using RegisterLocalization (you should be noticing a pattern here ;) ).

public override void RegisterContent() {
    Logger.Debug($"Registering content.");
    
    var def = RegisterEncounterIcon("@:my_item_icon", "my_item.png");
    RegisterSpriteCollection(
        "@:my_item_coll",
        def
    );
    RegisterSpriteTemplate(
        "@:my_item_sprite",
        "@:my_item_coll",
        "@:my_item_icon"
    );
    RegisterLocalization(
		"@:english_items",
		"english_items.txt",
		"gungeon:english",
		I18N.StringTable.Items
	);
}

Okay, okay, okay. I hear you. That's quite the number of arguments to load that one simple file. But you see, they are all there for a reason.

The first argument is of course the new localization's ID. It doesn't matter what you pick for this. The second argument is the path to the resource. The third is the target language, in this case English, and the fourth is the string table. There are 6 string tables - Core, Enemies, Intro, Items, Synergies and UI. This is a relic of how Gungeon was made - these string tables are essentially separate files per each language. For example, when obtaining the value MY_ITEM_NAME for the name of the item, the game will look in the Items table because it expects item localizations to be there. If it wants to get the name of a synergy, it'll look in the Synergies table. If you ask me, it's a bit of an unnecessary system, and hopefully we can get abstract it away in the future, but we have to use it for now.

While we are on the topic of the resources folder already, let's drop in our my_item.png into there as well. You can find it here.

Alright, we've got everything prepared now. It's time to start writing the actual item code. We will create a new Class called MyItem. This class will extend from PassiveItem.

PassiveItem has virtual methods Pickup and Drop, so we will extend those:

For simplicity, this item will simply double the player's health. Arguably the most useful tool while modding is intellisense, also known as the autocomplete. If ever you want to accomplish something and you don't want to spend time on looking through the decompiler, just try certain keywords and you might hit jackpot. This is certainly the case here - typing player.health points me to player.healthHaver, and typing player.healthHaver.max points me to the GetHealthMax and SetHealthMaximum methods. However, sometimes it's worth it to bring out the decompiler - because checking how BasicStatItem works has led me to realizing that I can use the StatModifier system to be more in line with what the game expects. Here's how I accomplished doubling health:

public class MyItem : PassiveItem {
	public StatModifier Modifier = new StatModifier {
		statToBoost = PlayerStats.StatType.Health,
		amount = 2,
		modifyType = StatModifier.ModifyMethod.MULTIPLICATIVE
	};

	public override void Pickup(PlayerController player) {
		base.Pickup(player);

		player.ownerlessStatModifiers.Add(Modifier);
		player.stats.RecalculateStats(player);
	}

	public override DebrisObject Drop(PlayerController player) {
		player.ownerlessStatModifiers.Remove(Modifier);
		player.stats.RecalculateStats(player);

		return base.Drop(player);
	}
}

I made it so that the stat modifier is saved as a property on the item, so that I can remove the modifier when the item is dropped. The code makes sure to recalculate the PlayerStats object after adding/removing the modifier to instantly see the difference.

Alright, looks like our item is ready. Time to finally register it with RegisterItem.

public override void RegisterContent() {
	Logger.Debug($"Registering content.");

	var def = RegisterEncounterIcon("@:my_item_icon", "my_item.png");
	RegisterSpriteCollection(
		"@:my_item_coll",
		def
	);
	RegisterSpriteTemplate(
		"@:my_item_sprite",
		"@:my_item_coll",
		"@:my_item_icon"
	);
	RegisterLocalization(
		"@:english_items",
		"english_items.txt",
		"gungeon:english",
		I18N.StringTable.Items
	);
	
	RegisterItem<MyItem>(
		"@:my_item",
		"@:my_item_icon",
		"@:my_item_sprite",
		"#@:MY_ITEM_NAME",
		"#@:MY_ITEM_SHORT_DESC",
		"#@:MY_ITEM_LONG_DESC"
	);
}

We start off with the new item's ID, then we have the ID for the Ammonomicon icon and the ID for the sprite template. After that, we have the localization keys we defined previously. Note how they also have that @: context ID namespace - localizations in Semi are namespaced. Localization keys start with #, so what we end up with is a combination of both the namespace and the hash symbol. Otherwise, it's pretty much the same as other IDs. The order of the key arguments is as follows: item name, short description and long description.

It looks like we are done here. Build your mod in your IDE of choice, then copy over the mod folder to an appropriately named folder in SemiMods. Add the ID to MySemiMods.txt, and let's run it.

Note: It seems like older versions of MonoDevelop (or possibly all of them) might not properly follow default compilation targets specified in the config file of the project. Make sure to check the modification date on mod.dll after building your mod. If it does not match with the last time you built the solution, try running xbuild or msbuild from the terminal.

Now you can try running the game. Keep an eye on the output log.

If you are using the semi-dev build (which you should if you are writing mods), you can enter the game and open up the debug console (F2). Then, run give adding_an_item:my_item. This should give you the item and the effect.

(For some reason, the game doesn't give you that last heart filled, which is interesting. We could do that ourselves by healing the player, but this is enough for the tutorial.)

Last updated