aboutsummaryrefslogtreecommitdiff
path: root/internal/behavior/store.go
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-09-18 01:50:17 -0400
committerhistoria <[not public]>2026-09-18 01:50:17 -0400
commit87d08f0151e643951769e323995f1c5ed89ee200 (patch)
treedc42a0b30ebb5a00dd7ba505cc4eaa44f4f308bc /internal/behavior/store.go
parent32a3c59cebf2a16277e54d715a67adfdbe00b456 (diff)
downloadthehouseoficarus-87d08f0151e643951769e323995f1c5ed89ee200.tar.gz
fix: harden economy recovery, shutdown concurrency, world validation, and inventory-loss paths
Diffstat (limited to 'internal/behavior/store.go')
-rw-r--r--internal/behavior/store.go35
1 files changed, 30 insertions, 5 deletions
diff --git a/internal/behavior/store.go b/internal/behavior/store.go
index 8112e1c..cd7a6b7 100644
--- a/internal/behavior/store.go
+++ b/internal/behavior/store.go
@@ -5,6 +5,7 @@ import (
"math/rand"
"os"
"path/filepath"
+ "strconv"
"strings"
"sync"
@@ -31,10 +32,14 @@ func BuildRoomIndex(dir string) map[int]string {
return nil
}
id := strings.TrimSuffix(d.Name(), ".yaml")
- var n int
- if _, scanErr := fmt.Sscanf(id, "%d", &n); scanErr == nil {
- index[n] = path
+ n, convErr := strconv.Atoi(id)
+ if convErr != nil {
+ // A non-numeric filename (for example a "1001_copy.yaml"
+ // backup) must never shadow a real room via a numeric prefix;
+ // skip it here and let validation flag it.
+ return nil
}
+ index[n] = path
return nil
})
return index
@@ -150,7 +155,20 @@ func pickWeighted(drops []DropEntry) *DropEntry {
return &drops[0]
}
+// maxDropTableDepth bounds sub-table recursion. A self- or mutual-reference
+// in authored drop tables would otherwise recurse until the goroutine stack
+// is exhausted (an unrecoverable fatal error); the validator also rejects
+// cycles at startup.
+const maxDropTableDepth = 32
+
func ResolveDrop(dataDir string, drops []DropEntry) *DropEntry {
+ return resolveDrop(dataDir, drops, 0)
+}
+
+func resolveDrop(dataDir string, drops []DropEntry, depth int) *DropEntry {
+ if depth > maxDropTableDepth {
+ return nil
+ }
picked := pickWeighted(drops)
if picked == nil {
return nil
@@ -160,7 +178,7 @@ func ResolveDrop(dataDir string, drops []DropEntry) *DropEntry {
if err != nil {
return nil
}
- resolved := ResolveDrop(dataDir, sub.Drops)
+ resolved := resolveDrop(dataDir, sub.Drops, depth+1)
if resolved == nil {
return nil
}
@@ -187,6 +205,13 @@ func ResolveDrop(dataDir string, drops []DropEntry) *DropEntry {
// (default 1) — each roll may produce a different item — and all results are
// returned. Tables are just weighted lists of items; this is skill-independent.
func ResolveDropList(dataDir string, drops []DropEntry) []DropEntry {
+ return resolveDropList(dataDir, drops, 0)
+}
+
+func resolveDropList(dataDir string, drops []DropEntry, depth int) []DropEntry {
+ if depth > maxDropTableDepth {
+ return nil
+ }
picked := pickWeighted(drops)
if picked == nil {
return nil
@@ -204,7 +229,7 @@ func ResolveDropList(dataDir string, drops []DropEntry) []DropEntry {
}
var out []DropEntry
for i := 0; i < rolls; i++ {
- if resolved := ResolveDrop(dataDir, sub.Drops); resolved != nil && resolved.ItemID != "" {
+ if resolved := resolveDrop(dataDir, sub.Drops, depth+1); resolved != nil && resolved.ItemID != "" {
out = append(out, *resolved)
}
}