aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-09-18 20:11:29 -0400
committerhistoria <[not public]>2026-09-18 20:11:29 -0400
commit0f812ccc307ea0550511ccced40b65dfb0cdaa91 (patch)
tree7f2f343985275a478ba7295a37fcd48871ca4956
parent79764732406d6484f67d3dec045483dcb5d08c28 (diff)
downloadthehouseoficarus-0f812ccc307ea0550511ccced40b65dfb0cdaa91.tar.gz
fix: world and casino integrity fixes
-rw-r--r--.gitignore1
-rwxr-xr-xcardsbin11187266 -> 0 bytes
-rw-r--r--internal/behavior/store.go16
-rw-r--r--internal/casino/credits.go6
-rw-r--r--internal/casino/manager.go6
-rw-r--r--internal/casino/money_regression_test.go2
-rw-r--r--internal/game/cmd_swapid.go16
-rw-r--r--internal/game/cmd_undig.go27
-rw-r--r--internal/game/look_entities.go8
-rw-r--r--internal/validate/validate.go20
10 files changed, 72 insertions, 30 deletions
diff --git a/.gitignore b/.gitignore
index bf7e510..358ebe1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,6 +11,7 @@
# does not also ignore the cmd/thoi/ source directory (and any new file in it).
/thoi
/mud
+/cards
.cache/
.npm/
.local/
diff --git a/cards b/cards
deleted file mode 100755
index 22d34e6..0000000
--- a/cards
+++ /dev/null
Binary files differ
diff --git a/internal/behavior/store.go b/internal/behavior/store.go
index a558f87..4051836 100644
--- a/internal/behavior/store.go
+++ b/internal/behavior/store.go
@@ -47,12 +47,24 @@ func BuildRoomIndex(dir string) map[int]string {
func WalkYAMLDir(dir string, fn func(path, id string, data []byte) error) error {
return filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error {
- if err != nil || d.IsDir() || !strings.HasSuffix(d.Name(), ".yaml") {
+ if err != nil {
+ // A missing root is an empty tree; any other walk error (for
+ // example an unreadable directory) must surface instead of
+ // silently validating nothing.
+ if os.IsNotExist(err) {
+ return nil
+ }
+ return err
+ }
+ if d.IsDir() || !strings.HasSuffix(d.Name(), ".yaml") {
return nil
}
data, readErr := os.ReadFile(path)
if readErr != nil {
- return nil
+ if os.IsNotExist(readErr) {
+ return nil
+ }
+ return readErr
}
id := strings.TrimSuffix(d.Name(), ".yaml")
return fn(path, id, data)
diff --git a/internal/casino/credits.go b/internal/casino/credits.go
index 631a4bc..19364da 100644
--- a/internal/casino/credits.go
+++ b/internal/casino/credits.go
@@ -76,6 +76,10 @@ func walletErrorMessage(err error, context ...string) string {
// payoutFailedMessage explains a settlement the wallet could not persist:
// the journal record for the payout stays open, so startup recovery will
// re-apply the owed amount. Games must NOT announce such a payout as paid.
+// payoutFailedMessage explains a settlement the wallet could not apply right
+// now. This covers both a persistence failure and a credit-cap block (the
+// journal record stays open either way), so it must not assert a persistence
+// failure as the cause. Recovery re-applies the owed amount.
func payoutFailedMessage() string {
- return "The house could not process your payout (persistence failure). Your settlement is recorded and will be restored when you return."
+ return "The house could not process your payout right now. Your settlement is recorded and will be restored when you return."
}
diff --git a/internal/casino/manager.go b/internal/casino/manager.go
index b71e5df..d9b2c3e 100644
--- a/internal/casino/manager.go
+++ b/internal/casino/manager.go
@@ -75,9 +75,9 @@ func (m *Manager) EnsureMachine(roomID int, cfg MachineConfig) *Machine {
m.mu.Lock()
defer m.mu.Unlock()
if machine := m.machines[key]; machine != nil {
- // DeepEqual (not ==) so the *bool rule fields compare by value;
- // == would treat every freshly parsed config as different and
- // discard an idle machine on every room load.
+ // DeepEqual (not ==) compares the config by value and stays correct
+ // as rule structs gain pointer/slice fields; == would panic or, for
+ // a struct with incomparable fields, fail to compile.
if reflect.DeepEqual(machine.Config, cfg) {
return machine
}
diff --git a/internal/casino/money_regression_test.go b/internal/casino/money_regression_test.go
index 31cd0e6..bb8263e 100644
--- a/internal/casino/money_regression_test.go
+++ b/internal/casino/money_regression_test.go
@@ -129,7 +129,7 @@ func TestBlackjackFailedPayoutNotAnnouncedAsPaid(t *testing.T) {
if strings.Contains(e.PrivateMessage, "You win! +") {
t.Fatalf("announced a successful payout after the wallet rejected it: %q", e.PrivateMessage)
}
- if strings.Contains(e.PrivateMessage, "persistence failure") {
+ if strings.Contains(e.PrivateMessage, "could not process your payout") {
return // the failure was explained to the player
}
}
diff --git a/internal/game/cmd_swapid.go b/internal/game/cmd_swapid.go
index 8b637fb..d1dbd68 100644
--- a/internal/game/cmd_swapid.go
+++ b/internal/game/cmd_swapid.go
@@ -66,13 +66,15 @@ func (g *Game) executeSwapID(sess *net.Session, args []string, rawInput string)
// Refuse while non-admin players are online: swapping room IDs relocates
// every character whose state embeds those IDs, which is disruptive to
// normal play. Admins may stay online (their own state is migrated too).
- for _, other := range g.Hub.AllSessions() {
- if other == sess || other.Player == nil {
- continue
- }
- if !g.checkAdmin(other) {
- sess.WriteLine("Cannot swap room IDs while non-admin players are online.")
- return
+ if g.Hub != nil {
+ for _, other := range g.Hub.AllSessions() {
+ if other == sess || other.Player == nil {
+ continue
+ }
+ if !g.checkAdmin(other) {
+ sess.WriteLine("Cannot swap room IDs while non-admin players are online.")
+ return
+ }
}
}
diff --git a/internal/game/cmd_undig.go b/internal/game/cmd_undig.go
index c752c86..88dd661 100644
--- a/internal/game/cmd_undig.go
+++ b/internal/game/cmd_undig.go
@@ -224,18 +224,23 @@ func (g *Game) performUndig(sess *net.Session, p *player.Player, targetID int) {
g.World.RebuildRoomIndex(g.DataDir)
- if p.RoomID == targetID {
- p.RoomID = 0
- }
-
- for _, other := range g.Hub.AllSessions() {
- if other.Player != nil && other.Player.RoomID == targetID {
- other.Player.RoomID = p.RoomID
- if g.Hub != nil {
- g.Hub.EnterRoom(other, p.RoomID)
+ // Everyone in the deleted room relocates to the admin's room. If the
+ // admin is somehow standing in the target room (0 is not a valid room),
+ // fall back to the starting room rather than teleporting players to a
+ // nonexistent room and soft-locking them.
+ destination := p.RoomID
+ if destination == targetID {
+ destination = g.StartingRoom
+ }
+
+ if g.Hub != nil {
+ for _, other := range g.Hub.AllSessions() {
+ if other.Player != nil && other.Player.RoomID == targetID {
+ other.Player.RoomID = destination
+ g.Hub.EnterRoom(other, destination)
+ other.WriteLine("The room around you dissolves. You find yourself elsewhere.")
+ g.doLook(other)
}
- other.WriteLine("The room around you dissolves. You find yourself elsewhere.")
- g.doLook(other)
}
}
diff --git a/internal/game/look_entities.go b/internal/game/look_entities.go
index 70077a3..e06d49f 100644
--- a/internal/game/look_entities.go
+++ b/internal/game/look_entities.go
@@ -230,7 +230,13 @@ func (g *Game) showRoomCasinos(sess *net.Session, room *world.Room) []string {
}
var lines []string
for _, table := range room.CasinoTables {
- name := casinoGameLabel(table.Game) + " table"
+ // Legacy slots declared under casino_tables are instantiated as the
+ // room's slots machine, so label them as a machine, not a table.
+ kind := "table"
+ if table.Game == casino.GameSlots {
+ kind = "machine"
+ }
+ name := casinoGameLabel(table.Game) + " " + kind
lines = append(lines, fmt.Sprintf("A %s is here.", g.colorize(sess, "casino_action", name)))
}
for _, machine := range room.CasinoMachines {
diff --git a/internal/validate/validate.go b/internal/validate/validate.go
index d77eafa..a7ce1b0 100644
--- a/internal/validate/validate.go
+++ b/internal/validate/validate.go
@@ -126,7 +126,7 @@ func Run(s Source) []Issue {
func validateRoomFilenames(dataDir string) []Issue {
dir := filepath.Join(dataDir, "rooms")
var issues []Issue
- _ = behavior.WalkYAMLDir(dir, func(path, id string, data []byte) error {
+ if err := behavior.WalkYAMLDir(dir, func(path, id string, data []byte) error {
if _, err := strconv.Atoi(id); err != nil {
rel := strings.TrimPrefix(path, dataDir+"/")
issues = append(issues, Issue{
@@ -136,7 +136,13 @@ func validateRoomFilenames(dataDir string) []Issue {
})
}
return nil
- })
+ }); err != nil {
+ issues = append(issues, Issue{
+ Level: "ERROR",
+ Type: "integrity",
+ Message: fmt.Sprintf("rooms: could not scan room files: %v", err),
+ })
+ }
return issues
}
@@ -159,7 +165,7 @@ func validateKnownFields(dataDir string, extra []KnownFieldTarget) []Issue {
var issues []Issue
for _, t := range targets {
dir := filepath.Join(dataDir, t.SubDir)
- _ = behavior.WalkYAMLDir(dir, func(path, id string, data []byte) error {
+ if err := behavior.WalkYAMLDir(dir, func(path, id string, data []byte) error {
dec := yaml.NewDecoder(bytes.NewReader(data))
dec.KnownFields(true)
if err := dec.Decode(t.NewDef()); err != nil {
@@ -171,7 +177,13 @@ func validateKnownFields(dataDir string, extra []KnownFieldTarget) []Issue {
})
}
return nil
- })
+ }); err != nil {
+ issues = append(issues, Issue{
+ Level: "ERROR",
+ Type: "unknown-field",
+ Message: fmt.Sprintf("%s: could not scan %s: %v", t.Label, t.SubDir, err),
+ })
+ }
}
// cards.yaml lives at the data root rather than a subdirectory.
if data, err := os.ReadFile(filepath.Join(dataDir, "cards.yaml")); err == nil {