diff options
| author | historia <[not public]> | 2026-09-18 15:42:23 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-09-18 15:42:23 -0400 |
| commit | 67e01aab85607b9e601185c7d62dff576af3bb43 (patch) | |
| tree | 059134b2434eba7c14aa9d89f48c5a475feb0312 /internal/game/economy.go | |
| parent | 7eab582d8a8ee2af1a6baae7baf0696c7005de2e (diff) | |
| download | thehouseoficarus-67e01aab85607b9e601185c7d62dff576af3bb43.tar.gz | |
fix: close map OOM, admin traversal, transport safespot, room rekey, and legacy payout recovery
Diffstat (limited to 'internal/game/economy.go')
| -rw-r--r-- | internal/game/economy.go | 62 |
1 files changed, 37 insertions, 25 deletions
diff --git a/internal/game/economy.go b/internal/game/economy.go index 3505a86..9ce4404 100644 --- a/internal/game/economy.go +++ b/internal/game/economy.go @@ -885,6 +885,24 @@ func pruneOpenPayouts(j *economyJournal, p *player.Player) bool { // written later proves the round resolved (a win/push/refund already // returned the stake inside that payout) → settled with no refund. // Anything else stays open for operator inspection. +// reanchorOwedPayout rewrites an open payout record's checkpoints to the +// current durable balance so a later recovery reads it as still owed (the +// balance sits at the record's before-checkpoint). It is used when a recovery +// run could not pay every sibling in an identical-signature group: the paid +// siblings moved the durable balance, so leaving the unpaid remainder at its +// original checkpoints would let a later run mistake the balance for its own +// durability and settle the liability without paying it. The post-checkpoint +// respects the economy cap: when the credit cannot fit, before == after, so +// the record keeps reading as owed at this balance instead of claiming an +// impossible post-state. +func reanchorOwedPayout(rec *economyRecord, credits, credit int) { + rec.CreditsBefore = credits + rec.CreditsAfter = credits + if after, err := addCreditsChecked(credits, credit); err == nil { + rec.CreditsAfter = after + } +} + func (g *Game) recoverEconomy() { if g.Economy == nil { return @@ -1099,35 +1117,29 @@ func (g *Game) recoverEconomy() { settled[other.ID] = true continue } - if capBlocked { - // The credit cap blocked re-application. Re-anchor the - // record's checkpoints to the current durable balance so - // a later recovery re-applies it against a balance it can - // actually be paid from — without this, the balance a PAID - // sibling moved reads as this record's own durability and - // the liability is silently settled instead of paid. - // The re-anchored post-state must respect the cap: when it - // would exceed it, no reachable balance can pay the record, - // so the current balance is recorded as both checkpoints — - // the record then keeps reading as owed at this balance - // (retrying the re-application) instead of claiming an - // impossible post-state that a coincidental balance could - // be mistaken for as this record's own durability. - other.CreditsBefore = p.Credits - other.CreditsAfter = p.Credits - if after, err := addCreditsChecked(p.Credits, other.Credit); err == nil { - other.CreditsAfter = after - } + if capBlocked || paid > 0 { + // Either the credit cap blocked re-application, or an + // earlier sibling WAS paid before this record's save failed + // transiently. In both cases the durable balance already + // moved away from this record's original before-checkpoint, + // so re-anchor the checkpoints to the current durable balance + // — without this, the balance a PAID sibling moved reads as + // this record's own durability and the liability is silently + // settled instead of paid (and compaction then erases the + // settled sibling evidence, so a later run sees len(group)==1 + // and settles the remainder as "already durable"). + reanchorOwedPayout(other, p.Credits, other.Credit) settled[other.ID] = true // handled for the rest of this run _ = g.Economy.append(*other) continue } - // A TRANSIENT save failure blocked re-application and the - // balance still has room below the cap: keep the record open - // with its ORIGINAL checkpoints. Re-anchoring here would make - // the record unreadable against the unchanged durable balance - // (before-checkpoint mismatch) and permanently ambiguous; the - // original checkpoints let a later recovery retry cleanly. + // A TRANSIENT save failure blocked the FIRST re-application + // (paid == 0) and the balance still sits at the original + // before-checkpoint: keep the record open with its ORIGINAL + // checkpoints so a later recovery reads it as owed and retries + // cleanly. Re-anchoring here would make the record unreadable + // against the unchanged durable balance (before-checkpoint + // mismatch) and permanently ambiguous. settled[other.ID] = true // handled for the rest of this run } } |
