aboutsummaryrefslogtreecommitdiff
path: root/internal/game/casino.go
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-09-18 21:09:05 -0400
committerhistoria <[not public]>2026-09-18 21:09:05 -0400
commitff04d26ce90d6b411a04a2fb59f7c24ceefb161f (patch)
tree754bbd991d147f48822bda519586917e5440916c /internal/game/casino.go
parent0f812ccc307ea0550511ccced40b65dfb0cdaa91 (diff)
downloadthehouseoficarus-ff04d26ce90d6b411a04a2fb59f7c24ceefb161f.tar.gz
fix: keep uncertain payout append from double-paying on recovery
Diffstat (limited to 'internal/game/casino.go')
-rw-r--r--internal/game/casino.go25
1 files changed, 24 insertions, 1 deletions
diff --git a/internal/game/casino.go b/internal/game/casino.go
index 736add2..f111ed0 100644
--- a/internal/game/casino.go
+++ b/internal/game/casino.go
@@ -136,6 +136,22 @@ func (w casinoWallet) payCreditsFlagged(amount int, refund bool, stakeIDs []stri
return capErr
}
w.p.Credits = creditsAfter
+ // The append failed, but the record may still be IN the journal: a
+ // failed rollback (a truncate, or the sync that would make the
+ // truncation durable) leaves the bytes behind, and appendRetry can
+ // leave bytes from an earlier attempt even when its final error is
+ // definite. Record the attempted record's ID as open-payout evidence
+ // in the SAME save that makes this payout durable, so recovery
+ // recognizes a surviving record as already paid instead of
+ // re-applying it (a double-pay). When no record survived, the stale
+ // ID is pruned at the next startup.
+ attemptID := ""
+ if rec != nil {
+ attemptID = rec.ID
+ }
+ if attemptID != "" {
+ w.p.OpenPayouts = append(w.p.OpenPayouts, attemptID)
+ }
// The character file is the only durable record, so it must also
// carry the resolution evidence for the stakes this settlement
// resolves: a crash before the games' own ResolveStakes call would
@@ -148,6 +164,9 @@ func (w casinoWallet) payCreditsFlagged(amount int, refund bool, stakeIDs []stri
resolved := w.recordStakeEvidence(stakeIDs)
if serr := w.g.AccountStore.SaveCharacter(w.p); serr != nil {
w.p.Credits = creditsBefore
+ if attemptID != "" {
+ removeOpenPayout(w.p, attemptID)
+ }
if resolved > 0 {
w.p.ResolvedStakes = w.p.ResolvedStakes[:len(w.p.ResolvedStakes)-resolved]
}
@@ -157,7 +176,11 @@ func (w casinoWallet) payCreditsFlagged(amount int, refund bool, stakeIDs []stri
if refund {
kind = "refund"
}
- fmt.Fprintf(os.Stderr, "economy: %s payout of %d credits for %s could not be journaled; the character file is the only durable record\n", kind, amount, w.p.Name)
+ if errors.Is(err, errJournalAppendUncertain) {
+ fmt.Fprintf(os.Stderr, "economy: %s payout of %d credits for %s could not be journaled and the append could not be rolled back; recorded as open-payout evidence so recovery will not re-apply it\n", kind, amount, w.p.Name)
+ } else {
+ fmt.Fprintf(os.Stderr, "economy: %s payout of %d credits for %s could not be journaled; the character file is the only durable record\n", kind, amount, w.p.Name)
+ }
return nil
}
if capErr != nil {