aboutsummaryrefslogtreecommitdiff
path: root/building_guide
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-07-17 20:15:57 -0400
committerhistoria <[not public]>2026-07-17 20:15:57 -0400
commit9898d42f2d4ef61b5cbec7e212914d1495e04772 (patch)
treeafa604ad77a7a03832ebbd60e77c47d4961b9d77 /building_guide
parentc23c87b56fd568956d263bb8b8c5d26fbd8d01ee (diff)
downloadthehouseoficarus-9898d42f2d4ef61b5cbec7e212914d1495e04772.tar.gz
feat: add deterministic order to mob/player tick processing, document how tick processing works
Diffstat (limited to 'building_guide')
-rw-r--r--building_guide/tick_order.md141
1 files changed, 141 insertions, 0 deletions
diff --git a/building_guide/tick_order.md b/building_guide/tick_order.md
new file mode 100644
index 0000000..2d22ac9
--- /dev/null
+++ b/building_guide/tick_order.md
@@ -0,0 +1,141 @@
+## Per-Tick System Order
+
+The server runs on a fixed game tick (600ms by default; configurable via
+`tick_length` in `config.yaml`). Each tick, every online player's pending
+commands resolve and a fixed sequence of systems advance world state. The
+**canonical order** in which these per-tick systems fire is a load-bearing
+contract: combat resolution, deplete/restock timers, regen, respawns, and
+sequence-driven triggers all depend on it. Reordering it is a breaking change.
+
+### Source of truth
+
+The authoritative list lives in code, not in this doc:
+
+- `internal/game/tick_systems.go` — the `tickSystemOrder` slice and
+ `(*Game).RunTickSystems()`, the method that fires them in order.
+- `cmd/thoi/main.go` — the master per-tick subscriber, registered with the
+ engine as **subscription ID 1**, whose body is `g.RunTickSystems()`. The
+ engine (`internal/engine/tick.go` `processTick`) sorts subscribers by
+ registration ID, so the master subscriber fires first each tick, before
+ any dynamically-registered combat / aggro / respawn callbacks (which get
+ higher IDs as players act).
+- `internal/game/tick_systems_test.go` — `TestTickSystemOrder` fails if the
+ table drifts from the list below. `TestRunTickSystemsEmptyGameIsSafe`
+ guards the "safe to tick with no players connected" contract every system
+ must uphold (see "Per-system contracts" below).
+
+If you change the order, add a system, or remove one: update `tickSystemOrder`
+in `tick_systems.go`, the `expectedTickSystemOrder` list in
+`tick_systems_test.go`, and the table in this file. All three are intentional
+duplicates so any one of them drifting fails the test loudly.
+
+### Canonical sequence
+
+| # | System | What it does |
+| --- | ----------------------- | ------------------------------------------------------------------------------------------------------------------ |
+| 1 | `ProcessQueuedCommands` | Drains each player's free-queued commands and the globally timestamp-sorted active queue; advances walk sequences. |
+| 2 | `MoveTick` | Counts down `p.MoveTicks`; on reach-zero, completes a pending run/walk and re-prompts. |
+| 3 | `SequenceTick` | Advances every active trigger `sequence` (per-player and global) one step. |
+| 4 | `TransientMobTick` | Despawns owned / `DespawnOnLeave` mobs whose owner has left their room. |
+| 5 | `World.Tick` | World-internal: ground-item respawn/despawn/reserve timers and shared-object deplete timers. |
+| 6 | `MobStore.Tick` | Mob shop restock and mob HP regenerate. Order: by `InstanceID` so processing is stable across ticks. |
+| 7 | `RegenTick` | Player HP regeneration, accelerated by tech HP-regen multipliers. |
+| 8 | `EnergyRegenTick` | Run-energy regeneration for every online player. |
+| 9 | `DisconnectTick` | Counts down pending disconnects and hard-removes finished sessions (skipping players still in combat). |
+| 10 | `WanderTick` | Mob wandering (exit-based) and object wandering (fishing spots), with enter/leave messages to observers. |
+| 11 | `SharedDepletionTick` | Ticks shared-object deplete timers up (in use) or down (recovering). |
+| 12 | `FireTick` | Decays fire objects; on burn-out, spawns the removal item and cancels stokers. |
+| 13 | `AdvanceActions` | Advances every active player action by one tick; fires completion handlers per action type. |
+| 14 | `TechTick` | Drains battery for active tech, applies equipment / PreserveDrain reductions, and deactivates all on empty. |
+| 15 | `ConsumeTick` | Counts down `ConsumeCooldown` and `HomeTransportCooldown`. |
+| 16 | `BroadcastRespawns` | Flushes respawned world objects and broadcasts their `respawn_broadcast` lines to players in those rooms. |
+| 17 | `VisualTick` | Optional per-player "visual tick" line/counter for AFK feedback. |
+| 18 | `SneakTick` | Advances guard watch timers and sneaking players' tick counters. |
+| 19 | `FarmTick` | Crops grow, drain water, and yield on harvest (fires every `FarmTickInterval` ticks). |
+| 20 | `BuffTick` | Potion buff duration countdown; expires and removes effects. |
+| 21 | `SafespotTick` | Safespot unsafe-chance and decay-chance rolls per player. |
+| 22 | `HazardTick` | Room hazard damage to non-GodMode players. |
+
+### Determinism contract
+
+Three layers of ordering decide what happens on a given tick, in priority:
+
+1. **Subscriber order** — deterministic. `engine.processTick` sorts
+ subscribers by registration ID. The master subscriber (ID 1, set in
+ `cmd/thoi/main.go`) always fires first; every system in
+ `RunTickSystems` then runs in the table order above. Dynamic combat /
+ aggro / respawn subscribers registered later get higher IDs and fire
+ after the canonical 22 systems. This is asserted by
+ `internal/engine/tick_test.go::TestProcessTickFiresInSubscriptionOrder`.
+
+2. **Per-system iteration order** — deterministic, as of the deterministic
+ per-tick subscriber work. Systems that iterate over players or mobs use
+ stable ordering:
+ - `Hub.AllSessions()` and `Hub.PlayersInRoom` (`internal/net/server.go`)
+ sort sessions by `Session.seq`, a monotonic counter assigned in
+ `Hub.Add` in connection order.
+ - `MobStore.AllInstances()`, `MobStore.MobsInRoom()`, and `MobStore.Tick()`
+ (`internal/world/mob.go`) sort by `MobInstance.InstanceID`.
+
+ This means, for example, that cosmetic "X enters." / "Y leaves."
+ interleavings in `WanderTick`, shop-restock order in `MobStore.Tick`,
+ and per-player processing order in `RegenTick` / `HazardTick` /
+ `SneakTick` etc. are reproducible across runs.
+
+3. **Random-number generation** — **not deterministic across server
+ restarts.** The `math/rand` top-level functions used across the engine
+ (`rand.Float64`, `rand.Intn`, `rand.Int`, `rand.Shuffle`) are
+ auto-seeded at program startup (Go 1.20+; calling `rand.Seed` is
+ deprecated and is never done here). So combat hit rolls, drop rolls,
+ wandering exit picks, and minigame randomness differ between server
+ runs. Two ticks from the same seeded run are reproducible *relative* to
+ each other (the global source is deterministic), but a fresh server
+ start produces a different stream.
+
+ Only `rollAggroRand` (`internal/game/sys_combat.go`) is replaceable for
+ tests; the rest are global-function calls. Reproducible full-tick
+ traces (e.g. for record/replay debugging) would require threading a
+ seeded `*math/rand.Rand`/`math/rand/v2.ChaCha8` through the engine; not
+ currently done.
+
+### Per-system contracts (the empty-game invariant)
+
+Every system in the table must be safe to invoke on a `*Game` that has its
+stores initialized (`World`, `MobStore`, `Hub`, `queue`, etc. via
+`game.New` + `SetHub`) but **no players connected**. Concretely:
+
+- Systems that touch the hub: guard with `if g.Hub == nil { return }` at the
+ top, *or* guard each `Hub.*` call with `if g.Hub != nil`. Either is fine.
+ (Note: `Ticks.Start` runs *before* `SetHub` in `main.go`, so the master
+ subscriber can fire with `Hub == nil` during startup — guards are not
+ optional.)
+- Systems that iterate over maps/slices held on `*Game` (`sequences`,
+ `restTimers`, `guardWatchTimers`, `hackingStates`, …) must tolerate empty
+ / freshly-initialized maps. `game.New` initializes all of them; tests that
+ build a stub `*Game` must too (see `newRunTickSystemsTestGame`).
+
+Violating either of these is caught by
+`TestRunTickSystemsEmptyGameIsSafe` in `internal/game/tick_systems_test.go`.
+If you add a new system that needs a new field initialized, update that
+helper as well.
+
+### Notes for builders
+
+When writing content that depends on per-tick ordering (a trigger that
+should fire *after* regen, a sequence whose step messages should appear
+*before* the move completes, etc.), consult the table above and remember:
+
+- Trigger `sequences` advance in step 3, **before** movement completes in
+ step 2 has any knock-on effects like enter-room triggers — wait, step 2
+ (MoveTick) actually runs *before* step 3 (SequenceTick) because the table
+ is fired top-to-bottom. Double-check the exact step your event lives in.
+- `on_enter` / `on_exit` room triggers fire from command execution, not
+ from a per-tick system; they run inside `ProcessQueuedCommands` (step 1)
+ or `MoveTick` (step 2) when the move actually completes.
+- Shop restock (step 6) happens *before* `RegenTick` (step 7) and *before*
+ `AdvanceActions` (step 13); if a gathering action completes this tick
+ it does so in step 13, by which point shop state for this tick has
+ already advanced.
+
+When in doubt, the test in `tick_systems_test.go` is the executable
+specification. \ No newline at end of file