aboutsummaryrefslogtreecommitdiff
path: root/internal/behavior
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-10-06 12:57:23 -0400
committerhistoria <[not public]>2026-10-06 12:57:23 -0400
commit9666676766bf21364f4fc4d0d1d336e72f2a91b4 (patch)
tree0cade34091d7602790dc38cd44053f0b74bbadf2 /internal/behavior
parent4bc1daaf259e0c19c5fe4a12ec8bc7f6e5a735c1 (diff)
downloadthehouseoficarus-9666676766bf21364f4fc4d0d1d336e72f2a91b4.tar.gz
refactor: split casino.go, dedup helpers, remove audit docs
Diffstat (limited to 'internal/behavior')
-rw-r--r--internal/behavior/behavior.go24
-rw-r--r--internal/behavior/store.go20
-rw-r--r--internal/behavior/yml_test.go35
3 files changed, 68 insertions, 11 deletions
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")
+ }
+}