Lua scripting

Services

A named, documented interface one mod renders and others call, declared in the .ulmod, so the engine can check and replace it.

warning

Format in progress, engine in build (2026-10-25). The keys are the vocabulary of Tools/ModSchemas/formats/ulservice.schema.json; the file is a Lua table.

Today a mod calls another through a global table (Debts, DatePlanner) guarded by Game:isModEnabled("ul_Debts"): 617 such calls in 393 files, and nothing that says what a mod provides or needs. A service is a name and an interface. The mod that renders it declares and documents it; a mod that calls it names it in its .ulmod; who renders it is indifferent, so a modder may replace ul_Health with their own as long as it provides injuries.

Declaring one

Services/*.ulservice in the providing mod:

luareturn {
	id = "injuries",
	version = 1,
	doc = "Physical harm on a person, healing over days, visible on the body and in the sheet.",
	impl = "Health.service",
	functions = {
		bruise = { args = { who = "person", severity = "int" }, doc = "A visible bruise, heals in severity*2 days." },
		isHurt = { args = { who = "person" }, returns = "bool" },
	},
}
Key Means
id The service name callers use.
version Bumped when the interface changes.
doc What it is for.
impl The implementing table, defined in the mod's Lua/: the path "Health.service" (_G.Health.service), or the table itself.
functions Each function with its args (name = "type": person, int, number, bool, text, table), returns and doc.

And in the .ulmod (Anatomy of a mod):

iniMODULE_PROVIDES: injuries

Using one

The caller names it in its .ulmod: MODULE_SERVICES for a service it cannot load without (the engine refuses the mod with a readable reason when nobody provides it), MODULE_USES for an optional one the mod adapts to.

luaif Services.has("injuries") then
	Services.injuries.bruise(p, 2)
end
Services.provider("injuries")   -- the providing mod's id, or nil

From a scene, the service effect:

lua{ service = "injuries", call = "bruise", who = "target", args = { severity = 2 }, optional = true }

optional = true skips the effect when the service is absent (a mod that only uses it); without it the validator requires the service under MODULE_SERVICES. At the call the engine checks that the caller declared the service, that the function exists and that the argument types pass.

The services ul_Base and ul_Core render

What the 79 *_lib.lua files used to reinvent, one implementation each: ledger (money), time, random, people (find, generate), notify, phone, agenda, witness, memory, outcomes, events. Their declarations are in ul_Base/Services/ and ul_Core/Services/.

What stays

A mod's own global table (Debts, DatePlanner) is fine inside the mod. No other mod calls it: the validator refuses a call into another mod's global. Game:isModEnabled keeps one legitimate question, "is this content pack installed" (a pack of scenes); for any function call, it is a service.