Services
A named, documented interface one mod renders and others call, declared in the .ulmod, so the engine can check and replace it.
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.