aboutsummaryrefslogtreecommitdiff
path: root/internal/game/game.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/game/game.go')
-rw-r--r--internal/game/game.go28
1 files changed, 24 insertions, 4 deletions
diff --git a/internal/game/game.go b/internal/game/game.go
index 7f29d1f..ca38e27 100644
--- a/internal/game/game.go
+++ b/internal/game/game.go
@@ -135,6 +135,15 @@ func New(dataDir string, colorConfig *config.ColorsConfig, constantColorConfig *
// initialization cycle with the shutdown coordinator.
g.shutdownHook = g.RequestShutdown
g.Economy = newEconomyJournal(dataDir)
+ // Lifecycle-journal rollback MUST run before economy recovery: a rolled-
+ // back rename transaction restores an older character before-image, and
+ // if money compensation ran first its settled/compacted records would no
+ // longer be recoverable — the rollback would silently erase a payout
+ // that was already re-applied. Rollback also precedes every store read
+ // below (courses, mods, techs, craft index) that a leftover transaction
+ // may reference. See the TestEconomyRecoveryRunsAfterLifecycleRollback
+ // regression.
+ g.recoverJournals()
// Compensate open money records from any prior crash before any
// session can connect.
g.recoverEconomy()
@@ -144,9 +153,7 @@ func New(dataDir string, colorConfig *config.ColorsConfig, constantColorConfig *
g.LoadTechs()
g.buildCraftIndex()
g.loadAllFlagTriggers()
- // Journal recovery before any store reads files a leftover transaction
- // may reference, then one-time stable-identity migration.
- g.recoverJournals()
+ // One-time stable-identity migration.
g.backfillCharacterIDs()
g.ValidateAndLog()
return g
@@ -273,9 +280,22 @@ func (g *Game) saveAllOnlineCharacters() error {
// HandleSessionOnOwner is the entry point handed to the network server:
// connection goroutines submit one task per input line, so all game state
-// mutation executes on the owner goroutine.
+// mutation executes on the owner goroutine. Once shutdown has begun, new
+// input is REJECTED (not queued and not executed inline): the shutdown
+// sequence owns final persistence, and the transports keep draining until
+// they close, so an accepted line could mutate characters after the final
+// save or race the refund/save barrier.
func (g *Game) HandleSessionOnOwner(sess *net.Session, input string) {
+ if g.isShuttingDown() {
+ return
+ }
g.RunOnOwner(func() {
+ // Re-checked at task execution: the flag may flip between
+ // submission and execution, and RunOnOwner must never fall back
+ // to inline execution after the owner has stopped.
+ if g.isShuttingDown() {
+ return
+ }
g.HandleSession(sess, input)
})
}