Skip to content

5. Example mod

Andre Louis Issa edited this page May 25, 2024 · 6 revisions

In this part you will be shown an example of the process of making a mod.

The mod we will make adds a new keepsake and a way to obtain it, and covers LUA, SJSON and adding new assets.

Adding the new keepsake

First, I make a mod as shown in the section above with no code yet. I only have the MyMod folder with modfile.txt, config.lua and main.lua.

Since we want to make a new keepsake, we'll need to see how the existing keepsakes are made.

Cerberus' keepsake is called Old Spiked Collar in game, so we search that name in Helptext.

We find that it's called MaxHealthKeepsakeTrait internally, so we search that name in Scripts.

We get a couple results, and from experience we know that GiftData and TraitData are where we want to look.

  • GiftData.lua contains data related to keepsakes and companions.
  • TraitData.lua contains data related to all the effects that can be applied to the player.

In GiftData we see that the first match is a table named GiftOrdering, which we can safely assume lists all the keepsakes in the rack.

The second match at first glance doesn't seem relevant to us, as it's the data related to the relationship progression with Cerberus.

We want to add a new keepsake so we will need to add our keepsake to the GiftOrdering table.

Next, in TraitData the match is the effect of the keepsake, which increases the player's maximum health.

We'll need to create our own and add it to the TraitData table.

With this knowledge, we need to create two new files in our mod folder, GiftData.lua and TraitData.lua. For better organization we'll put them in a new folder called Data.

The reason we have to make these files is because we can't do what we want from our main.lua file, since it is imported in RoomManager.lua. If we added our changes there, they would get overwritten when the game loads GiftData.lua and TraitData.lua.

To circumvent this, we'll import our two new files directly into GiftData.lua and TraitData.lua.

Open your modfile.txt and add the following :

To "Scripts/TraitData.lua"
Import "Data/TraitData.lua"

To "Scripts/GiftData.lua"
Import "Data/GiftData.lua"

With this done, we can add our data, which for the time being is a copy of the Cerberus keepsake.

Data/GiftData.lua :

if not MyMod.Config.Enabled then return end

table.insert(GiftOrdering, "MyModTrait")

Data/TraitData.lua :

if not MyMod.Config.Enabled then return end

TraitData.MyModTrait =
{
    Icon = "Keepsake_Collar",
    EquipSound = "/SFX/Menu Sounds/KeepsakeCerberusCollar",
    InheritFrom = { "GiftTrait" },
    InRackTitle = "MaxHealthKeepsakeTrait_Rack",
    RarityLevels =
    {
        Common =
        {
            Multiplier = 1.0,
        },
        Rare =
        {
            Multiplier = 1.5,
        },
        Epic =
        {
            Multiplier = 2.0,
        }
    },
    PropertyChanges =
    {
        {
            LuaProperty = "MaxHealth",
            BaseValue = 25,
            AsInt = true,
            ChangeType = "Add",
            MaintainDelta = true,
            ExtractValue =
            {
                ExtractAs = "TooltipHealth",
            }
        },
    },
    SignOffData =
    {
      {
        Text = "CerberusSignoff",
      },
      {
        RequiredTextLines = { "CerberusGift09" },
        Text = "CerberusSignoff_Max"
      }
    },
}

Now we run modimporter and reload our save.

However, when we open the keepsake rack we don't see any new locked keepsake. This means we've missed something.

The only difference between our keepsake and Cerberus' is the entry in GiftData. We'll add our own and see if it fixes the problem.

Data/GiftData.lua :

if not MyMod.Config.Enabled then return end

table.insert(GiftOrdering, "MyModTrait")

GiftData.MyModNpc =
{
    InheritFrom = {"DefaultGiftData"},	
    MaxedIcon = "Keepsake_Cerberus_Max",
    MaxedSticker = "Keepsake_CerberusSticker_Max",
    MaxedRequirement = { RequiredTextLines = { "CerberusGift09" }, },
    Locked = 7,
    Maximum = 9,
    [1] = { Gift = "MyModTrait" },
    [7] = { RequiredResource = "SuperGiftPoints" },
    [8] = { RequiredResource = "SuperGiftPoints" },
    [9] = { RequiredResource = "SuperGiftPoints" },
    UnlockGameStateRequirements = { RequiredTextLines = { "CerberusAboutBeingBestBoy01" } },
    TrackUnlockedBlockInput = true,
}

We reload our save and now we can see a new keepsake has appeared in the rack.

However we are not adding any new NPC so we don't need most of these lines. After testing, we only need to keep two fields for the keepsake to show up.

Data/GiftData.lua :

if not MyMod.Config.Enabled then return end

table.insert(GiftOrdering, "MyModTrait")

GiftData.MyModNpc =
{
    InheritFrom = {"DefaultGiftData"},
    [1] = { Gift = "MyModTrait" }
}

Unlocking the keepsake

Now we just need to make interacting with Zagreus' bed unlock the keepsake.

For that we'll need to find 3 things :

  • The engine trigger for interacting with objects
  • The ID of the bed
  • Code that unlocks the keepsake

First we'll look for the engine trigger. Thankfully, one of the scripts in the Scripts folder is conveniently named Interactables.lua, which makes it a good place to start.

And the engine trigger we need is conveniently on line 3.

We note the engine trigger we'll use is named OnUsed for later.

Now we'll look for the ID of the bed. However, we have no idea what the bed could be called internally. Fortunately, when we interact with the bed it plays a text line, so we can look for that.

We search for can't sleep and get exactly 1 result, leading to TakeANapVoiceLines.

From there we can search for TakeANapVoiceLines and see what uses it. We find two objects use these voicelines, Bedroom Couch and Bed, with their ID.

This might not be what we're looking for so we'll need to test it using the engine trigger we found.

main.lua :

if not MyMod.Config.Enabled then return end

OnUsed{ 310036, function (triggerArgs)
    DebugPrint({Text="@@@TEST@@@"})
end}

We reload our save, and when we interact with the bed we see our debug print works.

The engine trigger works and we have the correct ID, now we only need some code to unlock the keepsake.

Since GiftData.lua contains data related to keepsakes, we can assume GiftScripts.lua contains code related to keepsakes.

We search for unlock in GiftScripts.lua and see if something interesting pops up.

There are no conveniently named unlock function, however under the last match we notice a function named IncrementGiftMeter.

If we search for it, we find there's a debug function to max all keepsakes that uses it.

We can probably use it for our purpose. Looking at our GiftData, if we increment by 1 it should unlock our keepsake.

main.lua :

if not MyMod.Config.Enabled then return end

OnUsed{ 310036, function (triggerArgs)
    IncrementGiftMeter("MyModNpc", 1)
end}

We reload our save, interact with the bed, go to the keepsake rack and now our keepsake is unlocked.

Changing the effect

For the effect, we'll make it heal the player by 5 when they go through doors, like the Chthonic Vitality mirror option.

So first we will search for Chthonic Vitality and see how it works.

We find that it's called DoorHeal internally and there are quite a few matches. After going through the results, we notice the mirror option seems to be DoorHealMetaUpgrade in MetaUpgradeData.lua.

Searching for DoorHealMetaUpgrade, we get a much shorter list of results, and find a function called CheckDoorHealTrait in RoomManager.lua.

Looking at the code, this appears to be the function that handles Chthonic Vitality healing. We need to alter it for our keepsake to function, but we have to do so from our main.lua file.

The best way to go about it is a context wrap. We will essentially hijack the round function to change the result it returns when it is called by CheckDoorHealTrait.

Before we do that, first we need to give our trait a value we can then pull with GetTotalHeroTraitValue.

Add MyModTraitValue = 5, on line 8 of Data/TraitData.lua.

Data/TraitData.lua :

if MyMod.Config.Enabled then
    TraitData.MyModTrait =
	{
		Icon = "Keepsake_Collar",
		EquipSound = "/SFX/Menu Sounds/KeepsakeCerberusCollar",
		InheritFrom = { "GiftTrait" },
		InRackTitle = "MaxHealthKeepsakeTrait_Rack",
		MyModTraitValue = 5,

Then we add our context wrap in main.lua :

    ModUtil.Path.Context.Wrap("CheckDoorHealTrait", function()
        ModUtil.Path.Wrap("round", function(base, ...)
            return base(...) + GetTotalHeroTraitValue("MyModTraitValue")
        end, MyMod)
    end, MyMod)

As you can see, we are using Mod Utility here. For more information on this function and many others, check the Mod Utility Wiki.

This bit of code can be broken down to 'if CheckDoorHealTrait calls the round function, have the round function do this'.

And what me make it do is include our keepsake value in the healing that happens when you go through a door.

To test it we'll reload our save, equip our keepsake, start a run, take some damage then take the first door. And as we can see, our mod works.

Now we can remove all the unneeded data in our keepsake trait.

Looking at it, it seems that PropertyChanges is what increases max health and RarityLevels what increases the value at each keepsake level. SignOffData is unclear, but seems related to relationship with Cerberus.

We don't need any of these as our keepsake doesn't scale with level, doesn't increase max health and isn't related to Cerberus.

TraitData.MyModTrait =
{
	Icon = "Keepsake_Collar",
	EquipSound = "/SFX/Menu Sounds/KeepsakeCerberusCollar",
	InheritFrom = { "GiftTrait" },
	InRackTitle = "MaxHealthKeepsakeTrait_Rack",
	MyModTraitValue = 5,
	RarityLevels =
	{
		Common =
		{
			Multiplier = 1.0,
		},
		Rare =
		{
			Multiplier = 1.5,
		},
		Epic =
		{
			Multiplier = 2.0,
		}
	},
	PropertyChanges =
	{
		{
			LuaProperty = "MaxHealth",
			BaseValue = 25,
			AsInt = true,
			ChangeType = "Add",
			MaintainDelta = true,
			ExtractValue =
			{
				ExtractAs = "TooltipHealth",
			}
		},
	},
	SignOffData =
	{
		{
		Text = "CerberusSignoff",
		},
		{
		RequiredTextLines = { "CerberusGift09" },
		Text = "CerberusSignoff_Max"
		}
	},
}

Deleting them, our trait is now much more lightweight. Quickly testing in game, our keepsake still works as normal.

TraitData.MyModTrait =
{
	Icon = "Keepsake_Collar",
	InheritFrom = { "GiftTrait" },
	InRackTitle = "MaxHealthKeepsakeTrait_Rack",
	MyModTraitValue = 5
}

Now we'll make this value configurable by the user. In our config.lua file, we add a new field HealValue with a value of 5.

mod.Config = {
    Enabled = true,
    HealValue = 5
}

Then we use it in our keepsake trait.

Data/TraitData.lua :

TraitData.MyModTrait =
{
	Icon = "Keepsake_MyModKeepsake",
	InheritFrom = { "GiftTrait" },
	InRackTitle = "MyModTrait_Rack",
	MyModTraitValue = MyMod.Config.HealValue
}

Now the user can set whatever value they like.

However, we quickly notice some issues. Though our keepsake works, it needs a name and a proper description to look legitimate. We also now know what SignOffData is.

Changing the description

Oddly enough, our trait shows up as Old Spiked Collar in the keepsake rack, but as MyModTrait in our boons.

However, at the very beginning of making our mod, we found the entry in HelpText for Cerberus' keepsake, which internally was called MaxHealthKeepsakeTrait.

In our trait, we can see an InRackTitle field with the value MaxHealthKeepsakeTrait_Rack. Taking a look in HelpText, right below the MaxHealthKeepsakeTrait entry is this one.

Now we know where the name and description come from. Originally our trait was named MaxHealthKeepsakeTrait, and it is safe to assume the game used that name to display the in game name and description.

Since we changed it and there is no corresponding entry in HelpText, now it doesn't work.

    {
      Id = "MaxHealthKeepsakeTrait"
      DisplayName = "Old Spiked Collar"
      Description = "Add {#AltUpgradeFormat}+{$TooltipData.TooltipHealth}{!Icons.HealthUp_Small}{#PreviousFormat} to your Life Total."
    }
    {
      Id = "MaxHealthKeepsakeTrait_Rack"
      InheritFrom = "MaxHealthKeepsakeTrait"
      Description = "Add {#AltUpgradeFormat}+{$TooltipData.TooltipHealth}{!Icons.HealthUp_Small}{#PreviousFormat} to your Life Total. \n\n\n {#AwardFlavorFormat}{$TooltipData.SignoffText}"
    }

This means we need to add our own entries to HelpText to display our name and description in game.

So we'll make a new folder in our mod folder named Game, with another folder inside called Text. Then in that folder we make a new file called HelpText.en.sjson.

Next, we'll prepare our sjson file for Mod Importer.

Game/Text/HelpText.en.sjson :

{
  ? = [
    "_append"
  ]
}

Now we need to replace ? with the name of the sequence in HelpText, which we can find by scrolling all the way up.

The sequence is called Texts, so we'll put that name in our sjson file.

Game/Text/HelpText.en.sjson :

{
  Texts = [
    "_append"
  ]
}

For more information on how this works, you can read about it in the SGG Mod Format Wiki.

To make our entries, we'll first copy Cerberus' Keepsake's.

Game/Text/HelpText.en.sjson :

{
  Texts = [
    "_append"
    {
      Id = "MaxHealthKeepsakeTrait"
      DisplayName = "Old Spiked Collar"
      Description = "Add {#AltUpgradeFormat}+{$TooltipData.TooltipHealth}{!Icons.HealthUp_Small}{#PreviousFormat} to your Life Total."
    }
    {
      Id = "MaxHealthKeepsakeTrait_Rack"
      InheritFrom = "MaxHealthKeepsakeTrait"
      Description = "Add {#AltUpgradeFormat}+{$TooltipData.TooltipHealth}{!Icons.HealthUp_Small}{#PreviousFormat} to your Life Total. \n\n\n {#AwardFlavorFormat}{$TooltipData.SignoffText}"
    }
    {
      Id = "CerberusSignoff"
      DisplayName = "From Cerberus"
    }
  ]
}

Now we'll edit them to our liking. For our keepsake's name, we'll name it Old Feather.

Game/Text/HelpText.en.sjson :

{
  Texts = [
    "_append"
    {
      Id = "MyModTrait"
      DisplayName = "Old Feather"
      Description = "Restores {#AltUpgradeFormat}5{!Icons.HealthUp_Small}{#PreviousFormat} health when you exit a chamber."
    }
    {
      Id = "MyModTrait_Rack"
      InheritFrom = "MyModTrait"
      Description = "Restores {#AltUpgradeFormat}5{!Icons.HealthUp_Small}{#PreviousFormat} health when you exit a chamber. \n\n\n {#AwardFlavorFormat}{$TooltipData.SignoffText}"
    }
    {
      Id = "MyModSignoff"
      DisplayName = "From your bed"
    }
  ]
}

Satisfied, we can now edit our trait to use these new entries.

Data/TraitData.lua :

TraitData.MyModTrait =
{
	Icon = "Keepsake_Collar",
	InheritFrom = { "GiftTrait" },
	InRackTitle = "MyModTrait_Rack",
	MyModTraitValue = MyMod.Config.HealValue,
	SignOffData =
	{
	  {
		Text = "MyModSignoff"
	  }
	}
}

Then we add a new entry to our modfile.txt to import our sjson file :

To "Game/Text/en/HelpText.en.sjson"
SJSON "Game/Text/HelpText.en.sjson"

Now to test if it worked we'll close our game, run modimporter, start our game then load our save.

As we can see, everything worked correctly.

However, if the user changes the heal value, the description won't change in game to reflect it, as we hard set the description value to 5.

Looking at what we replaced from the original trait and description, it seems we can get the value from the trait and display it.

{
    LuaProperty = "MaxHealth",
    BaseValue = 25,
    AsInt = true,
    ChangeType = "Add",
    MaintainDelta = true,
    ExtractValue =
    {
        ExtractAs = "TooltipHealth",
    }
}
Description = "Add {#AltUpgradeFormat}+{$TooltipData.TooltipHealth}{!Icons.HealthUp_Small}{#PreviousFormat} to your Life Total."

However our trait doesn't have PropertyChanges, so let's look at the HydraLite trait.

We'll replicate it and edit our description.

Data/TraitData.lua :

TraitData.MyModTrait =
{
	Icon = "Keepsake_MyModKeepsake",
	InheritFrom = { "GiftTrait" },
	InRackTitle = "MyModTrait_Rack",
	MyModTraitValue = MyMod.Config.HealValue,
	ExtractValues =
	{
		{
			Key = "MyModTraitValue",
			ExtractAs = "TooltipHeal"
		}
	},
	SignOffData =
	{
	  {
		Text = "MyModSignoff"
	  }
	}
}

Game/Text/HelpText.en.sjson :

    {
      Id = "MyModTrait"
      DisplayName = "Old Feather"
      Description = "Restores {#AltUpgradeFormat}{$TooltipData.TooltipHeal}{!Icons.HealthUp_Small}{#PreviousFormat} health when you exit a chamber."
    }
    {
      Id = "MyModTrait_Rack"
      InheritFrom = "MyModTrait"
      Description = "Restores {#AltUpgradeFormat}{$TooltipData.TooltipHeal}{!Icons.HealthUp_Small}{#PreviousFormat} health when you exit a chamber. \n\n\n {#AwardFlavorFormat}{$TooltipData.SignoffText}"
    }

Now we close our game, run modimporter, start our game and load our save to verify it worked, which it did.

To test it, we change our HealValue to 10 and reload our save.

Now the user can set the value they want and the descripion will reflect the change in game.

However, we are still using Cerberus' keepsake's icon. It's time to add our own icon to replace it.

Adding a new icon

To add our icon we will need to import it. We'll search for our current keepsake icon to see what SJSON we need to add.

We get 4 results in a single file, GUIAnimations.sjson. Checking each result, there are 3 different entries.

This means we'll need to repeat what we did for the description. We create a new folder Animations in our MyMod/Game folder and create the file GUIAnimations.sjson.

In that file we put copies of each entry we found along with the name of the sequence we're importing into, Animations.

Game/Animations/GUIAnimations.sjson :

{
  Animations = [
    "_append"
    {
      Name = "Keepsake_Collar_Small"
      InheritFrom = "KeepsakeTrayIconBacking"
      ChildAnimation = "Keepsake_Collar_Child"
      EndFrame = 1
      StartFrame = 1
    }
    {
      Name = "Keepsake_Collar_Child"
      InheritFrom = "KeepsakeTrayIcon"
      FilePath = "GUI\Screens\AwardMenu\old_spiked_collar_06"
      EndFrame = 1
      StartFrame = 1
    }
    {
      Name = "Keepsake_Collar"
      InheritFrom = "KeepsakeIcon"
      FilePath = "GUI\Screens\AwardMenu\old_spiked_collar_06"
      EndFrame = 1
      StartFrame = 1
    }
  ]
}

Then we edit them, keeping the Filepath fields empty for now.

Game/Animations/GUIAnimations.sjson :

{
  Animations = [
    "_append"
    {
      Name = "Keepsake_MyModKeepsake_Small"
      InheritFrom = "KeepsakeTrayIconBacking"
      ChildAnimation = "Keepsake_MyModKeepsake_Child"
      EndFrame = 1
      StartFrame = 1
    }
    {
      Name = "Keepsake_MyModKeepsake_Child"
      InheritFrom = "KeepsakeTrayIcon"
      FilePath = ""
      EndFrame = 1
      StartFrame = 1
    }
    {
      Name = "Keepsake_MyModKeepsake"
      InheritFrom = "KeepsakeIcon"
      FilePath = ""
      EndFrame = 1
      StartFrame = 1
    }
  ]
}

Now we need to create a package containing our image.

This is done with a program called Deppth, made by modder quaerus, and a script to prepare the textures for import, made by modder Erumi.

For it to work you need to have installed Python if you haven't already.

Once you have installed Python, open a command prompt (type cmd in windows search) and run these commands one by one to install their dependencies :

pip install pillow
pip install lz4
pip install lzf
pip install PyTexturePacker
pip install scipy

To create our package we need to prepare a folder structure. Pick a location on your computer and create a folder. In this tutorial it will be called env.

Download Deppth and the texture packing script from the Hades Modding Discord.

In the #files-softwares channel click the pin icon and you'll find the two download links.

Extract both into your env folder and create two new folders, build and img.

In the img folder, we'll create a folder named MyMod and place our icon inside, which we rename to MyModIcon.

This will be the path to our icon, which we can put already in our file.

Game/Animations/GUIAnimations.sjson :

{
  Animations = [
    "_append"
    {
      Name = "Keepsake_MyModKeepsake_Small"
      InheritFrom = "KeepsakeTrayIconBacking"
      ChildAnimation = "Keepsake_MyModKeepsake_Child"
      EndFrame = 1
      StartFrame = 1
    }
    {
      Name = "Keepsake_MyModKeepsake_Child"
      InheritFrom = "KeepsakeTrayIcon"
      FilePath = "MyMod\MyModIcon"
      EndFrame = 1
      StartFrame = 1
    }
    {
      Name = "Keepsake_MyModKeepsake"
      InheritFrom = "KeepsakeIcon"
      FilePath = "MyMod\MyModIcon"
      EndFrame = 1
      StartFrame = 1
    }
  ]
}

Back to our env folder, right click texture_packing_erumi.py then click Edit with Visual Studio Code/Notepad++, or if you don't see this option, open with > choose another app.

This opens the file, and there we need to change SOURCE_DIRECTORY and BASENAME to img and MyMod respectively.

With this done, we can save and close the file. Run texture_packing_erumi.py, and it should create a few files.

Delete MyMod0.json and open MyMod0.atlas.json with Notepad++. Then, press CTRL+F and go to the Replace tab. Type .png in the first bar then click on Replace All, which should replace 1 occurence. Save then close the file.

In your build folder, create 2 folders, manifest and textures.

Place MyMod0.atlas.json inside the manifest folder.

In the textures folder, create a folder named atlases.

Place MyMod0.png inside the atlases folder.

Once done, go back to the env folder. SHIFT+Right click an empty spot in the window, then click Open Command Prompt window here or Open PowerShell window here.

In the command prompt, type and run this command : .\deppth-runner.py pk -s .\build\ -t MyMod.pkg

You should now have 2 new files in your env folder.

Our package is made, and we can now import it. In our mod folder we create a new folder Packages and place our new files in there.

In our modfile.txt we add the following lines :

To "Game/Animations/GUIAnimations.sjson"
SJSON "Game/Animations/GUIAnimations.sjson

To Win/Packages/MyMod.pkg_manifest
Replace "Packages/MyMod.pkg_manifest"

To Win/Packages/720p/MyMod.pkg_manifest
Replace "Packages/MyMod.pkg_manifest"

To Win/Packages/MyMod.pkg
Replace "Packages/MyMod.pkg"

To Win/Packages/720p/MyMod.pkg
Replace "Packages/MyMod.pkg"

For the game to load our package we add this bit of code in our main.lua file :

local mod = "MyMod"
local package = "MyMod"
ModUtil.Wrapbasetion( "SetupMap", function(base)
    DebugPrint({Text = "@"..mod.." Trying to load package "..package..".pkg"})
    LoadPackages({Name = package})
    return base()
end)

And finally, we need to change the icon our keepsake trait uses.

Data/TraitData.lua :

TraitData.MyModTrait =
{
	Icon = "Keepsake_MyModKeepsake",
	InheritFrom = { "GiftTrait" },
	InRackTitle = "MyModTrait_Rack",
	MyModTraitValue = 5,
	SignOffData =
	{
	  {
		Text = "MyModSignoff"
	  }
	}
}

Now we close our game, run Mod Importer, start our game then load our save. And we can see our icon is displayed in game correctly.

Conclusion

You now know the general process of mod making and have all the tools and knowledge needed to start making mods for Hades.

If you have any questions feel free to ask them in the modding discord.

Happy modding!