Skip to content

Format Specification

Andre Issa edited this page May 11, 2020 · 144 revisions

The wiki is under a rewrite so some information here may not be fully relevant

Mod File
Importing
Anchors & Hooks
Mod Information
Global Mod Object
Local Mod Config
Feature Control
Global Overrides
Mapping Tables
Wrapping Functions
Advantages
Limitations

Mod File

The mod importer (modimporter.py) will read the modfile.txt file in your mod's folder.

The simplest file to import all the lua scripts in the folder "Scripts" in your mod's folder:

Import "Scripts"

More generally

Import <files or folders>

Modifiers you can use before Import are:

Load Priority <number>

To <files or folders>

The mod file can also be used to merge sjson files

To <some sjson>
SJSON <a mod's sjson>

and xml files

To <some xml>
XML <a mod's xml>

More details will be given later


Importing

This format allows mods to be imported rather than as replacements of the base files.
It provides a standard by which to better integrate multiple mods together.

The core premise behind this is the fact that SuperGiantGames' lua allows us to use the Import keyword like so: Import "filepath/luafilename.lua"
All of their games since Transistor have a certain lua or lua parsed script file which imports and uses a lot of other scripts.

  • Transistor: AllCampaignScripts.txt
  • Pyre: Campaign.lua and MPScripts.lua
  • Hades: RoomManager.lua

We can add an import statement to the end of that script in order to load our mods!
The only challenge is implementing all our desired changes from outside of the source. (see Limitations)

Mods should ideally assume that they will be placed inside Mods folder in Content
Mods should create their own folder in Mods that includes whatever they may need to integrate with the original game content.

So a typical mod would be placed in Content/Mods
and the main script file this format pertains to would be placed in Content/Mods/ModName
And to import that script file you would add "Import "../Mods/ModName/ModName.lua" to the end of some base script

Anchors & Hooks

Will be relevant later if the feature can be realised by the mod importer. Ignore for now.

Mod Information

After all of the headers are out of the way we now have some room to put comments that won't be parsed by a loader.

This section can contain moderate size multiline comments explaining how to install, configure, and utilise the mod (perhaps linking to resources) as well as author and acknowledgement information shown in whatever way you want

For example:

--[[
Mod: Example Mod
Author: Author (and co-authors or others worth crediting)
	To install this mod place the script inside Content/Mods/MyModName/Scripts and add
	"Import "../Mods/MyModName/Scripts/MyModName.lua"" to the end of "RoomManager.lua"
	Or use the mod loader at LINK to automatically add it to the file.
]]

Global Mod Object

The mod should create a global variable that shares the filename of the mod. 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.RegisterMod( "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.
(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)

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.MapSetTable(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 MapNilTable shown later)

It would look something like this:

ModUtil.MapSetTable( BaseTable, {
    BaseTableKey1 = SetTableValue1
    BaseTableKey2 = {
        BaseTableValue2Key1 = SetTableValue2Value1,
        ...
    }
    ...
})

For example:

ModUtil.MapSetTable( 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.MapNilTable(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.MapNilTable( 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 MapNilTable before MapSetTable if you want to completely overwrite nested tables with your own tables.

For example:

ModUtil.MapNilTable( HeroData, {
    DefaultHero = {
        HardModeForcedMetaUpgrades = true,
    }
})
ModUtil.MapSetTable( 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.MapSetTable( 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.MapSetTable( 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 baseBaseFunctionCall = BaseFunctionCall -- local copy of the base function
function BaseFunctionCall( ... ) -- overwrite the base function (global)
	preBaseFunctionCall( ... ) -- your code here to be executed BEFORE the original call
	baseBaseFunctionCall( ... ) -- call the copied base function to preserve the call
        postBaseFunctionCall( ... ) -- 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.WrapBaseFunction( baseFuncName, wrapFunc, modObject ):

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

For example:

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

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

Advantages

  1. Base lua files do not need to be overwritten aside from the one import statement
  2. ^ Multiple mods can edit the same base file without needing to be merged
  3. Mods are encouraged to be built in a way that makes them more customisable using standard configs.
  4. Feature Control means that compatibility issues can be partially resolved in a modular way.
  5. Updates will not force reinstallation of mods (may only mean that the import statement needs to be added back)
  6. Updates will not force mods to update as often
  7. Mods do not need to include all the things that they did not edit from the base
  8. Mods can edit eachother
  9. The loader can map sjson files so many of the advantages for lua scripts also apply to that too:
    • Same capabilities as mapping data tables
    • Don't need to include things not edited from the base
    • Mods can edit eachother
    • Re-installation is simple (but not as simple as for lua files)

Limitations

There are limitations to this format:

  1. It is not possible to delete specific hook calls from the base files or other mod files
    (or overwrite/replace/modify specific hook calls)
  2. Changes to the xml and other asset files cannot be incorporated into this format directly
    (sjson can be incorporated externally through the loader now though! xml soon to come)
  3. It is not possible to add your own code to the middle of an existing function without overwriting it entirely
    (use feature control and store the old reference to better manage this)
  4. Raw (umnergable) assets like images/audio/font seemingly can't be imported/swapped from within the code so would need to be swapped by a loader

It remains a priority to eliminate/mitigate them as soon as possible, however they will probably require intervention by the devs or patching the game executables.

Clone this wiki locally