diff options
| author | historia <[not public]> | 2026-09-18 01:50:17 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-09-18 01:50:17 -0400 |
| commit | 87d08f0151e643951769e323995f1c5ed89ee200 (patch) | |
| tree | dc42a0b30ebb5a00dd7ba505cc4eaa44f4f308bc /internal/behavior | |
| parent | 32a3c59cebf2a16277e54d715a67adfdbe00b456 (diff) | |
| download | thehouseoficarus-87d08f0151e643951769e323995f1c5ed89ee200.tar.gz | |
fix: harden economy recovery, shutdown concurrency, world validation, and inventory-loss paths
Diffstat (limited to 'internal/behavior')
| -rw-r--r-- | internal/behavior/store.go | 35 | ||||
| -rw-r--r-- | internal/behavior/types.go | 1 |
2 files changed, 31 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) } } diff --git a/internal/behavior/types.go b/internal/behavior/types.go index 86115a7..ad10ef4 100644 --- a/internal/behavior/types.go +++ b/internal/behavior/types.go @@ -210,5 +210,6 @@ type DropEntry struct { } type DropTableDef struct { + ID string `yaml:"id"` Drops []DropEntry `yaml:"drops"` } |
