Lua scripting

Libraries

Shared Lua code in Lua/*.lua, adding methods to every person, and building an API other mods can use.

Once a mod has more than a couple of scenes, the same code starts to repeat: a formula, a settings lookup, a helper that picks the right line. Put it in a library: a .lua file in your mod's Lua/ folder.

How libraries run

  • Every Lua/*.lua file of every enabled mod runs once, before the first scene of the session, in load order (and alphabetically within a mod).
  • They run in the same Lua world as scenes and UI scripts: a global a library defines is visible to every scene, every UI script and every other library that runs after it.
  • ModGame is available here: the same object scenes receive as Game. (Game itself is only set while a scene runs.)
lua-- Lua/nightlife.lua
Nightlife = Nightlife or {}

Nightlife.DRINKS = {
	{ id = "beer", price = 5, drunk = 8 },
	{ id = "shot", price = 4, drunk = 15 },
}

function Nightlife.priceOf(id)
	for _, d in ipairs(Nightlife.DRINKS) do
		if d.id == id then return d.price end
	end
	return 0
end

And in any scene:

luaPlayer:modifyStat("money", -Nightlife.priceOf("shot"))

Rules for library code

One global table per mod. Put everything in a table named after your mod (Nightlife), never loose global functions. Other mods' code lives in the same world.

Write it so it can run twice. Start with Nightlife = Nightlife or {}, and build lists the same way (Nightlife.DRINKS = Nightlife.DRINKS or {...}) when other mods may have added to them. A library must not do anything when it loads (no stat changes, no messages): define things, nothing else.

Only .ulscene files are scenes: a library never shares a name with one.

No state in library locals. A local at the top of a library is shared by everyone and is not saved. Keep per-person data in person variables and world data in globals.

Adding methods to people

ActorMethods is a table the game consults when a person is asked for a method it does not have built in. Anything you add there becomes a method of every person: the player, NPCs, people other mods create.

lua-- Lua/nightlife.lua
local M = ActorMethods

function M.drunkenness(a)
	return a:getStat("ynl_drunk") or 0
end

function M.drink(a, id)
	for _, d in ipairs(Nightlife.DRINKS) do
		if d.id == id then
			a:modifyStat("ynl_drunk", d.drunk)
			return true
		end
	end
	return false
end
lua-- any scene, in any mod
if Actor:drunkenness() > 50 then
	Game:Say(Actor, "Happy", "Heyyy, it's you!")
end
Player:drink("shot")
  • The first argument (a above) is the person the method was called on.
  • Built-in methods always win: you can add methods, never replace name or getStat.
  • Two mods adding the same method name overwrite each other (the later one wins), so prefix names that are not obviously yours.

Building an API other mods can use

The ul_Health and ul_Warrant modules are written as small public APIs: other mods say what happened, the module does the rest.

  1. Describe it in the header comment and in MODULE_DESCRIPTION, so authors know what they can call.
  2. Give people verbs, through ActorMethods: Actor:infect("flu"), Player:crime(2).
  3. Keep settings and registries on the global table: Health.CONDITIONS, Health.addCondition{...}.
  4. Let other mods extend your lists with an add… function that replaces an entry with the same id instead of duplicating it.
  5. Callers guard with Game:isModEnabled, so their mod still runs without yours.

A worked example: ul_Health

ul_Health/Lua/health.lua (abridged):

luaHealth = Health or {}

-- A registry other mods extend with Health.addCondition{ id = "allergy", ... }
Health.CONDITIONS = Health.CONDITIONS or {
	{ id = "cold", name = "Common cold", severity = 25, days = 3, random = 60 },
	{ id = "flu",  name = "Flu",         severity = 60, days = 6, random = 20 },
}

function Health.addCondition(c)
	for i, known in ipairs(Health.CONDITIONS) do
		if known.id == c.id then Health.CONDITIONS[i] = c return end
	end
	table.insert(Health.CONDITIONS, c)
end

-- Settings in story globals: 0 = default, so an untouched save reads right
function Health.frequency()
	local v = ModGame:getGlobal("ul_health_frequency")
	return v == 0 and 1 or v
end

-- Verbs on every person; their state lives in their own vars, so it is saved with them
local M = ActorMethods
function M.isSick(a) return a:getVar("ul_health_condition") ~= nil end
function M.infect(a, id) --[[ … ]] end
function M.treat(a, kind) --[[ … ]] end

Another mod then writes, in a food-poisoning scene:

luaif Game:isModEnabled("ul_Health") then
	Player:infect("food_poisoning")
end

Things that happen over time

Libraries are not called on a schedule: nothing tells them that a day started or an action finished. When something should change with time (an illness running its course, a wanted level cooling down), store the time it was last updated and catch up when you are next asked:

luafunction M.ynlSoberUp(a)
	local now = ModGame:GetHoursPassed()
	local last = a:getVar("ynl_drunk_checked") or now
	local hours = now - last
	if hours > 0 then
		a:modifyStat("ynl_drunk", -5 * hours)
	end
	a:setVar("ynl_drunk_checked", now)
end

function M.drunkenness(a)
	M.ynlSoberUp(a)          -- every read is up to date
	return a:getStat("ynl_drunk") or 0
end

This is what ul_Health's healthCheck does before every call. A HUD script's refresh also runs regularly while the map is on screen.

note

Event hooks for libraries (new day, action finished, scene ended, game loaded) do not exist yet. Until they do, the catch-up pattern above is the reliable way.