Lua modules let you replace or extend any Lua function in the server without editing the original file. Each module is a self-contained .lua file that registers one or more overrides. The server loads your overrides at startup, and the original function is still accessible as super().
How Lua modules work
The module system uses an Override object to wrap an existing function. When the override runs, it sets up a closure environment where super points to the original function. This means you can call the original behavior from within your override.
-- Call the original, then add your behavior
m:addOverride('xi.homepoint.onTrigger', function(player, csid, index)
super(player, csid, index) -- Call original first
player:addHP(player:getMaxHP())
player:addMP(player:getMaxMP())
end)
You can also replace behavior entirely by omitting the super() call.
The module_utils.lua API
Every Lua module begins with require('modules/module_utils'). This loads the following API:
Module object
Module:new(name) -- Create a new module. Name must be at least 5 characters.
module:addOverride(target_func_str, func) -- Register an override for a dotted function path.
module:setEnabled(bool) -- Enable or disable the module at runtime.
Helper functions
-- applyOverride: Low-level override applier (used internally by addOverride)
applyOverride(base_table, name, func, fullname, filename)
-- xi.module.ensureTable: Safely create nested tables
-- Example: ensures xi.aName.anotherName exists before writing to it
xi.module.ensureTable('xi.aName.anotherName')
-- xi.module.modifyInteractionEntry: Patch a quest/event interaction at startup
xi.module.modifyInteractionEntry(filename, modifyFunc)
modifyInteractionEntry
Use xi.module.modifyInteractionEntry when you need to alter a quest or event that is loaded through the InteractionGlobal system. Reload the resource, apply your changes, and re-register it:
m:addOverride('xi.server.onServerStart', function()
super()
xi.module.modifyInteractionEntry('scripts/quests/ahtUrhgan/Rock_Bottom', function(quest)
-- Disable the first check so the quest cannot be started
quest.sections[1].check = function(player, status, vars)
return false
end
end)
end)
Do not hot-reload original quest files on a live server while using modifyInteractionEntry. Reloading the source file clears the cache entry and undoes your module’s changes.
Creating a Lua module
Create your file
Place your .lua file anywhere under modules/. The recommended location for custom modules is modules/custom/lua/.modules/custom/lua/homepoint_heal.lua
-----------------------------------
-- Home Point Healing
-- Heals players to full HP and MP when they use a home point.
-----------------------------------
require('modules/module_utils')
require('scripts/globals/homepoint')
-----------------------------------
local m = Module:new('homepoint_heal')
m:addOverride('xi.homepoint.onTrigger', function(player, csid, index)
player:addHP(player:getMaxHP())
player:addMP(player:getMaxMP())
super(player, csid, index)
end)
return m
Register it in init.txt
Add the file or its parent folder to modules/init.txt:# Load a single file
custom/lua/homepoint_heal.lua
# Or load everything in the folder
custom/lua/
Restart the server
Lua modules are loaded at server startup. Restart xi_map (or the relevant process) for your changes to take effect.
Module file structure
Every Lua module follows this pattern:
-- 1. Load module utilities
require('modules/module_utils')
-- 2. Optionally require scripts your overrides depend on
require('scripts/globals/someGlobal')
-- 3. Create a module object with a unique name (min. 5 characters)
local m = Module:new('my_module_name')
-- 4. Add one or more overrides
m:addOverride('xi.some.function.path', function(arg1, arg2)
-- optionally call original behavior
super(arg1, arg2)
-- your new behavior
end)
-- 5. Return the module
return m
Examples
Adding a GM command
Place command files in a commands/ subfolder. The filename becomes the command name.
modules/example/commands/test.lua
---@type TCommand
local commandObj = {}
commandObj.cmdprops =
{
permission = 1, -- 1 = GM level required
parameters = '',
}
local function double_print(player, str)
print(str)
player:printToPlayer(str, xi.msg.channel.SYSTEM_3, '')
end
commandObj.onTrigger = function(player)
double_print(player, 'Test print')
end
return commandObj
Register the containing folder in init.txt:
Overriding a mob spawn
You can override per-zone mob callbacks using their dotted path:
m:addOverride('xi.zones.South_Gustaberg.mobs.Leaping_Lizzy.onMobInitialize', function(mob)
-- Add a listener that fires whenever the mob spawns
mob:addListener('SPAWN', 'LL_SPAWN', function(mob)
-- Your logic here
end)
super(mob)
end)
Reverting a status effect
Override the onEffectGain callback to change what modifiers a status effect applies:
-- Revert Hasso to pre-SoA behavior (no Zanshin bonus)
m:addOverride('xi.effects.hasso.onEffectGain', function(target, effect)
effect:addMod(xi.mod.TWOHAND_STR, effect:getPower())
effect:addMod(xi.mod.TWOHAND_HASTE_ABILITY, 1000)
effect:addMod(xi.mod.TWOHAND_ACC, 10)
-- Omit the Zanshin modifier that was added later
end)
Directly modifying a global table
Not every customization requires addOverride. You can directly write to global tables if the module system just needs the values to be present at startup:
-- Override all pDIF caps with pre-WotG values
xi.combat.physical.pDifWeaponCapTable[xi.skill.HAND_TO_HAND] = { 2, 2 }
xi.combat.physical.pDifWeaponCapTable[xi.skill.DAGGER] = { 2, 2 }
xi.combat.physical.pDifWeaponCapTable[xi.skill.SWORD] = { 2, 2 }
-- ... and so on for each weapon skill
Loading order and super()
- Overrides are applied in the order modules are listed in
init.txt.
- Each override wraps the previous version of the function, so chaining works correctly: if two modules both override the same function, both
super() calls chain through.
- Always call
super() unless you intentionally want to discard the original behavior.