From f2d44f10f52d3fe68fdb19b518a330f7af9e1ed2 Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Wed, 16 Sep 2026 02:56:56 -0400 Subject: fix: disconnect race, rename ownership, reserved pickup, economy recovery retry/refund semantics --- internal/net/server.go | 35 ++++++++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) (limited to 'internal/net/server.go') diff --git a/internal/net/server.go b/internal/net/server.go index 42077a1..adc828c 100644 --- a/internal/net/server.go +++ b/internal/net/server.go @@ -67,13 +67,20 @@ type Session struct { PendingSkill string PendingLastFlag string PendingBackground bool - Disconnecting bool - DisconnectTicks int PendingUndigDir string PendingUndigRoom int promptVisible bool writeMu sync.Mutex + // Disconnect bookkeeping is written by the connection goroutine that + // discovered the dead transport (Hub.Remove's deferred path) and read + // by the game's per-tick DisconnectTick on the owner goroutine, so both + // fields are atomics. Plain fields would be a data race (and, worse, a + // lost disconnect: the owner's tick could spin forever on a stale + // value). + disconnecting atomic.Bool + disconnectTicks atomic.Int64 + // Outbound queueing. Transport sessions (telnet/TLS/websocket) are put // into queued mode by startWriter before their input goroutine begins: // Write* then enqueues into outbox, and the dedicated writer goroutine @@ -198,6 +205,25 @@ func NewHub() *Hub { } } +// StartDisconnect arms the graceful-disconnect countdown. Called by the +// connection goroutine that discovered the dead transport; DisconnectTick +// (on the game owner) counts the session down and hard-removes it. +func (sess *Session) StartDisconnect(ticks int) { + sess.disconnectTicks.Store(int64(ticks)) + sess.disconnecting.Store(true) +} + +// IsDisconnecting reports whether the graceful-disconnect countdown has +// been armed. Safe to call from any goroutine. +func (sess *Session) IsDisconnecting() bool { return sess.disconnecting.Load() } + +// TickDisconnectCountdown decrements the armed countdown and reports +// whether it has expired. Called only from the game's DisconnectTick +// (owner goroutine); safe against a concurrent StartDisconnect. +func (sess *Session) TickDisconnectCountdown() bool { + return sess.disconnectTicks.Add(-1) <= 0 +} + func (h *Hub) OnRemove(cb func(*Session)) { h.mu.Lock() defer h.mu.Unlock() @@ -219,9 +245,8 @@ func (h *Hub) Add(s *Session) { func (h *Hub) Remove(s *Session) { h.mu.Lock() - if s.Player != nil && !s.Disconnecting { - s.Disconnecting = true - s.DisconnectTicks = 10 + if s.Player != nil && !s.IsDisconnecting() { + s.StartDisconnect(10) onDisconnect := h.onDisconnect h.mu.Unlock() if onDisconnect != nil { -- cgit v1.2.3