From 9666676766bf21364f4fc4d0d1d336e72f2a91b4 Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Tue, 6 Oct 2026 12:57:23 -0400 Subject: refactor: split casino.go, dedup helpers, remove audit docs --- internal/behavior/behavior.go | 24 +++++++++++++++++------- internal/behavior/store.go | 20 ++++++++++++++++---- internal/behavior/yml_test.go | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 68 insertions(+), 11 deletions(-) (limited to 'internal/behavior') diff --git a/internal/behavior/behavior.go b/internal/behavior/behavior.go index e5039af..975624e 100644 --- a/internal/behavior/behavior.go +++ b/internal/behavior/behavior.go @@ -114,11 +114,11 @@ type Trigger struct { // SpawnMobConfig configures a transient mob spawned by a trigger step. The // mob id is required; other fields are optional. type SpawnMobConfig struct { - ID string `yaml:"id"` - OwnerOnly bool `yaml:"owner_only"` - DespawnOnLeave bool `yaml:"despawn_on_leave"` - DespawnRooms []int `yaml:"despawn_rooms"` - DespawnTicks float64 `yaml:"despawn_ticks"` + ID string `yaml:"id" json:"id"` + OwnerOnly bool `yaml:"owner_only" json:"owner_only"` + DespawnOnLeave bool `yaml:"despawn_on_leave" json:"despawn_on_leave"` + DespawnRooms []int `yaml:"despawn_rooms" json:"despawn_rooms"` + DespawnTicks float64 `yaml:"despawn_ticks" json:"despawn_ticks"` } // ShopConfig is a root-level mob property (mob YAML `shop:`). Buy prices are @@ -193,10 +193,20 @@ func (c *ShopConfig) SellPrice(value, stock int) int { // RestockInterval returns the effective restock interval (in ticks) for an item, // honoring the per-item override, then the configurable global default. func (c *ShopConfig) RestockInterval(item *ShopItem) int { + var ticks float64 if item != nil && item.RestockTicks > 0 { - return int(item.RestockTicks) + ticks = item.RestockTicks + } else { + ticks = ShopDefaultRestock } - return int(ShopDefaultRestock) + // restock_ticks is authored as a float, so truncation can turn a valid + // fractional interval (e.g. 0.5) into 0; the shop loop would then + // decrement to -1 and restock every tick. Clamp to at least one tick. + n := int(ticks) + if n < 1 { + n = 1 + } + return n } // FindItem returns the configured shop item with the given id, or nil. diff --git a/internal/behavior/store.go b/internal/behavior/store.go index d31fe84..3ea527c 100644 --- a/internal/behavior/store.go +++ b/internal/behavior/store.go @@ -32,13 +32,16 @@ func excludedContentPath(root, path string) bool { func BuildPathIndex(dir string) map[string]string { index := make(map[string]string) 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 || d.IsDir() { + return nil + } + id, ok := yamlID(d.Name()) + if !ok { return nil } if excludedContentPath(dir, path) { return nil } - id := strings.TrimSuffix(d.Name(), ".yaml") index[id] = path return nil }) @@ -48,13 +51,16 @@ func BuildPathIndex(dir string) map[string]string { func BuildRoomIndex(dir string) map[int]string { index := make(map[int]string) 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 || d.IsDir() { + return nil + } + id, ok := yamlID(d.Name()) + if !ok { return nil } if excludedContentPath(dir, path) { return nil } - id := strings.TrimSuffix(d.Name(), ".yaml") n, convErr := strconv.Atoi(id) if convErr != nil { // A non-numeric filename (for example a "1001_copy.yaml" @@ -187,6 +193,9 @@ func LoadDropTable(dataDir, id string) (*DropTableDef, error) { index := loadDropIndex(dataDir) path, ok := index[id] if !ok { + if id == "" || strings.Contains(id, "..") || strings.ContainsAny(id, `/\`) { + return nil, fmt.Errorf("invalid drop table id %q", id) + } path = filepath.Join(dataDir, "drops", id+".yaml") } data, err := os.ReadFile(path) @@ -268,6 +277,9 @@ func resolveDrop(dataDir string, drops []DropEntry, depth int) *DropEntry { Quantity: qty, Depletes: picked.Depletes, SuccessMessage: picked.SuccessMessage, + Tool: picked.Tool, + Level: picked.Level, + XP: picked.XP, } } return picked diff --git a/internal/behavior/yml_test.go b/internal/behavior/yml_test.go index 9452e9e..82acb19 100644 --- a/internal/behavior/yml_test.go +++ b/internal/behavior/yml_test.go @@ -27,3 +27,38 @@ func TestWalkYAMLDirIncludesYml(t *testing.T) { t.Fatalf("WalkYAMLDir ids = %v, want both a and b", ids) } } + +func TestBuildPathIndexIncludesYml(t *testing.T) { + dir := t.TempDir() + if err := os.WriteFile(filepath.Join(dir, "a.yaml"), []byte("x: 1"), 0644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, "b.yml"), []byte("x: 2"), 0644); err != nil { + t.Fatal(err) + } + idx := BuildPathIndex(dir) + if idx["a"] == "" || idx["b"] == "" { + t.Fatalf("BuildPathIndex = %v, want both a and b", idx) + } +} + +func TestBuildRoomIndexIncludesYml(t *testing.T) { + dir := t.TempDir() + if err := os.WriteFile(filepath.Join(dir, "1001.yaml"), []byte("name: a"), 0644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, "1002.yml"), []byte("name: b"), 0644); err != nil { + t.Fatal(err) + } + idx := BuildRoomIndex(dir) + if idx[1001] == "" || idx[1002] == "" { + t.Fatalf("BuildRoomIndex = %v, want both 1001 and 1002", idx) + } +} + +func TestLoadDropTableRejectsPathEscape(t *testing.T) { + dataDir := t.TempDir() + if _, err := LoadDropTable(dataDir, "../../../etc/passwd"); err == nil { + t.Fatal("accepted a path-escaping drop table id") + } +} -- cgit v1.2.3