aboutsummaryrefslogtreecommitdiff
path: root/internal/game/economy.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/game/economy.go')
-rw-r--r--internal/game/economy.go61
1 files changed, 45 insertions, 16 deletions
diff --git a/internal/game/economy.go b/internal/game/economy.go
index e917fa6..be3f5ef 100644
--- a/internal/game/economy.go
+++ b/internal/game/economy.go
@@ -234,6 +234,24 @@ func (rec *economyRecord) key() string {
return rec.Player
}
+// payoutIdentifies reports whether two journal records belong to the same
+// character identity. ID-carrying records match only on the stable ID: a
+// different character that reused the display name must never supply (or
+// consume) another character's resolution evidence. Legacy records without
+// IDs keep the historical name-based match among themselves.
+func payoutIdentifies(a, b economyRecord) bool {
+ if a.PlayerID != "" && b.PlayerID != "" {
+ return a.PlayerID == b.PlayerID
+ }
+ if a.PlayerID != "" || b.PlayerID != "" {
+ // Mixed legacy/ID records: refuse to match on name alone so an ID
+ // can never be resolved through a name that another character may
+ // have reused.
+ return false
+ }
+ return strings.EqualFold(a.Player, b.Player)
+}
+
// reloadStakes rebuilds the outstanding-stake index from the journal file:
// open wager records remain tracked (and refundable/resolvable) after
// recovery compaction.
@@ -435,16 +453,14 @@ func (g *Game) recoverEconomy() {
}
loadPlayer := func(rec *economyRecord) *player.Player {
if rec.PlayerID != "" {
+ // A record carrying a stable ID resolves to THAT identity only.
+ // Never fall back to the display name: a different character
+ // that later reused the name would collect the original
+ // character's payout and the original liability would be
+ // settled as paid. See TestRecoveryDoesNotPayReusedCharacterName.
if p, ok := playersByID[rec.PlayerID]; ok {
return p
}
- }
- if rec.Player != "" {
- if p, ok := playersByName[strings.ToLower(rec.Player)]; ok {
- return p
- }
- }
- if rec.PlayerID != "" {
// A character file added since the index was built: load every
// file once and match by stable ID.
if names, err := g.AccountStore.ListCharacterNames(); err == nil {
@@ -456,11 +472,20 @@ func (g *Game) recoverEconomy() {
}
}
}
+ // Identity not found (character deleted or unreadable): leave
+ // the record open so a later attempt (or operator inspection)
+ // can resolve it; do not silently drop or reassign money.
+ fmt.Fprintf(os.Stderr, "economy recovery: load %s (id %q): character not found\n", rec.Player, rec.PlayerID)
+ return nil
+ }
+ // Legacy record written before stable IDs existed: the (renameable)
+ // name is the only identity available.
+ if rec.Player != "" {
+ if p, ok := playersByName[strings.ToLower(rec.Player)]; ok {
+ return p
+ }
}
- // Character file unreadable: leave the record open so a later
- // attempt (or operator inspection) can resolve it; do not
- // silently drop money.
- fmt.Fprintf(os.Stderr, "economy recovery: load %s (id %q): character not found\n", rec.Player, rec.PlayerID)
+ fmt.Fprintf(os.Stderr, "economy recovery: load %s: character not found\n", rec.Player)
return nil
}
// laterPayoutEvidence reports whether any non-void payout record was
@@ -476,9 +501,10 @@ func (g *Game) recoverEconomy() {
// instead marks a refund in progress: its credit part is re-applied by
// phase 1 and the chip part (if the debit was durable) still comes
// back in chips.
- laterPayoutEvidence := func(name string, seq int) (resolved bool, settlement bool) {
+ laterPayoutEvidence := func(rec *economyRecord, seq int) (resolved bool, settlement bool) {
for _, fr := range folded {
- if fr.rec.Kind == "payout" && fr.rec.Player == name && fr.rec.Status != "void" && fr.seq > seq {
+ if fr.rec.Kind == "payout" && fr.rec.Status != "void" && fr.seq > seq &&
+ payoutIdentifies(fr.rec, *rec) {
resolved = true
if !fr.rec.Refund {
settlement = true
@@ -499,6 +525,9 @@ func (g *Game) recoverEconomy() {
// re-application as its own durability and silently drop the owed
// payout — the group is settled together instead.
type payoutSig struct {
+ // player is the record's stable identity key (PlayerID when known,
+ // else the name): grouping by display name alone would merge two
+ // different characters that reused the same name.
player string
before, after int
credit int
@@ -507,7 +536,7 @@ func (g *Game) recoverEconomy() {
for i := range folded {
rec := &folded[i].rec
if rec.Kind == "payout" && rec.Status == "open" {
- sig := payoutSig{rec.Player, rec.CreditsBefore, rec.CreditsAfter, rec.Credit}
+ sig := payoutSig{rec.key(), rec.CreditsBefore, rec.CreditsAfter, rec.Credit}
siblings[sig] = append(siblings[sig], rec)
}
}
@@ -521,7 +550,7 @@ func (g *Game) recoverEconomy() {
if p == nil {
continue
}
- sig := payoutSig{rec.Player, rec.CreditsBefore, rec.CreditsAfter, rec.Credit}
+ sig := payoutSig{rec.key(), rec.CreditsBefore, rec.CreditsAfter, rec.Credit}
group := siblings[sig]
switch {
case p.Credits == rec.CreditsBefore:
@@ -589,7 +618,7 @@ func (g *Game) recoverEconomy() {
}
for k := len(openWagers) - 1; k >= 0; k-- {
rec := &folded[openWagers[k]].rec
- resolvedByPayout, resolvedBySettlement := laterPayoutEvidence(rec.Player, folded[openWagers[k]].seq)
+ resolvedByPayout, resolvedBySettlement := laterPayoutEvidence(rec, folded[openWagers[k]].seq)
p := loadPlayer(rec)
if p == nil {
continue