aboutsummaryrefslogtreecommitdiff
path: root/internal/game/economy.go
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-09-15 20:44:31 -0400
committerhistoria <[not public]>2026-09-15 20:44:31 -0400
commit61881fd0c1a7066293f552be1a747143f70d03ff (patch)
tree74256e16848fd3dbf66ebd8596395be5fa0f8136 /internal/game/economy.go
parentfcb806c3445ce56f2e57e208de773b4dac957c8d (diff)
downloadthehouseoficarus-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.go71
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