From 35ddd0ad2215760bb1d3dc7724480f179ea33fa6 Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Wed, 16 Sep 2026 21:05:39 -0400 Subject: fix: login account race, casino chip double-refund, room-create rollback, rename collision, fatal transport errors --- internal/game/casino.go | 37 +++++++++++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 4 deletions(-) (limited to 'internal/game/casino.go') diff --git a/internal/game/casino.go b/internal/game/casino.go index f9eb771..79d324a 100644 --- a/internal/game/casino.go +++ b/internal/game/casino.go @@ -103,16 +103,28 @@ func (w casinoWallet) payCreditsFlagged(amount int, refund bool) error { // resort: pay through the character file only. The save below is // the commit point — the money is durable even without a journal // record, so the settlement may be announced as paid. The stake - // resolution evidence (ResolveStakes) keeps recovery from treating - // the round as unresolved; without a payout record, an open wager - // can no longer be distinguished from a lost one, so the balance - // itself is the only record of what happened. + // resolution evidence (recordStakeEvidence) recorded in that same + // file keeps recovery from treating the round as unresolved; + // without a payout record, an open wager can no longer be + // distinguished from a lost one, so the balance itself is the only + // record of what happened. if capErr != nil { return capErr } w.p.Credits = creditsAfter + // The character file is the only durable record, so it must also + // carry the resolution evidence for this player's open stakes: a + // crash before the games' own ResolveStakes call would otherwise + // leave the wager records open with checkpoint balances that no + // longer match, and recovery would refund chip stakes the paid + // settlement had already consumed. The single save below is the + // commit point for the payout AND the resolution. + resolved := w.recordStakeEvidence() if serr := w.g.AccountStore.SaveCharacter(w.p); serr != nil { w.p.Credits = creditsBefore + if resolved > 0 { + w.p.ResolvedStakes = w.p.ResolvedStakes[:len(w.p.ResolvedStakes)-resolved] + } return casino.ErrWalletPersistence } kind := "settlement" @@ -174,6 +186,23 @@ func (w casinoWallet) ResolveStakes() { } } +// recordStakeEvidence adds every currently open stake of the player to the +// file-side resolution evidence list and returns how many IDs were newly +// recorded. Used when the economy journal is unavailable: the open wager +// record IDs in the character file prove the round resolved even though no +// settled marker (or payout record) could ever be journaled. The caller +// must persist the character file for the evidence to count. +func (w casinoWallet) recordStakeEvidence() int { + added := 0 + for _, rec := range w.g.Economy.openStakes(economyKey(w.p)) { + if !resolvedStakeRecorded(w.p, rec.ID) { + w.p.ResolvedStakes = append(w.p.ResolvedStakes, rec.ID) + added++ + } + } + return added +} + // RefundOutstanding returns every outstanding stake to the player: chips // come back as chips, credits through the durable payout path. The stakes // are resolved only once the return is durable; on failure the chip return -- cgit v1.2.3