Skip to main content
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

1

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
2

Register it in init.txt

Add the file or its parent folder to modules/init.txt:
init.txt
# Load a single file
custom/lua/homepoint_heal.lua

# Or load everything in the folder
custom/lua/
3

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:
example/commands/

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.

Build docs developers (and LLMs) love