From 194164a77eb4f2479b94c19e6f0e1278ab608781 Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Fri, 18 Sep 2026 16:13:49 -0400 Subject: fix: more casino, economy, gameplay, security, and world data hardening gaps --- internal/game/game.go | 38 ++++++++++++++++++++++++++------------ 1 file changed, 26 insertions(+), 12 deletions(-) (limited to 'internal/game/game.go') diff --git a/internal/game/game.go b/internal/game/game.go index 3ffc72e..5ad1033 100644 --- a/internal/game/game.go +++ b/internal/game/game.go @@ -79,6 +79,13 @@ type Game struct { sequences map[string]*sequence globalSeqs []*sequence + // disconnectDeferrals counts, per session, how many ticks its pending + // disconnect has been deferred for combat. Per-Game (not package-level) + // so a second Game in one process cannot inherit stale counters, and + // pruned each tick so a session hard-removed elsewhere cannot pin its + // Player in memory for the life of the process. + disconnectDeferrals map[*net.Session]int + shutdownMu sync.Mutex // shutdownOnce guards RequestShutdown; transportStop is the registered @@ -134,18 +141,19 @@ func New(dataDir string, colorConfig *config.ColorsConfig, constantColorConfig * // Entropy-seeded, not clock-seeded: a wall-clock seed makes every // shoe and reel stream reconstructible. casino.NewManager(seed) stays // the deterministic test seam. - Casino: casino.NewManagerEntropy(), - flagIndex: newFlagTriggerIndex(), - queue: NewCommandQueue(), - safespot: NewSafespotManager(), - ValidationConfig: valConfig, - StartingRoom: startingRoom, - loggedInChars: make(map[string]*net.Session), - restTimers: make(map[string]uint64), - guardWatchTimers: make(map[string]int), - hackingStates: make(map[string]*hacking.Session), - sequences: make(map[string]*sequence), - shutdownDone: make(chan struct{}), + Casino: casino.NewManagerEntropy(), + flagIndex: newFlagTriggerIndex(), + queue: NewCommandQueue(), + safespot: NewSafespotManager(), + ValidationConfig: valConfig, + StartingRoom: startingRoom, + loggedInChars: make(map[string]*net.Session), + restTimers: make(map[string]uint64), + guardWatchTimers: make(map[string]int), + hackingStates: make(map[string]*hacking.Session), + sequences: make(map[string]*sequence), + disconnectDeferrals: make(map[*net.Session]int), + shutdownDone: make(chan struct{}), } g.owner = newOwner() // Runtime assignment (not initializer-level) so the command registry, @@ -700,6 +708,12 @@ func (g *Game) ProcessQueuedCommands() { }) for _, qc := range actives { + // A queued command can outlive its session (logout, disconnect, or a + // re-login on a new connection): never dereference a departed one on + // the owner goroutine. Mirrors the free-queue guard above. + if qc.Session == nil || qc.Session.Player == nil { + continue + } raw := qc.Command if qc.Args != "" { raw += " " + qc.Args -- cgit v1.2.3