Events architecture
The Event System is a game-agnostic engine for scheduled, bounded, audited changes to a live game world. Core runs it and cannot name a single thing in your game.
The administrator’s view is Scheduled events.
The two sentences the design turns on
Section titled “The two sentences the design turns on”An event does not edit the world. It holds a lease.
Anything an event changes that already existed is borrowed, not set: the game keeps the
baseline, the site records both halves, and the lease carries its own deadline. When the
deadline passes the game restores the value — whether or not it ever hears from the site
again. A lease is never written to the game’s save file either, so a server restart also
puts every borrowed value back. That is the difference between automating a change and
handing an unattended process a [set command.
The module declares; core dispatches. A module says a verb exists, what it costs and what it needs; core decides whether it is permitted, when it runs, in what order, how many times, within what budget, what it created and who is told. Nothing crosses that line as a string core interprets — the browser posts an action id and a params object, both validated against the registry before anything is dispatched. There is no passthrough field and no place a request body can name a game command.
What is a table, and what deliberately is not
Section titled “What is a table, and what deliberately is not”Eleven core tables, no ORM, and no migration system — which makes every table a permanent commitment. The rule applied was: a table is for what must be queried, claimed or joined.
| Kind | Where it lives |
|---|---|
| Definitions, series, versions, runs, steps, budget, resources, participants, gates, settings, log | Tables |
| Phases | Configuration inside an immutable version snapshot. A phase has no identity a query needs; a step does |
| Actions, budget dimensions, conditions | Registry entries a module declares at load. A stored one would outlive the module that can perform it |
| A reward catalog | Neither. A reward is an ordinary action, so a granted reward is an ordinary ledger row |
The step is the unit of execution, and it is a row — one action invocation with a due time, a status, an attempt count and a claim. Retries, timeouts, duplicate execution and resumption after a crash are then all properties of that row rather than of a process’s memory, which is what lets the runner be killed mid-run and pick up where it stopped.
One run per occurrence, guaranteed by a unique index on the definition, the scope and the scheduled instant — not by the claim. Two application instances cannot both start the same occurrence, because the second insert fails.
Budgets are enforced in SQL
Section titled “Budgets are enforced in SQL”Consumption is spent with a conditional update:
UPDATE event_run_budget SET consumed = consumed + ? WHERE run_id = ? AND dimension = ? AND consumed + ? <= capNo transaction, no read-then-write, and no way for two concurrent steps to both squeeze past the same ceiling.
The ledger, and why cleanup is generated
Section titled “The ledger, and why cleanup is generated”Every world write appends to a resource ledger before it is confirmed: the run, the step, the owning module, an opaque kind and reference, and — for a borrowed value — the baseline alongside what was applied.
Teardown is then derived from the ledger, never authored, and runs on every terminal path: completion, cancellation and abort alike. An operator cannot be relied on to write the undo, and an aborted run never reaches the phase they wrote it in.
Two rules make that hold up:
- A unique index across non-reverted rows stops two events leasing the same target. The second one is refused rather than layered on top of the first.
- A restore is a compare-and-set. If the current value is not what the lease applied,
somebody else changed it since; the row is marked
driftedrather than stamped over. The ledger would rather say “I do not know what happened here” than lie about having undone it.
At-most-once, on a wire that can lose an answer
Section titled “At-most-once, on a wire that can lose an answer”Every command the site sends the game carries an idempotency key, and the game executes a given key at most once — a repeat is answered with the original reply rather than re-run.
Without it, a lost acknowledgement is indistinguishable from a command that never applied, so every world write has to be declared un-retryable and one has to be lost rather than risk doubling it. The key is what makes a world-changing step an ordinary retried row like any other.
Three layers, and the role check is only one of them
Section titled “Three layers, and the role check is only one of them”- Declaration — a module says a verb exists. That is code the operator installed; it is not a permission.
- Enablement — an admin turns an action on for this deployment and sets its caps. Nothing above a notification is on by default.
- Invocation — the role check, then the cap, then the game’s own switches. Admin routes are re-validated against the database on every request, so a demotion takes effect on the next click.
The game’s switches are the layer the site cannot reach: EventsEnabled and
AdminWriteEnabled live in a file on the shard host and are off out of the box, and the
game’s own ceilings refuse rather than clamp — because a silently shortened request leaves
the two halves disagreeing about what happened.
Where it meets everything else
Section titled “Where it meets everything else”- Engagement — core registers
event.triggers and owns none of the delivery. Who is told about a run is an ordinary rule. - The bridge — every world verb becomes a command on the same versioned wire the game already speaks, through the same sidecar. Core still holds no game connection.
- Teams — “this Team’s members” is already a registered audience, so a guild-scoped event needs no event-side feature at all.
- News — an event does not write posts. A core action links an existing post to a run and enqueues it through the announcement pipeline, so the in-game town crier and Discord arrive as legs that already exist.
What it deliberately does not do
Section titled “What it deliberately does not do”- No branching. The condition grammar is
and/or/notover comparisons, and the phase editor is a timeline rather than a canvas, because a canvas would promise power the engine has not got. - No delegation, grants or proposal queue. Permissions gate on the admin roles that already exist. The whole authorisation decision lives behind one function, which is what keeps a coordinator model a later option rather than a redesign.
- No event invoking another event. It already works by composition — a second event’s condition can be the first one completing.
- No mutation of game-owned content without a baseline. If it cannot be restored, it cannot be leased, and it is out.
Canonical documents
Section titled “Canonical documents”website/EVENTS.md
is the design of record;
website/MODULE_API.md
is the contract a module registers against; and
link/v7.md
is the wire protocol the world verbs travel on.