OPX//77
opx_infinityConcepts

Modules and contracts

Last updated on

How a module is declared, how modules depend on each other, how contracts are published and read, and the Init, Api, Start and Stop phases.

A module is one gameplay concern in its own folder, modules/<id>/. Modules never reach into each other's files; they talk through contracts — a table of functions one module publishes and others ask for by name. The registry (core/shared/registry.lua) and the lifecycle (core/shared/lifecycle.lua) run all of this.

Declaring a module

modules/<id>/module.lua is a shared_script, so it runs on both sides.

modules/thing/module.lua
local M = OPX.Modules.Declare{
	id = 'thing',
	side = 'both',                -- 'server', 'client' or 'both'
	fatal = false,                -- true: the resource reports a failed boot if this module fails
	requires = { 'character' },   -- hard dependencies
	optional = { 'downed' },      -- soft dependencies: only change the order
}
FieldMeaning
idRequired. Unique string; declaring an id twice raises.
sideWhere the module runs. Default 'both'. On the other side it is marked absent.
requiresModules that must be running. If one is missing, disabled or failed, this module is unavailable and never runs.
optionalModules that run first when present. Nothing happens if they are missing.
fataltrue: a failure of this module makes OPX.Modules.Run answer false.
  • Declare returns the module's own table (M). Its other files get it back with OPX.Modules.Get('<id>') and hang their functions on it.
  • The runtime only reads Settings and the four phase functions from M.

API: OPX.Modules.Declare.

Settings

M.Settings is OPX.Config.MODULES[id], the table set by config/<id>.lua.

Do not copy M.Settings into a file-scope local

The platform runs every shared_script before any server_script. Three config files are server scripts (config/server.lua, config/vehicles.lua, config/theme.lua), so at declare time their module still sees an empty table. The registry re-points M.Settings at the real table once every file has loaded (OPX.Modules.Rebind). Read M.Settings inside your phases.

enabled = false in the config turns the module off: it is marked disabled and its dependants become unavailable.

The lifecycle

Phases run in dependency order (a module runs after everything it requires or lists as optional). A cycle is refused at boot with module dependency cycle: a -> b -> a.

PhaseUse it toRules
M.Init()Build state, read settings, add tables (OPX.Schema.Add), declare tunables (OPX.Tune.Declare).Do not call other modules.
M.Api()Publish your contract with OPX.Api.Provide.Contracts may only be published here.
M.Start()Register events, commands, scheduler jobs; read the database.Runs on a thread and may yield.
M.Stop()Release what you hold: focus, controls, cameras, jobs.Runs in reverse order, also for a module that failed half-way.
  • Between Api and Start the server publishes the tunables and applies the schema, so Start is the first phase that may touch the database.
  • Each phase yields one frame (Wait(0)) after each module, resetting the client's instruction budget (Two runtimes).

Module states

/opx.modules (server) and /opx.client (client) print one line per module with its state and reason — see diagnostics.

StateMeaning
declaredWaiting to run.
startedAll phases ran.
disabledenabled = false in its config.
absentIts side is the other runtime.
unavailableA required module is missing, disabled or failed. The reason names it.
failedA phase raised. Reason: <phase> failed: <error>.
stoppedStop ran.

When a module fails, every contract it already published is withdrawn and its dependants are dropped after the phase.

Contracts

A plain table of functions with a name and a version — the only supported way for one module to use another.

-- modules/thing/server/main.lua
function M.Api()
	OPX.Api.Provide('thing', 1, {
		DoIt = function(source) ... end,
	})
end

-- another module
local thing = OPX.Api.Get('thing')   -- nil when 'thing' is not running
if thing then thing.DoIt(source) end
FunctionUse
OPX.Api.Provide(name, version, table)Publish. Only one module may provide a name; a second one raises.
OPX.Api.Get(name, minimum?)Read. nil when nobody provides it (or only an older version). For soft dependencies.
OPX.Api.Require(name, minimum?)Read or raise. For a module listed in requires.
OPX.Api.Versions()Every published contract, name -> version.
  • Contracts live in one Lua VM: server and client each have their own set, and a separate resource cannot call them.
  • Each module documents its contract on its server and client pages (module list).

Call OPX.Api.Get when you need it, not at file scope

Contracts exist only after the Api phase. Resolve them in Start or at the moment of use.

On this page