diff options
| author | historia <[not public]> | 2026-07-17 19:21:01 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-07-17 19:21:01 -0400 |
| commit | 3d90a18c15ca54f262cfa61edba4866c91f29802 (patch) | |
| tree | b35abea56430ff3e92f586b785a828d39006c552 /internal | |
| parent | e6d0d46ac9977e77c7f19f41121ec466a3c53e56 (diff) | |
| download | thehouseoficarus-3d90a18c15ca54f262cfa61edba4866c91f29802.tar.gz | |
refactor: deterministic per-tick subscriber ordering (now fires in same order registered)
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/engine/tick.go | 21 | ||||
| -rw-r--r-- | internal/engine/tick_test.go | 62 |
2 files changed, 82 insertions, 1 deletions
diff --git a/internal/engine/tick.go b/internal/engine/tick.go index ff1bace..251373c 100644 --- a/internal/engine/tick.go +++ b/internal/engine/tick.go @@ -2,6 +2,7 @@ package engine import ( "math/rand" + "sort" "sync" "time" ) @@ -85,6 +86,17 @@ func (e *Engine) Stop() { } } +// processTick fires every due subscriber for the current engine tick. +// +// Subscribers are invoked in ascending subscription-ID order, i.e. the order in +// which they were registered with Subscribe. IDs are allocated monotonically +// (e.nextID++), so subscription order is a stable, deterministic sequence: +// the bootstrap master subscriber (cmd/thoi/main.go) subscribes at startup as +// ID 1 and therefore always fires first each tick, followed by each +// subsequently-registered tick (combat rounds, aggro rolls, scheduled respawns, +// etc.) in the order they registered. This guarantees predictable per-tick +// ordering across runs — critical for features whose behaviour depends on the +// relative ordering of combat resolution vs. movement vs. aggro checks. func (e *Engine) processTick() { e.mu.Lock() snapshot := make(map[uint64]*subscriber, len(e.subscribers)) @@ -93,7 +105,14 @@ func (e *Engine) processTick() { } e.mu.Unlock() - for id, sub := range snapshot { + ids := make([]uint64, 0, len(snapshot)) + for id := range snapshot { + ids = append(ids, id) + } + sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] }) + + for _, id := range ids { + sub := snapshot[id] sub.ticks++ if sub.ticks >= sub.interval { sub.ticks = 0 diff --git a/internal/engine/tick_test.go b/internal/engine/tick_test.go new file mode 100644 index 0000000..520ef19 --- /dev/null +++ b/internal/engine/tick_test.go @@ -0,0 +1,62 @@ +package engine + +import ( + "sync" + "testing" +) + +// TestProcessTickFiresInSubscriptionOrder asserts the engine invokes due +// tick subscribers in ascending subscription-ID order, i.e. the order in +// which they registered. This is the determinism contract that callers +// (notably the game's master subscriber at cmd/thoi/main.go, which subscribes +// first as ID 1 and must fire before combat/aggro/respawn per-tick callbacks) +// increasingly rely on. +func TestProcessTickFiresInSubscriptionOrder(t *testing.T) { + e := New() + var mu sync.Mutex + var order []int + + // Three interval-1 subscribers. Each appends its own index. All return + // true to stay subscribed (we Unsubscribe at the end). + id1 := e.Subscribe(1, func() bool { + mu.Lock() + order = append(order, 1) + mu.Unlock() + return true + }) + id2 := e.Subscribe(1, func() bool { + mu.Lock() + order = append(order, 2) + mu.Unlock() + return true + }) + id3 := e.Subscribe(1, func() bool { + mu.Lock() + order = append(order, 3) + mu.Unlock() + return true + }) + + if id1 >= id2 || id2 >= id3 { + t.Fatalf("ID monotonicity broken: got %d, %d, %d", id1, id2, id3) + } + + // processTick is synchronous: it invokes every due interval-1 subscriber + // inline in turn before returning. So `order` is fully populated here. + e.processTick() + + mu.Lock() + got := append([]int(nil), order...) + mu.Unlock() + + e.Unsubscribe(id1) + e.Unsubscribe(id2) + e.Unsubscribe(id3) + + if len(got) != 3 { + t.Fatalf("expected 3 subscriber firings, got %d: %v", len(got), got) + } + if got[0] != 1 || got[1] != 2 || got[2] != 3 { + t.Errorf("subscribers fired out of registration order: got %v, want [1 2 3]", got) + } +} |
