-
Notifications
You must be signed in to change notification settings - Fork 10
Format Specification
| Importing |
| Import Header |
| Anchors & Hooks |
| Mod Information |
| Global Mod Object |
| Local Mod Config |
| Feature Control |
| Global Overrides |
| Mapping Tables |
| Wrapping Functions |
| Advantages |
| Limitations |
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.luaandMPScripts.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 the folder Scripts and whatever else they may need to integrate with the original game content.
So a typical mod would be placed in Content/Scripts/Mods
and the main script file this format pertains to would be placed in Content/Mods/ModName/Scripts
And to import that script file you would add "Import "../Mods/ModName/Scripts/ModName.lua" to the end of some base script
The import header is to tell the mod importer how to treat this file. The mod importer will automatically import mods so users don't have to manually go through the steps of adding import lines.
Mods of this format must begin with
-- IMPORT @ DEFAULT
DEFAULT can be replaced by (space separated) filenames of a specific base files such as "RoomManager.lua"
IMPORT indicates that the mod is to be loaded after that file is loaded with the Import keyword
This header is really just there so the mod importer importmods.py knows what type of file this is supposed to be and where it is intended to go or be patched with if otherwise incapable of being imported by that loader
Optionally mods can place under this header a comment
-- PRIORITY N
Where N is some integer (or float/decimal). By default mods have priority 100.
Lower priorities get loaded first among the same import file. (And loaded alphabetically if they have the same priority)
The mod importer should output something like this:
Adding import statements for Hades mods...
RoomManager.lua
#1 ../Mods/ModUtil/Scripts/ModUtil.lua
#2 ../Mods/TESTING/Scripts/LoadTest.lua
#3 ../Mods/CodexMenu_PonyWarrior/Scripts/CodexMenu.lua
#4 ../Mods/modMagicEdits/Scripts/modMagicEdits.lua
1 files import a total of 4 mods.
Press any key to end program...
In this example ModUtil and LoadTest have lower priorities than 100 while CodexMenu and modMagicEdits have default priority.
Will be relevant later if the feature can be realised by the mod importer. Ignore for now.
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.
]]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"] = trueInstead if you are using ModUtil do:
ModUtil.RegisterMod( "modName" )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
modName.Data = ...or
function modName.Function( ... )
...
endMaking 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 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 = configYou 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
endThis 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.
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:
- Take note of all of the specific changes you made, exactly where they are and what you didn't change
- Put every variable, table, and function you changed into the mod file (and not the things you didn't change)
- Make adjustments where needed so your changes load in the proper places (see below)
- 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 overwriting functions, you should let mods access the old function if they need to
modName.Overrides = {}
...
modName.Overrides.BaseFunctionCallName=BaseFunctionCall
... -- override BaseFunctionCallSummarised by ModUtil as ModUtil.BaseOverride( BaseFunctionCallName , modObject )
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)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
endDoing 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 add the base function and the wrapped function to the global mod object:
modName.BaseFunctions = {}
modName.WrappedFunctions = {}
...
... -- wrap BaseFunctionCall
modName.BaseFunctions.BaseFunctionCall=baseBaseFunctionCall
modName.WrappedFunctions.BaseFunctionCall=BaseFunctionCallIf you are using ModUtil:
All of the above can be automated by using 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)
- Base lua files do not need to be overwritten aside from the one import statement
- ^ Multiple mods can edit the same base file without needing to be merged
- Mods are encouraged to be built in a way that makes them more customisable using standard configs.
- Feature Control means that compatibility issues can be partially resolved in a modular way.
- Updates will not force reinstallation of mods (may only mean that the import statement needs to be added back)
- Updates will not force mods to update as often
- Mods do not need to include all the things that they did not edit from the base
- Mods can edit eachother
There are limitations to this format:
- It is not possible to delete specific hook calls from the base files or other mod files
(or overwrite/replace/modify specific hook calls) - Changes to the xml, sjson and other asset files cannot be incorporated into this format directly
(so a way to implement the intent of these changes through scripts is required) - 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) - 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.