Domain code does not read the clock: every server step has one time
Status: Accepted (2026-11-05)
Context
When every part of the server reads the system clock for itself, two checks of one packet see two different times, and a comparison at the edge of a window depends on which check runs first. Tests then need hooks in production code that age one timestamp of one subsystem at a time, and such a test never exercises the rest of the world moving on. The server is single-threaded per step, so nothing requires each decision to have its own time.
Decision
- Step time: one
Instantper server step: receiving a packet, a timer tick, a lifecycle transition, a deadline, a console command. Only the server loop sets it, at the entry of the step (Session::set_now); it never goes back.Session,Roster, the server facade, routing and Lua timers read it; domain modules receive it as a parameter. - It lives in one place, the
Roster.Sessionsets and reads it there; the facade sees it through theRoster. - Calendar time follows the step too. The loop anchors a wall-clock reading (unix seconds) at start and on every tick; what an outside party dates on the calendar, such as the expiry of a web sign-in token, is the step time converted through that anchor.
- Only limits on real execution read the real clock: the server loop (step time, timer schedule, uptime), the time limit of a Lua callback, the time limit of a storage statement, and log throttling. They bound how long code actually runs and how much it logs, not game time.
- Tests move the step time (
Session::advance); there are no hooks that age one subsystem. A test that needs time to pass lets all of it pass: every other timer of the world runs too.
Consequences
- All decisions of one step see the same time, so checks on the edge of a window do not depend on their order.
- New code in
src/outsidemain.rsdoes not callInstant::now(); it takes the step's time. - A step that was not opened sees the time of the previous one. That is why only the entry points of the server loop set the time.
- A few places still read the OS calendar clock directly: ban expiry, the
saved_atstamp of a saved map and the realtime mode of the world clock.
Alternatives considered
- Pass
nowto every method of the roster and the facade. The most explicit option, but time would appear in hundreds of signatures and in every Lua binding, with no gain over a step time: the step is single-threaded anyway. - A clock adapter (a trait, or an enum of real and manual clocks). It gives a real seam, but a manual clock is mutable state shared between the test and the session, again with no gain over a step time.
