diff options
| author | historia <[not public]> | 2026-09-15 20:44:31 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-09-15 20:44:31 -0400 |
| commit | 61881fd0c1a7066293f552be1a747143f70d03ff (patch) | |
| tree | 74256e16848fd3dbf66ebd8596395be5fa0f8136 /internal/game/economy.go | |
| parent | fcb806c3445ce56f2e57e208de773b4dac957c8d (diff) | |
| download | thehouseoficarus-61881fd0c1a7066293f552be1a747143f70d03ff.tar.gz | |
fix: registration overwrite, recovery chip/payout edge cases, bulk-delete rollback
Diffstat (limited to 'internal/game/economy.go')
| -rw-r--r-- | internal/game/economy.go | 71 |
1 files changed, 55 insertions, 16 deletions
diff --git a/internal/game/economy.go b/internal/game/economy.go index fd12d07..04508be 100644 --- a/internal/game/economy.go +++ b/internal/game/economy.go @@ -400,17 +400,26 @@ func (g *Game) recoverEconomy() { switch { case p.Credits == rec.CreditsBefore: // The payout (and every identical sibling) was lost in the - // crash: the player is owed them all. + // crash: the player is owed them all. Only the siblings whose + // re-application actually became durable are settled; a save + // failure (e.g. the credit cap) leaves the unpaid remainder open + // for operator inspection — a liability is never erased. + paid := 0 for m := 0; m < len(group); m++ { if err := g.payCreditsDurable(p, rec.Credit); err != nil { fmt.Fprintf(os.Stderr, "economy recovery: payout save %s: %v\n", p.Name, err) break } + paid++ compensated++ } for _, other := range group { + if paid == 0 { + break + } g.Economy.settle(other) settled[other.ID] = true + paid-- } case p.Credits == rec.CreditsAfter && len(group) == 1: g.Economy.settle(rec) @@ -435,37 +444,61 @@ func (g *Game) recoverEconomy() { if p == nil { continue } + // Checkpoint comparisons use the DURABLE state, captured before any + // in-memory chip restoration below. creditsNow, chipsNow := p.Credits, countChips(p) + // Chips are recovered independently of the credit checkpoints. Any + // state where the chip debit is durable but the chips have not come + // back (post-debit chip balance) still owes the chips, even when a + // later payout record already resolves the credit side. A durable + // chip refund always restores the balance to ChipsBefore, so this + // cannot double-return. + chipsOwed := rec.Chips > 0 && chipsNow == rec.ChipsAfter + if chipsOwed && !g.giveChips(p, rec.Chips) { + // No inventory room for the chip return: keep the record open + // for operator inspection rather than silently dropping the + // chips; a later startup (or operator action) retries. + fmt.Fprintf(os.Stderr, "economy recovery: wager %s for %s: no inventory room for %d chips — kept open for operator inspection\n", + rec.ID, rec.Player, rec.Chips) + continue + } + saveChipReturn := func() error { + if !chipsOwed { + return nil + } + if err := g.AccountStore.SaveCharacter(p); err != nil { + p.RemoveItem("chips", rec.Chips) + fmt.Fprintf(os.Stderr, "economy recovery: chip return save %s: %v\n", p.Name, err) + return err + } + return nil + } switch { case creditsNow == rec.CreditsBefore && chipsNow == rec.ChipsBefore: // The debit never became durable: void it. g.Economy.void(rec) - case creditsNow == rec.CreditsAfter && chipsNow == rec.ChipsAfter: + case creditsNow == rec.CreditsAfter: if resolvedByPayout { // A later payout record proves the round resolved (the win, - // push, or refund already returned this stake inside that - // payout). Only the resolution marker was lost. - g.Economy.settle(rec) + // push, or refund already returned the credit stake inside + // that payout). Only the resolution marker was lost; the + // chip stake may still be outstanding. + if saveChipReturn() == nil { + g.Economy.settle(rec) + } continue } // The debit is durable but the round was lost in the crash: - // refund the stake (approved refund-on-recovery policy). + // refund the stake (approved refund-on-recovery policy). The + // chips (if owed) are already restored in memory; one durable + // save covers both the chip return and the credit refund. p.Credits += rec.Credit - if rec.Chips > 0 && !g.giveChips(p, rec.Chips) { - // No inventory room for the chip return: revert the credit - // and keep the record open for operator inspection rather - // than silently dropping the chips. - p.Credits = creditsNow - fmt.Fprintf(os.Stderr, "economy recovery: wager %s for %s: no inventory room for %d chips — kept open for operator inspection\n", - rec.ID, rec.Player, rec.Chips) - continue - } if err := g.AccountStore.SaveCharacter(p); err != nil { // Revert the in-memory refund so later records for this // player still compare against the durable balance; the // record stays open and a later startup can retry. p.Credits = creditsNow - if rec.Chips > 0 { + if chipsOwed { p.RemoveItem("chips", rec.Chips) } fmt.Fprintf(os.Stderr, "economy recovery: refund save %s: %v\n", p.Name, err) @@ -474,6 +507,12 @@ func (g *Game) recoverEconomy() { g.Economy.settle(rec) compensated++ default: + // Persist an owed chip return even when the credit side stays + // ambiguous: the record remains open either way, but the chips + // must not be silently lost. A failed save reverts them. + if saveChipReturn() != nil { + continue + } if resolvedByPayout { g.Economy.settle(rec) continue |
