1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
|
package game
import (
"bytes"
"fmt"
"os"
"path/filepath"
"strings"
"gopkg.in/yaml.v3"
"thehouseoficarus/internal/casino"
"thehouseoficarus/internal/player"
)
var _ casino.PokerWallet = casinoWallet{}
type pokerBankFile struct {
Banks map[string][]int `yaml:"banks"`
}
func (g *Game) pokerBankPath(roomID int, cabinetID string) (string, error) {
if cabinetID == "" {
return "", fmt.Errorf("empty cabinet id")
}
for _, r := range cabinetID {
if !(r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' || r >= '0' && r <= '9' || r == '_' || r == '-') {
return "", fmt.Errorf("invalid cabinet id %q", cabinetID)
}
}
return filepath.Join(g.DataDir, "casino", "video_poker_banks", fmt.Sprintf("%d_%s.yaml", roomID, cabinetID)), nil
}
// Missing is a new cabinet. Corrupt/unreadable state is NOT an empty bank:
// refuse deals instead of silently destroying or replaying entitlements.
func (w casinoWallet) LoadPokerBanks(roomID int, cabinetID string) (map[string][]int, error) {
path, err := w.g.pokerBankPath(roomID, cabinetID)
if err != nil {
return nil, err
}
data, err := os.ReadFile(path)
if os.IsNotExist(err) {
return map[string][]int{}, nil
}
if err != nil {
return nil, err
}
var file pokerBankFile
decoder := yaml.NewDecoder(bytes.NewReader(data))
decoder.KnownFields(true)
if err := decoder.Decode(&file); err != nil {
return nil, err
}
for key, values := range file.Banks {
if len(strings.Split(key, "/")) != 4 || len(values) == 0 || len(values) > 10 {
return nil, fmt.Errorf("invalid multiplier bank %q", key)
}
for _, n := range values {
if n < 1 || n > 12 {
return nil, fmt.Errorf("invalid multiplier %d", n)
}
}
}
return file.Banks, nil
}
// PayPoker replaces the character payout/stake ledger AND the whole cabinet
// bank in one undo-journal transaction, on the game owner goroutine. A crash
// before commit restores both; startup then refunds the still-pending stake.
// A committed payout cannot leave an old bank that another player can collect.
func (w casinoWallet) PayPoker(amount int, stakeID string, roomID int, cabinetID string, banks map[string][]int) error {
candidate := *w.p
candidate.PendingWagers = append([]player.PendingWager(nil), w.p.PendingWagers...)
if stakeID == "" || len(takePendingWagers(&candidate, []string{stakeID})) != 1 {
return casino.ErrWalletPersistence
}
sum, err := addCreditsChecked(candidate.Credits, amount)
if err != nil {
return err
}
candidate.Credits = sum
path, err := w.g.pokerBankPath(roomID, cabinetID)
if err != nil {
return err
}
if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
return err
}
for _, directory := range []string{filepath.Dir(path), filepath.Dir(filepath.Dir(path)), w.g.DataDir} {
d, err := os.Open(directory)
if err != nil {
return err
}
err = d.Sync()
d.Close()
if err != nil {
return err
}
}
bankData, err := yaml.Marshal(pokerBankFile{Banks: banks})
if err != nil {
return err
}
charData, err := w.g.AccountStore.MarshalCharacter(&candidate)
if err != nil {
return err
}
charPath := w.g.AccountStore.CharPath(w.p.Name)
if err := os.MkdirAll(w.g.journalDir(), 0700); err != nil {
return err
}
txn := w.g.newTxn()
if err := txn.Replace(charPath, charData, 0600); err != nil {
return err
}
if err := txn.Replace(path, bankData, 0600); err != nil {
return err
}
if err := txn.Commit(); err != nil {
// A commit-point/unlink error is ambiguous. Finish recovery before any
// retry or later character save, then reconcile the actual pair of files.
// If recovery/directory sync fails, fail closed, as startup recovery does.
w.g.recoverJournals()
dir, syncErr := os.Open(w.g.journalDir())
if syncErr == nil {
syncErr = dir.Sync()
dir.Close()
}
if syncErr != nil {
panic(fmt.Sprintf("poker journal durability: %v", syncErr))
}
actualChar, cerr := os.ReadFile(charPath)
actualBank, berr := os.ReadFile(path)
charApplied := cerr == nil && bytes.Equal(actualChar, charData)
bankApplied := berr == nil && bytes.Equal(actualBank, bankData)
if !charApplied {
return err
} // recovered before-images; no live mutation
if !bankApplied {
panic("poker settlement recovery left inconsistent files")
}
}
w.p.Credits = candidate.Credits
w.p.PendingWagers = candidate.PendingWagers
return nil
}
|