Skip to content
Andre Louis Issa edited this page May 25, 2024 · 6 revisions
Importing
Global Mod Object
Local Mod Config
Feature Control
Global Overrides
Mapping Tables
Wrapping Functions

Importing

Supergiant Games' lua allows us to use the Import keyword like so: Import "filepath/luafilename.lua"
All of their games since Pyre have a certain lua or lua parsed script file which imports and uses a lot of other scripts.

  • Pyre: Campaign.lua and MPScripts.lua
  • Hades: RoomManager.lua
  • Hades II: RoomLogic.lua

We can add an import statement to the end of that script in order to load the modded script file.
The only challenge is implementing all our desired changes from outside of the source.

Global Mod Object

The mod should create a global variable that shares the filename of the mod.
Sub-modules from the mod can be sub-tables of this variable instead.
This will be a table that stores its global functions and data that are not overwrites of base functions and data.
Other mods can check if this global exists to check if this mod is installed and act accordingly.

modName = {}
SaveIgnores["modName"] = true

Instead if you are using ModUtil do:

ModUtil.Mod.Register( "modName" )

Mods should use ModUtil whenever possible to maximise compatibility with other mods.

It should be part of SaveIgnores so that functions defined in it don't get deleted when the game saves (or worse: game crashes and save is bricked).
(And so that the table doesn't linger inside save data incorrectly indicating the mod is installed when it may not be)

You can add things to this object using

modObject.Data = ...

or

function modObject.Function( ... )
	...
end

Making these things global allows other mods to edit this mod in the same way that this mod can edit the base files.
Putting it all in one table with a restricted name allows mods to use global functions that so happen to have the same name without accidentally overwriting eachother while still allowing mods to deliberately overwrite eachother.

Local Mod Config

local config = {
	...
}

A table that contains all of the mod's config options easily editable by users.

Create global accessor functions if you want other mods to be able to change these.
Or if you want you can do this to let all mods access your local config:

modName.config = config

Feature Control

You can use config options to enable/disable features in your mod.
Useful if a feature relies on overwriting globals or adding functions to events as disabling that feature could solve compatability issues
(should the user decide they do not want this feature or if avoiding the issue is preferable).

You could do something like:

if config.featureXXX then
	DeployFeatureXXX( ... ) -- Overwrite globals particular to this feature here
end

This can get very complicated if you have features that rely on parts from other features so may require creativity in laying out your features.

Other mods changing these config settings remotely (using the global mod object or accessors) cannot be used for feature control as the globals will already have been overwritten by the time that mod is loaded.
So you may want to do something like this:

local function doMakeEdit()
    ... -- function definitions and feature control
    function doMakeEdit() end -- turn the function into a dud so it doesn't load again later
end
OnAnyLoad{ doMakeEdit }

or the shorthand from ModUtil

ModUtil.LoadOnce( function()
	... -- function definitions and feature control
end)

This will make it so mods have a chance to overwrite your configs before the features are implemented.

Global Overrides

If you are converting from the old mod format to this format, chances are you have a bunch of tables you've barely edited and a bunch of functions you've barely changed.
These can usually be minimised so their changes still allow base updates and other mods in the aspects from those tables or functions you didn't change.

However to start off with if you just want to be able to import your mod and don't care about extra layers of compatibility:

  1. Take note of all of the specific changes you made, exactly where they are and what you didn't change
  2. Put every variable, table, and function you changed into the mod file (and not the things you didn't change)
  3. Make adjustments where needed so your changes load in the proper places (see below)
  4. Consider storing references to everything you change somewhere other mods can access in case they need the old functionality.

Sometimes the data table you're trying to edit isn't loaded when your mod is loaded so you need to wait for the game to load.
In that case, you have to wrap your edit in a function and put it in the load hook:

local function doMakeEdit()
    ... -- all the necessary global table overwrites here
    function doMakeEdit() end -- turn the function into a dud so it doesn't load excess later
end
OnAnyLoad{ doMakeEdit }

or the shorthand from ModUtil

ModUtil.LoadOnce( function()
    ... -- all the necessary global table overwrites here
end)

For example:

MyMod = {}
SaveIgnores["MyMod"]

local config = {
  healthmult = 2
}
MyMod.config = config

local function doMakeEdit()
    HeroData.health = HeroData.health * config.healthmult
    function doMakeEdit() end
end
OnAnyLoad{ doMakeEdit }

Or, using ModUtil:

ModUtil.Mod.Register("MyMod")
local config = {
  healthmult = 2
}
MyMod.config = config
ModUtil.LoadOnce( function()
  HeroData.health = HeroData.health * config.healthmult
end)

(multiplies the player's health by a configurable value, this value has a chance to be edited by other mods before the game loads)

If you are completely overwriting functions or base tables, you should store it so other mods can access the original if they need to using ModUtil.BaseOverride( basePath, Value , modObject )

Mapping Tables

Many times we only want to edit parts of data nested into tables in various places, without overwriting the whole thing.
ModUtil provides functions we can use to safely edit these tables only changing what we want to change and leaving the rest intact to be as compatible and concise as possible.
(The benefits of this method are more tangible the more edits you want to do at once)

For changing (not deleting) values, we use ModUtil.Path.Table.Merge(InTable, SetTable).
For every key in the SetTable, the corresponding key in InTable will have the value set to the value in SetTable.
(If you want to entirely overwrite a value that was originally a table with another table you need to first set it to nil using Path.Table.NilMerge shown later)

It would look something like this:

ModUtil.Path.Table.Merge( BaseTable, {
    BaseTableKey1 = SetTableValue1
    BaseTableKey2 = {
        BaseTableValue2Key1 = SetTableValue2Value1,
        ...
    }
    ...
})

For example:

ModUtil.Path.Table.Merge( HeroData, {
    DefaultHero = {
        MaxHealthMultiplier = 2,
        MaxHealth = 150,
        DashManeuverTimeThreshold = 0.1,
        InvulnerableFrameDuration = 100.3,
    }
})

(Changes multiple default hero stats without needing to copy the rest of the table out from the original file)

If we wish to delete values (set them to nil), then we must use ModUtil.Path.Table.NilMerge(InTable, NilTable).
Because keys in tables that have the value nil do not get looped over and so won't be mapped to InTable. Instead we give some true values to the key we want to set to nil in a NilTable.

It would look something like this:

ModUtil.Path.Table.NilMerge( BaseTable, {
    BaseTableKey1 = true
    BaseTableKey2 = {
        BaseTableValue2Key1 = true,
        ...
    }
    ...
})

Every key that has a true value (not nil or false) in the NilTable provided will be set to nil in the InTable.
You should use Path.Table.NilMerge before Path.Table.Merge if you want to completely overwrite nested tables with your own tables.

For example:

ModUtil.Path.Table.NilMerge( HeroData, {
    DefaultHero = {
        HardModeForcedMetaUpgrades = true,
    }
})
ModUtil.Path.Table.Merge( HeroData, {
    DefaultHero = {
        HardModeForcedMetaUpgrades = {},
    }
})

(Removes all forced pact of punishment clauses. A small example but hopefully one can see how it can be used on larger sets of tables)

If the table you are mapping isn't loaded by the time your mod is loaded, you can queue your edits to be loaded when the game has loaded everything (as shown in Global Overrides).

For Example:

local function doSetSkellyHealth()
    ModUtil.Path.Table.Merge( UnitSetData.Enemies, {
        TrainingMelee = {
            MaxHealth = 10000,
        },
    })
    doSetSkellyHealth = function() end
end
OnAnyLoad{ doSetSkellyHealth }

(Sets Skelly's health to 10000 when the game first loads)

Mod Utility provides a standard shorthand for this with ModUtil.LoadOnce( triggerFunction )

ModUtil.LoadOnce( function()
    ModUtil.Path.Table.Merge( UnitSetData.Enemies, {
        TrainingMelee = {
            MaxHealth = 10000,
        },
    })
end)

Wrapping Functions

Almost every function from the base game is global meaning you can overwrite them from anywhere in the code.
So it is possible to edit functions from the base game without having to edit the files those functions are defined in.

There is a very particular way to do this which is optimum if:

  • You are editing a function from the base files (or any global function) and
  • Your change to the base function can happen strictly before or after its original call context

Then you can create a bunch of local copies of these base functions and then override their global functions.
So might look a bit like:

local basebasetionCall = basetionCall -- local copy of the base function
function basetionCall( ... ) -- overwrite the base function (global)
	prebasetionCall( ... ) -- your code here to be executed BEFORE the original call
	basebasetionCall( ... ) -- call the copied base function to preserve the call
        postbasetionCall( ... ) -- your code here to be executed AFTER the original call
end

Doing this means that you can add a behaviour to a function call without overwriting the function, this makes your edit more compatible with patches to the game and other mods.

For example:

local baseShowHealthUI = ShowHealthUI
function ShowHealthUI()
	ShowDepthCounter() 
	baseShowHealthUI()
end

(Makes it so it always displays the chamber counter during runs in Hades)

You can also modify {...} (varargs passed to the original function) during the overwritten function to change the behaviour when passed into the original function call.
For maximum compatibility this should not change the type of any arguments so potential future operations do not cause exceptions or cascading errors.

For example:

local baseHasResource = HasResource
function HasResource( name, amount )
	if amount then
		return baseHasResource( name, config.PurchaseCost*amount )
	end
	return baseHasResource( name, amount )
end

(Modifies the argument amount before sending it back into the original function)

One drawback of wrapping your functions means that another mod can't edit your changes without also overriding the original function.
If you want a mod to be able to modify your changes safely then you should allow a module to keep track of all the wrappings that happen.

If you are using ModUtil use ModUtil.Path.Wrap( baseName, wrapFunc, modObject ):

ModUtil.Path.Wrap( baseName, 
	function( base, ... )
		-- can return at any point
		... -- do stuff with base and the variables from the base
        	-- should call base and capture its return at least once somewhere
	end,
modObject )

For example:

ModUtil.Path.Wrap( "HasResource", function(base, name, amount )
	ModUtil.Hades.PrintStack( amount.." of "..name, Color.Green, Color.Black, 1 )
	return base(name, amount)
end, LoadTest)

(Displays in a list the resource and amount checked when a function compares your resources)