Make a mod

A mod is a small folder of Lua files that the Mod Loader runs when the game starts. This page builds the smallest useful one, then adds a window to it.

What a mod is

A mod is a folder in user/mods/<id>/ inside the game folder. It holds a manifest, a file named <id>_mod.lua that describes the mod, and the Lua files the manifest lists. The loader reads the manifest, runs those files, registers the mod's commands and builds its windows.

Rules

  • The id is lower-case: it starts with a letter and holds only letters, digits and _, such as hello or my_timer.
  • The folder is named for the id, and the manifest's id must match it.
  • Every file the mod ships is named for its id (hello_mod.lua, hello.lua, hello_icons.png ...), because the client's search path is flat and two mods must not use the same file name.
  • A mod that fails to load does not stop the others. /mods in game shows the error.
Step 1: say hello
  1. 1. Make the folder user/mods/hello/

    Inside the game folder, next to the loader's own files.

  2. 2. Write the manifest, hello_mod.lua
    ModLoader.register {
    	id          = "hello",
    	name        = "Hello",
    	version     = "0.1.0",
    	author      = "You",
    	description = "Says hello in chat.",
    	tags        = { "example" },
    
    	loader      = 1,
    	tested_with = "Omega / v26.09.416",
    
    	files = {
    		lua = { "hello.lua" },
    	},
    
    	commands = {
    		{ name = "hello", usage = "/hello [name]", help = "Say hello in chat" },
    	},
    	windows  = {},
    }

    id, name and version are required. loader = 1 is the loader API the mod was written for; the loader refuses a mod that needs a newer one. files.lua lists the Lua files to run, in order. Each entry in commands makes the loader register a slash command by its name; usage and help are for the people reading about your mod. windows lists the windows the mod builds, none yet.

  3. 3. Write the code, hello.lua
    local mod = ModLoader:get("hello")
    
    mod:onCommand("hello", function(self, args)
    	local who = string.match(args or "", "^%s*(%S+)") or "Atys"
    	self:say("hello, " .. who .. "!")
    end)

    ModLoader:get("hello") gives your mod's object. mod:onCommand(name, function) sets what /hello does: the function gets the mod and the text typed after the command. self:say(text) prints [Hello] text in the system info channel.

  4. 4. Add it to the list

    In user/mods/modloader_list.lua:

    ModLoader.installed = { "hello" }
  5. 5. Restart the client and type /hello

    At login you should see [Mods] 1 of 1 mods running. Then:

    /hello
    [Hello] hello, Atys!
    
    /hello Chris
    [Hello] hello, Chris!
Step 2: add a window (optional)

Windows are described in the game's interface XML. mod:addUI(xml) hands the loader a string of it, and the manifest lists the window's id so the loader can check it was created. Change the manifest's windows line:

windows  = { "hello_main" },

Then replace hello.lua with this:

local mod = ModLoader:get("hello")

mod:addUI([[
<group type="container" id="hello_main" x="200" y="-200" w="240" h="64" posref="TL TL"
	title="HELLO" global_color="true" header_color="UI:SAVE:WIN:COLORS:COM"
	movable="true" resizer="true" openable="false" opened="true" active="false" savable="true"
	pop_min_w="240" pop_max_w="240" pop_min_h="64" pop_max_h="64">
	<group id="header_closed" x="0" y="0" w="0" h="16" posref="TL TL" />
	<group id="header_opened" x="0" y="0" w="0" h="16" posref="TL TL" />
	<group id="content" x="0" y="0" sizeref="hw" w="0" h="0" posref="TL TL">
		<view type="text" id="greeting" posref="TL TL" x="8" y="-8" fontsize="12" shadow="true"
			global_color="false" color="255 255 255 255" hardtext="" />
	</group>
</group>
<tree node="hello_main" />]])

mod:onCommand("hello", function(self, args)
	local who = string.match(args or "", "^%s*(%S+)") or "Atys"
	self:say("hello, " .. who .. "!")

	local win = getUI("ui:interface:hello_main")
	win:find("greeting").hardtext = "Hello, " .. who .. "!"
	win.active = not win.active
end)

The window starts hidden (active="false"). getUI finds it by its full id, win:find("greeting") finds a view inside it, and setting hardtext changes the text. Now /hello also puts the name in the window and shows or hides it. The loader remembers whether a listed window was open when you logged out.

Window XML is easy to get wrong. Copy a working one, such as one of the windows in mods/menu/menu.lua in the mods repository, and change it a little at a time. The loader builds windows once per game session, so a change to the XML needs a client restart; it tells you so.

The full manifest for step 2

ModLoader.register {
	id          = "hello",
	name        = "Hello",
	version     = "0.1.0",
	author      = "You",
	description = "Says hello in chat.",
	tags        = { "example" },

	loader      = 1,
	tested_with = "Omega / v26.09.416",

	files = {
		lua = { "hello.lua" },
	},

	commands = {
		{ name = "hello", usage = "/hello [name]", help = "Say hello in chat" },
	},
	windows  = { "hello_main" },
}
Tips
  • Errors. /mods lists every mod with its status and, for one that failed, the error. The client's client.log has XML errors.
  • Faster testing. /lr reloads the loader and every mod's Lua without restarting the game. It only works if you add AllowDebugLua = 1; to client.cfg in the game folder. Windows that already exist are kept, so XML changes still need a restart.
  • Settings. A mod can keep numbers between logins: list them in the manifest as settings = { key = default } (whole numbers only), then read and write them with mod:setting("key") and mod:setSetting("key", value). A key you did not declare is an error. All mods share 64 saved slots, so keep the list short.
  • More. The mods repository has the full reference and the source of every mod on this site, which are good to read and copy from.