aboutsummaryrefslogtreecommitdiff
path: root/internal/game/tick.go
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-09-18 05:31:09 -0400
committerhistoria <[not public]>2026-09-18 05:31:09 -0400
commit153acb7f644d755c45533b7c5311e3ffa47e8973 (patch)
tree7645e6f7b65bc3830b078e3b9336d2e6b199d584 /internal/game/tick.go
parent87d08f0151e643951769e323995f1c5ed89ee200 (diff)
downloadthehouseoficarus-153acb7f644d755c45533b7c5311e3ffa47e8973.tar.gz
fix: harden engine across casino, economy, concurrency, security, and content
Diffstat (limited to 'internal/game/tick.go')
-rw-r--r--internal/game/tick.go34
1 files changed, 32 insertions, 2 deletions
diff --git a/internal/game/tick.go b/internal/game/tick.go
index cd9b2fd..4181622 100644
--- a/internal/game/tick.go
+++ b/internal/game/tick.go
@@ -5,10 +5,25 @@ import (
"math/rand"
"thehouseoficarus/internal/behavior"
+ "thehouseoficarus/internal/net"
"thehouseoficarus/internal/player"
"thehouseoficarus/internal/world"
)
+// disconnectCombatGraceTicks bounds how many ticks DisconnectTick defers the
+// hard remove of a session that is still in combat, so an unresolvable fight
+// cannot keep the character's registry entry (and its "logged in elsewhere"
+// lockout) forever.
+const disconnectCombatGraceTicks = 5
+
+// disconnectCombatDeferrals counts, per session, how many ticks its pending
+// disconnect has already been deferred for combat. DisconnectTick is its only
+// reader/writer and every tick system runs on the owner goroutine, so the map
+// needs no lock. An entry is dropped when this path completes the hard remove;
+// a session hard-removed elsewhere (a second Hub.Remove, shutdown) leaves at
+// most one stale int behind.
+var disconnectCombatDeferrals = map[*net.Session]int{}
+
func (g *Game) DisconnectTick() {
if g.Hub == nil {
return
@@ -22,10 +37,20 @@ func (g *Game) DisconnectTick() {
g.Hub.Remove(sess)
continue
}
+ // An unresolvable fight (a task/worksite mob that takes no progress, a
+ // zero-damage stalemate) must not postpone the hard remove forever:
+ // the character registry entry only drops with the remove, so an
+ // unbounded deferral locks the player out ("logged in elsewhere")
+ // until a restart. Defer while the fight can still resolve, but only
+ // for a bounded grace period, then let the countdown proceed.
if g.Combat.Get(p.Name) != nil {
- continue
+ if deferred := disconnectCombatDeferrals[sess]; deferred < disconnectCombatGraceTicks {
+ disconnectCombatDeferrals[sess] = deferred + 1
+ continue
+ }
}
if sess.TickDisconnectCountdown() {
+ delete(disconnectCombatDeferrals, sess)
g.restoreGodPlayer(p)
g.saveCharacter(p)
g.Hub.HardRemove(sess)
@@ -76,6 +101,11 @@ func (g *Game) WanderTick() {
return
}
+ // One session snapshot per system invocation: the destination broadcast
+ // below used to re-snapshot every session (lock, allocate, sort) once per
+ // wandering mob/object.
+ sessions := g.Hub.AllSessions()
+
type moveEvent struct {
name string
fromRoom int
@@ -134,7 +164,7 @@ func (g *Game) WanderTick() {
}
for _, m := range moves {
- for _, sess := range g.Hub.AllSessions() {
+ for _, sess := range sessions {
p := sess.Player
if p == nil {
continue