diff options
Diffstat (limited to 'internal/world')
| -rw-r--r-- | internal/world/grid.go | 10 | ||||
| -rw-r--r-- | internal/world/insert_remove_test.go | 2 | ||||
| -rw-r--r-- | internal/world/mob.go | 22 | ||||
| -rw-r--r-- | internal/world/room.go | 64 | ||||
| -rw-r--r-- | internal/world/room_migrate.go | 162 | ||||
| -rw-r--r-- | internal/world/room_migrate_test.go | 104 | ||||
| -rw-r--r-- | internal/world/trigger.go | 16 | ||||
| -rw-r--r-- | internal/world/trigger_store.go | 330 |
8 files changed, 247 insertions, 463 deletions
diff --git a/internal/world/grid.go b/internal/world/grid.go index 7be8eed..ac80476 100644 --- a/internal/world/grid.go +++ b/internal/world/grid.go @@ -87,11 +87,11 @@ func BuildGrid(seed int, load func(int) (*Room, bool), include func(int) bool, e continue } - g.Coord[target] = want - g.RoomAt[want] = target - g.Dist[target] = g.Dist[rid] + 1 - queue = append(queue, target) - } + g.Coord[target] = want + g.RoomAt[want] = target + g.Dist[target] = g.Dist[rid] + 1 + queue = append(queue, target) + } } return g diff --git a/internal/world/insert_remove_test.go b/internal/world/insert_remove_test.go index 15d752e..201b0d4 100644 --- a/internal/world/insert_remove_test.go +++ b/internal/world/insert_remove_test.go @@ -95,8 +95,10 @@ func TestInsertGridConflictsOverlap(t *testing.T) { // TestInsertGridConflictsTwist: room 3 beyond 2 is also reachable from 1 via a // separate path, so after the push 3 would have two candidate positions. +// // 1 east→2 east→3 (3 at (2,0,0), pushed to (3,0,0)) // 1 south→4 east→5 north→3 (3 at (1,0,0)) +// // 3 is reachable two ways already → the pre-edit world is itself a twist. The // insert helper must surface a twist conflict. func TestInsertGridConflictsTwist(t *testing.T) { diff --git a/internal/world/mob.go b/internal/world/mob.go index 45b0fb8..fa0cdbf 100644 --- a/internal/world/mob.go +++ b/internal/world/mob.go @@ -78,13 +78,13 @@ type MobCombat struct { } type MobDef struct { - ID string `yaml:"id"` - Name string `yaml:"name"` - Description string `yaml:"description"` - IdleDescriptions []string `yaml:"idle_descriptions"` - Protected bool `yaml:"protected"` - Unique bool `yaml:"unique"` - Drops MobDropTable `yaml:"drops"` + ID string `yaml:"id"` + Name string `yaml:"name"` + Description string `yaml:"description"` + IdleDescriptions []string `yaml:"idle_descriptions"` + Protected bool `yaml:"protected"` + Unique bool `yaml:"unique"` + Drops MobDropTable `yaml:"drops"` Steal *MobSteal `yaml:"steal,omitempty"` Task *MobTask `yaml:"task,omitempty"` @@ -94,7 +94,7 @@ type MobDef struct { Talk *behavior.TalkConfig `yaml:"talk,omitempty"` Shop *behavior.ShopConfig `yaml:"shop,omitempty"` - OnKill []behavior.Interaction `yaml:"on_kill,omitempty"` + OnKill []behavior.Trigger `yaml:"on_kill,omitempty"` } func (d *MobDef) IsTalkable() bool { return d.Talk != nil } @@ -132,7 +132,7 @@ type MobInstance struct { AttackBonus int StrengthBonus int - AttackTypes []string + AttackTypes []string RangedBonus int ScienceBonus int @@ -172,7 +172,7 @@ type MobInstance struct { // HP draining to zero (the "completion" of the work). The first entry // whose item filter, Condition, and the first-match-wins rule all pass // fires. - OnKill []behavior.Interaction + OnKill []behavior.Trigger // Shop is the (shared, read-only) shop config from the def. Per-instance // live stock is tracked in ShopStock; ShopRestock holds per-item countdown @@ -313,7 +313,7 @@ func NewMobInstance(def *MobDef, instanceID string, roomID int, wanderRooms []in Drops: def.Drops, AttackBonus: attackBonus, StrengthBonus: strengthBonus, - AttackTypes: attackType, + AttackTypes: attackType, RangedBonus: rangedBonus, ScienceBonus: scienceBonus, SciencePercentBonus: sciencePercentBonus, diff --git a/internal/world/room.go b/internal/world/room.go index 8ae2d13..bc115f8 100644 --- a/internal/world/room.go +++ b/internal/world/room.go @@ -75,27 +75,29 @@ type SpawnDef struct { } type ExitDef struct { - Room int `yaml:"room"` - Condition *behavior.Condition `yaml:"condition,omitempty"` - BlockedMessage string `yaml:"blocked_message,omitempty"` - OnTraverse []behavior.Interaction `yaml:"on_traverse,omitempty"` - Hidden bool `yaml:"hidden,omitempty"` - AlwaysBlocked bool `yaml:"always_blocked,omitempty"` + Room int `yaml:"room"` + Condition *behavior.Condition `yaml:"condition,omitempty"` + BlockedMessage string `yaml:"blocked_message,omitempty"` + OnTraverse []behavior.Trigger `yaml:"on_traverse,omitempty"` + Hidden bool `yaml:"hidden,omitempty"` + AlwaysBlocked bool `yaml:"always_blocked,omitempty"` } type Room struct { - ID int `yaml:"id"` - Name string `yaml:"name"` - Color string `yaml:"color"` - Description behavior.DescList `yaml:"description"` - Exits map[ExitDir]ExitDef `yaml:"exits"` - Objects []RoomObject `yaml:"objects"` - ItemSpawns []SpawnDef `yaml:"item_spawns"` - Mobs []RoomMob `yaml:"mobs"` - OnEnter []behavior.StepAction `yaml:"on_enter"` - Hazard string `yaml:"hazard"` - BlockTransport bool `yaml:"block_transport"` - Triggers []TriggerDef `yaml:"triggers"` + ID int `yaml:"id"` + Name string `yaml:"name"` + Color string `yaml:"color"` + Description behavior.DescList `yaml:"description"` + Exits map[ExitDir]ExitDef `yaml:"exits"` + Objects []RoomObject `yaml:"objects"` + ItemSpawns []SpawnDef `yaml:"item_spawns"` + Mobs []RoomMob `yaml:"mobs"` + OnEnter []behavior.Trigger `yaml:"on_enter,omitempty"` + OnExit []behavior.Trigger `yaml:"on_exit,omitempty"` + OnFlagChange []behavior.Trigger `yaml:"on_flag_change,omitempty"` + OnGlobalFlagChange []behavior.Trigger `yaml:"on_global_flag_change,omitempty"` + Hazard string `yaml:"hazard"` + BlockTransport bool `yaml:"block_transport"` } type RoomMob struct { @@ -151,19 +153,19 @@ func (ro *RoomObject) UnmarshalYAML(value *yaml.Node) error { func (ro RoomObject) MarshalYAML() (interface{}, error) { if ro.Local != nil { type inlineObj struct { - Name string `yaml:"name"` - Aliases []string `yaml:"aliases,omitempty"` - Color string `yaml:"color,omitempty"` - Hidden bool `yaml:"hidden,omitempty"` - InRoomDescription string `yaml:"inroom_description,omitempty"` - RemovalItem string `yaml:"removal_item,omitempty"` - Description behavior.DescList `yaml:"description,omitempty"` - OnUse []behavior.Interaction `yaml:"on_use,omitempty"` - Steal *object.ObjectSteal `yaml:"steal,omitempty"` - Gather *behavior.GatherConfig `yaml:"gather,omitempty"` - Talk *behavior.TalkConfig `yaml:"talk,omitempty"` - Safespot *object.SafespotConfig `yaml:"safespot,omitempty"` - OnLook []behavior.Interaction `yaml:"on_look,omitempty"` + Name string `yaml:"name"` + Aliases []string `yaml:"aliases,omitempty"` + Color string `yaml:"color,omitempty"` + Hidden bool `yaml:"hidden,omitempty"` + InRoomDescription string `yaml:"inroom_description,omitempty"` + RemovalItem string `yaml:"removal_item,omitempty"` + Description behavior.DescList `yaml:"description,omitempty"` + OnUse []behavior.Trigger `yaml:"on_use,omitempty"` + Steal *object.ObjectSteal `yaml:"steal,omitempty"` + Gather *behavior.GatherConfig `yaml:"gather,omitempty"` + Talk *behavior.TalkConfig `yaml:"talk,omitempty"` + Safespot *object.SafespotConfig `yaml:"safespot,omitempty"` + OnLook []behavior.Trigger `yaml:"on_look,omitempty"` } def := ro.Local return inlineObj{ diff --git a/internal/world/room_migrate.go b/internal/world/room_migrate.go index 946417f..d609c6a 100644 --- a/internal/world/room_migrate.go +++ b/internal/world/room_migrate.go @@ -1,66 +1,174 @@ package world +import ( + "thehouseoficarus/internal/behavior" +) + // RewriteRoomIDs remaps every genuine room-ID reference in this room per idMap -// (oldID -> newID): exit targets, trigger Room/Teleport/DespawnRooms, on-enter -// Teleport/DespawnRooms, and mob WanderRooms. It does NOT touch author-chosen -// set_flags/set_player_flags values or condition `value:` fields — those are not -// structural room references (and historically the swapid regex rewrote them -// incorrectly as a side effect of matching any bare integer). +// (oldID -> newID): exit targets; every Step.Teleport and Step.SpawnMob +// .DespawnRooms inside on_enter/on_exit/on_flag_change/on_global_flag_change/ +// on_traverse/on_use/on_look/on_kill blocks; every Condition.Room anywhere in +// the room (exits, descriptions, trigger conditions, per-step conditions, +// including nested all_of/any_of); and mob WanderRooms. It does NOT touch +// author-chosen set_flags/set_player_flags values or condition `value:` fields +// — those are not structural room references. // // idMap is assumed to be a bijection (each old ID maps to a distinct new ID); -// this holds for both swap (A<->B) and rename-to-unused (A->B). Returns whether -// any field was changed. +// this holds for both swap (A<->B) and rename-to-unused (A->B). Returns +// whether any field was changed. func (r *Room) RewriteRoomIDs(idMap map[int]int) bool { if r == nil || len(idMap) == 0 { return false } changed := false + + // Exit targets + their on_traverse steps + exit/desc conditions. for dir, exit := range r.Exits { var exitChanged bool if remapInt(idMap, &exit.Room) { exitChanged = true } - for i := range exit.OnTraverse { - if exit.OnTraverse[i].Action != nil { - if remapInt(idMap, &exit.OnTraverse[i].Action.Teleport) { - exitChanged = true - } - if exit.OnTraverse[i].Action.SpawnMob != nil && - remapIntSlice(idMap, exit.OnTraverse[i].Action.SpawnMob.DespawnRooms) { - exitChanged = true - } - } + if remapTriggers(idMap, exit.OnTraverse) { + exitChanged = true + } + if exit.Condition != nil && remapCondition(idMap, exit.Condition) { + exitChanged = true } if exitChanged { r.Exits[dir] = exit changed = true } } - for i := range r.Triggers { - if remapInt(idMap, &r.Triggers[i].Room) { + + // Every trigger-shaped block on the room shares the Step list + Condition + // structure. Remap their steps and conditions uniformly. + for i := range r.OnEnter { + c := false + if remapStepList(idMap, r.OnEnter[i].Steps) { + c = true + } + if remapCondition(idMap, r.OnEnter[i].Condition) { + c = true + } + changed = changed || c + } + for i := range r.OnExit { + c := false + if remapStepList(idMap, r.OnExit[i].Steps) { + c = true + } + if remapCondition(idMap, r.OnExit[i].Condition) { + c = true + } + changed = changed || c + } + for i := range r.OnFlagChange { + c := false + if remapStepList(idMap, r.OnFlagChange[i].Steps) { + c = true + } + if remapCondition(idMap, r.OnFlagChange[i].Condition) { + c = true + } + changed = changed || c + } + for i := range r.OnGlobalFlagChange { + c := false + if remapStepList(idMap, r.OnGlobalFlagChange[i].Steps) { + c = true + } + if remapCondition(idMap, r.OnGlobalFlagChange[i].Condition) { + c = true + } + changed = changed || c + } + + // Conditional room descriptions carry a Condition. + for i := range r.Description { + if remapCondition(idMap, r.Description[i].Condition) { changed = true } - for j := range r.Triggers[i].Steps { - s := &r.Triggers[i].Steps[j] - if remapInt(idMap, &s.Teleport) { + } + + // Per-room local objects: on_use/on_look steps + object desc conditions. + for i := range r.Objects { + if r.Objects[i].Local != nil { + loc := r.Objects[i].Local + if remapTriggers(idMap, loc.OnUse) { changed = true } - if s.SpawnMob != nil && remapIntSlice(idMap, s.SpawnMob.DespawnRooms) { + if remapTriggers(idMap, loc.OnLook) { changed = true } + for j := range loc.Description { + if remapCondition(idMap, loc.Description[j].Condition) { + changed = true + } + } } } - for i := range r.OnEnter { - s := &r.OnEnter[i] + + for i := range r.Mobs { + if remapIntSlice(idMap, r.Mobs[i].WanderRooms) { + changed = true + } + } + return changed +} + +// remapTriggers remaps step fields across a []behavior.Trigger (each trigger's +// Steps + per-step Condition + trigger-level Condition). +func remapTriggers(idMap map[int]int, list []behavior.Trigger) bool { + changed := false + for i := range list { + c := false + if remapStepList(idMap, list[i].Steps) { + c = true + } + if remapCondition(idMap, list[i].Condition) { + c = true + } + changed = changed || c + } + return changed +} + +// remapStepList remaps Step.Teleport and Step.SpawnMob.DespawnRooms plus every +// step's per-step Condition.Room across a single Trigger's Steps. +func remapStepList(idMap map[int]int, steps []behavior.Step) bool { + changed := false + for i := range steps { + s := &steps[i] if remapInt(idMap, &s.Teleport) { changed = true } if s.SpawnMob != nil && remapIntSlice(idMap, s.SpawnMob.DespawnRooms) { changed = true } + if remapCondition(idMap, s.Condition) { + changed = true + } } - for i := range r.Mobs { - if remapIntSlice(idMap, r.Mobs[i].WanderRooms) { + return changed +} + +// remapCondition walks a Condition tree and remaps any Room field (a +// structural room reference) it finds, recursing into AllOf/AnyOf. +func remapCondition(idMap map[int]int, c *behavior.Condition) bool { + if c == nil { + return false + } + changed := false + if c.Room != 0 && remapInt(idMap, &c.Room) { + changed = true + } + for i := range c.AllOf { + if remapCondition(idMap, &c.AllOf[i]) { + changed = true + } + } + for i := range c.AnyOf { + if remapCondition(idMap, &c.AnyOf[i]) { changed = true } } diff --git a/internal/world/room_migrate_test.go b/internal/world/room_migrate_test.go index c54adf2..96f74ae 100644 --- a/internal/world/room_migrate_test.go +++ b/internal/world/room_migrate_test.go @@ -2,29 +2,42 @@ package world import ( "testing" - "thehouseoficarus/internal/behavior" ) func TestRoomRewriteRoomIDs(t *testing.T) { r := &Room{ Exits: map[ExitDir]ExitDef{ - North: {Room: 10, OnTraverse: []behavior.Interaction{{Action: &behavior.StepAction{ - NodeAction: behavior.NodeAction{SetGlobalFlags: map[string]any{"last": 10}, Teleport: 110}, - }}}}, - South: {Room: 99}, - }, - Triggers: []TriggerDef{ - { - Room: 30, - Steps: []behavior.StepAction{ - {NodeAction: behavior.NodeAction{Teleport: 40}, SpawnMob: &behavior.SpawnMobConfig{DespawnRooms: []int{50, 60}}}, - }, + North: { + Room: 10, + OnTraverse: []behavior.Trigger{{ + Condition: &behavior.Condition{Room: 60}, // projectile, ensures trigger-condition Room remaps too + Steps: []behavior.Step{{ + SetGlobalFlags: map[string]any{"last": 10}, + Teleport: 110, + }}, + }}, + Condition: &behavior.Condition{Room: 40}, }, + South: {Room: 99}, }, - OnEnter: []behavior.StepAction{ - {NodeAction: behavior.NodeAction{Teleport: 70}, SpawnMob: &behavior.SpawnMobConfig{DespawnRooms: []int{80}}}, - }, + OnEnter: []behavior.Trigger{{ + Condition: &behavior.Condition{Room: 20}, + Steps: []behavior.Step{{ + Teleport: 70, + SpawnMob: &behavior.SpawnMobConfig{DespawnRooms: []int{80}}, + }}, + }}, + OnExit: []behavior.Trigger{{ + Steps: []behavior.Step{{Teleport: 50}}, + }}, + OnFlagChange: []behavior.Trigger{{ + Condition: &behavior.Condition{Room: 30}, + Steps: []behavior.Step{{ + Teleport: 40, + SpawnMob: &behavior.SpawnMobConfig{DespawnRooms: []int{50, 60}}, + }}, + }}, Mobs: []RoomMob{ {ID: "guard", WanderRooms: []int{90, 100}}, }, @@ -38,40 +51,48 @@ func TestRoomRewriteRoomIDs(t *testing.T) { t.Fatal("expected changed=true") } - checkExit := func(dir ExitDir, want int) { - t.Helper() - if got := r.Exits[dir].Room; got != want { - t.Errorf("exit %s room = %d, want %d", dir, got, want) - } - } - checkExit(North, 11) - checkExit(South, 99) // unchanged (not in idMap) - - // Author set_flags values are NOT structural room references and must not - // be remapped (the old swapid regex wrongly rewrote these). - if v := r.Exits[North].OnTraverse[0].Action.SetGlobalFlags["last"]; v != 10 { + if got := r.Exits[North].Room; got != 11 { + t.Errorf("exit north room = %d, want 11", got) + } + if got := r.Exits[South].Room; got != 99 { + t.Errorf("exit south room = %d, want 99 (unchanged)", got) + } + // Author set_flags values are NOT structural room references and must not remap. + if v := r.Exits[North].OnTraverse[0].Steps[0].SetGlobalFlags["last"]; v != 10 { t.Errorf("set_global_flags value remapped: got %v, want 10", v) } - // Exit on_traverse teleport IS a structural room reference and must remap. - if r.Exits[North].OnTraverse[0].Action.Teleport != 111 { - t.Errorf("exit on_traverse teleport = %d, want 111", r.Exits[North].OnTraverse[0].Action.Teleport) + if got := r.Exits[North].OnTraverse[0].Steps[0].Teleport; got != 111 { + t.Errorf("on_traverse teleport = %d, want 111", got) } - - if r.Triggers[0].Room != 31 { - t.Errorf("trigger room = %d, want 31", r.Triggers[0].Room) + // Exit-level condition Room remaps. + if got := r.Exits[North].Condition.Room; got != 41 { + t.Errorf("exit condition room = %d, want 41", got) + } + // Trigger-level + per-step Condition.Room remaps. + if got := r.Exits[North].OnTraverse[0].Condition.Room; got != 61 { + t.Errorf("on_traverse trigger condition room = %d, want 61", got) } - if r.Triggers[0].Steps[0].Teleport != 41 { - t.Errorf("trigger teleport = %d, want 41", r.Triggers[0].Steps[0].Teleport) + if got := r.OnEnter[0].Condition.Room; got != 21 { + t.Errorf("on_enter trigger condition room = %d, want 21", got) } - if got := r.Triggers[0].Steps[0].SpawnMob.DespawnRooms; len(got) != 2 || got[0] != 51 || got[1] != 61 { - t.Errorf("trigger despawn_rooms = %v, want [51 61]", got) + if got := r.OnFlagChange[0].Condition.Room; got != 31 { + t.Errorf("on_flag_change condition room = %d, want 31", got) } - if r.OnEnter[0].Teleport != 71 { - t.Errorf("on_enter teleport = %d, want 71", r.OnEnter[0].Teleport) + if got := r.OnEnter[0].Steps[0].Teleport; got != 71 { + t.Errorf("on_enter teleport = %d, want 71", got) } - if got := r.OnEnter[0].SpawnMob.DespawnRooms; len(got) != 1 || got[0] != 81 { + if got := r.OnEnter[0].Steps[0].SpawnMob.DespawnRooms; len(got) != 1 || got[0] != 81 { t.Errorf("on_enter despawn_rooms = %v, want [81]", got) } + if got := r.OnExit[0].Steps[0].Teleport; got != 51 { + t.Errorf("on_exit teleport = %d, want 51", got) + } + if got := r.OnFlagChange[0].Steps[0].Teleport; got != 41 { + t.Errorf("on_flag_change teleport = %d, want 41", got) + } + if got := r.OnFlagChange[0].Steps[0].SpawnMob.DespawnRooms; len(got) != 2 || got[0] != 51 || got[1] != 61 { + t.Errorf("on_flag_change despawn_rooms = %v, want [51 61]", got) + } if got := r.Mobs[0].WanderRooms; len(got) != 2 || got[0] != 91 || got[1] != 101 { t.Errorf("mob wander_rooms = %v, want [91 101]", got) } @@ -81,7 +102,6 @@ func TestRoomRewriteRoomIDsNoChange(t *testing.T) { r := &Room{ Exits: map[ExitDir]ExitDef{North: {Room: 5}}, } - // idMap does not mention 5 -> nothing changes. if r.RewriteRoomIDs(map[int]int{99: 100}) { t.Error("expected changed=false when no references match") } @@ -100,7 +120,6 @@ func TestRoomRewriteRoomIDsSwapRoundTrips(t *testing.T) { if r.Exits[North].Room != 20 || r.Exits[South].Room != 10 { t.Errorf("after swap: north=%d south=%d, want north=20 south=10", r.Exits[North].Room, r.Exits[South].Room) } - // Applying the same swap again reverts (a swap is its own inverse). r.RewriteRoomIDs(swap) if r.Exits[North].Room != 10 || r.Exits[South].Room != 20 { t.Errorf("after double swap: north=%d south=%d, want north=10 south=20", r.Exits[North].Room, r.Exits[South].Room) @@ -114,7 +133,6 @@ func TestRoomRewriteRoomIDsIdempotentRename(t *testing.T) { if r.Exits[North].Room != 11 { t.Fatalf("after rename: room=%d, want 11", r.Exits[North].Room) } - // Second pass: 11 is not a key in idMap, so nothing changes. if r.RewriteRoomIDs(rename) { t.Error("expected changed=false on second pass of a rename idMap") } diff --git a/internal/world/trigger.go b/internal/world/trigger.go deleted file mode 100644 index de07c18..0000000 --- a/internal/world/trigger.go +++ /dev/null @@ -1,16 +0,0 @@ -package world - -import "thehouseoficarus/internal/behavior" - -type TriggerDef struct { - ID string `yaml:"id"` - OnPlayerFlag string `yaml:"on_player_flag"` - OnGlobalFlag string `yaml:"on_global_flag"` - Value any `yaml:"value"` - Room int `yaml:"room"` - Steps []behavior.StepAction `yaml:"steps"` -} - -func (t *TriggerDef) IsPlayerFlagTrigger() bool { return t.OnPlayerFlag != "" } - -func (t *TriggerDef) IsGlobalFlagTrigger() bool { return t.OnGlobalFlag != "" }
\ No newline at end of file diff --git a/internal/world/trigger_store.go b/internal/world/trigger_store.go deleted file mode 100644 index e50ae81..0000000 --- a/internal/world/trigger_store.go +++ /dev/null @@ -1,330 +0,0 @@ -package world - -import ( - "fmt" - "os" - "path/filepath" - "strings" - "sync" - - "gopkg.in/yaml.v3" - "thehouseoficarus/internal/behavior" - "thehouseoficarus/internal/engine" -) - -type TriggerStore struct { - mu sync.Mutex - - globalByPlayerFlag map[string][]*TriggerDef - globalByGlobalFlag map[string][]*TriggerDef - roomByPlayerFlag map[int]map[string][]*TriggerDef - roomByGlobalFlag map[int]map[string][]*TriggerDef - - playerSeqs map[string]*PlayerTriggerSeq - globalSeqs map[string]*GlobalTriggerSeq -} - -type PlayerTriggerSeq struct { - PlayerName string - TriggerID string - RoomID int - Steps []behavior.StepAction - Wait int - Started bool - FlagValue any -} - -type GlobalTriggerSeq struct { - TriggerID string - RoomID int - Steps []behavior.StepAction - Wait int - FlagValue any -} - -func NewTriggerStore() *TriggerStore { - return &TriggerStore{ - globalByPlayerFlag: make(map[string][]*TriggerDef), - globalByGlobalFlag: make(map[string][]*TriggerDef), - roomByPlayerFlag: make(map[int]map[string][]*TriggerDef), - roomByGlobalFlag: make(map[int]map[string][]*TriggerDef), - playerSeqs: make(map[string]*PlayerTriggerSeq), - globalSeqs: make(map[string]*GlobalTriggerSeq), - } -} - -func (ts *TriggerStore) ClearTriggers() { - ts.mu.Lock() - defer ts.mu.Unlock() - ts.globalByPlayerFlag = make(map[string][]*TriggerDef) - ts.globalByGlobalFlag = make(map[string][]*TriggerDef) - ts.roomByPlayerFlag = make(map[int]map[string][]*TriggerDef) - ts.roomByGlobalFlag = make(map[int]map[string][]*TriggerDef) -} - -func (ts *TriggerStore) LoadGlobal(dataDir string) error { - dir := filepath.Join(dataDir, "triggers") - if _, err := os.Stat(dir); os.IsNotExist(err) { - return nil - } - - entries, err := os.ReadDir(dir) - if err != nil { - return err - } - - for _, entry := range entries { - if entry.IsDir() { - continue - } - path := filepath.Join(dir, entry.Name()) - ext := strings.ToLower(filepath.Ext(path)) - if ext != ".yaml" && ext != ".yml" { - continue - } - id := strings.TrimSuffix(entry.Name(), filepath.Ext(entry.Name())) - data, err := os.ReadFile(path) - if err != nil { - continue - } - var trigger TriggerDef - if err := yaml.Unmarshal(data, &trigger); err != nil { - return fmt.Errorf("trigger %q: %w", path, err) - } - trigger.ID = id - ts.addGlobal(&trigger) - } - return nil -} - -func (ts *TriggerStore) SeedRoomTriggers(roomID int, triggers []TriggerDef) { - if len(triggers) == 0 { - return - } - ts.mu.Lock() - defer ts.mu.Unlock() - for i := range triggers { - t := &triggers[i] - if t.IsPlayerFlagTrigger() { - if ts.roomByPlayerFlag[roomID] == nil { - ts.roomByPlayerFlag[roomID] = make(map[string][]*TriggerDef) - } - ts.roomByPlayerFlag[roomID][t.OnPlayerFlag] = append( - ts.roomByPlayerFlag[roomID][t.OnPlayerFlag], t) - } - if t.IsGlobalFlagTrigger() { - if ts.roomByGlobalFlag[roomID] == nil { - ts.roomByGlobalFlag[roomID] = make(map[string][]*TriggerDef) - } - t.Room = roomID - ts.roomByGlobalFlag[roomID][t.OnGlobalFlag] = append( - ts.roomByGlobalFlag[roomID][t.OnGlobalFlag], t) - } - } -} - -func (ts *TriggerStore) addGlobal(t *TriggerDef) { - ts.mu.Lock() - defer ts.mu.Unlock() - if t.IsPlayerFlagTrigger() { - ts.globalByPlayerFlag[t.OnPlayerFlag] = append( - ts.globalByPlayerFlag[t.OnPlayerFlag], t) - } - if t.IsGlobalFlagTrigger() { - ts.globalByGlobalFlag[t.OnGlobalFlag] = append( - ts.globalByGlobalFlag[t.OnGlobalFlag], t) - } -} - -func (ts *TriggerStore) FirePlayer(playerName string, flagName string, flagValue any) []*PlayerTriggerSeq { - ts.mu.Lock() - defer ts.mu.Unlock() - - var started []*PlayerTriggerSeq - - for _, t := range ts.globalByPlayerFlag[flagName] { - if !ts.valueMatches(t.Value, flagValue) { - continue - } - seq := ts.startPlayerSeqLocked(playerName, t, 0, flagValue) - if seq != nil { - started = append(started, seq) - } - } - - return started -} - -func (ts *TriggerStore) FirePlayerInRoom(playerName string, roomID int, flagName string, flagValue any) []*PlayerTriggerSeq { - ts.mu.Lock() - defer ts.mu.Unlock() - - var started []*PlayerTriggerSeq - - for _, t := range ts.globalByPlayerFlag[flagName] { - if !ts.valueMatches(t.Value, flagValue) { - continue - } - seq := ts.startPlayerSeqLocked(playerName, t, roomID, flagValue) - if seq != nil { - started = append(started, seq) - } - } - - if roomFlags, ok := ts.roomByPlayerFlag[roomID]; ok { - for _, t := range roomFlags[flagName] { - if !ts.valueMatches(t.Value, flagValue) { - continue - } - seq := ts.startPlayerSeqLocked(playerName, t, roomID, flagValue) - if seq != nil { - started = append(started, seq) - } - } - } - - return started -} - -func (ts *TriggerStore) FireGlobal(flagName string, flagValue any) []*GlobalTriggerSeq { - ts.mu.Lock() - defer ts.mu.Unlock() - - var started []*GlobalTriggerSeq - - for _, t := range ts.globalByGlobalFlag[flagName] { - if !ts.valueMatches(t.Value, flagValue) { - continue - } - seq := ts.startGlobalSeqLocked(t, flagValue) - if seq != nil { - started = append(started, seq) - } - } - - for roomID, roomFlags := range ts.roomByGlobalFlag { - for _, t := range roomFlags[flagName] { - if !ts.valueMatches(t.Value, flagValue) { - continue - } - if t.Room == 0 { - t.Room = roomID - } - seq := ts.startGlobalSeqLocked(t, flagValue) - if seq != nil { - started = append(started, seq) - } - } - } - - return started -} - -func (ts *TriggerStore) startPlayerSeqLocked(playerName string, t *TriggerDef, roomID int, flagValue any) *PlayerTriggerSeq { - key := playerName + ":" + t.ID - if _, exists := ts.playerSeqs[key]; exists { - return nil - } - seq := &PlayerTriggerSeq{ - PlayerName: playerName, - TriggerID: t.ID, - RoomID: roomID, - Steps: make([]behavior.StepAction, len(t.Steps)), - FlagValue: flagValue, - } - copy(seq.Steps, t.Steps) - if len(seq.Steps) > 0 { - seq.Wait = seq.Steps[0].Delay - } - ts.playerSeqs[key] = seq - return seq -} - -func (ts *TriggerStore) startGlobalSeqLocked(t *TriggerDef, flagValue any) *GlobalTriggerSeq { - key := t.ID - if _, exists := ts.globalSeqs[key]; exists { - return nil - } - seq := &GlobalTriggerSeq{ - TriggerID: t.ID, - RoomID: t.Room, - Steps: make([]behavior.StepAction, len(t.Steps)), - FlagValue: flagValue, - } - copy(seq.Steps, t.Steps) - if len(seq.Steps) > 0 { - seq.Wait = seq.Steps[0].Delay - } - ts.globalSeqs[key] = seq - return seq -} - -func (ts *TriggerStore) valueMatches(want, got any) bool { - if want == nil { - return true - } - return engine.ValuesEqual(want, got) -} - -func (ts *TriggerStore) SnapshotPlayerSeqs() []*PlayerTriggerSeq { - ts.mu.Lock() - defer ts.mu.Unlock() - out := make([]*PlayerTriggerSeq, 0, len(ts.playerSeqs)) - for _, s := range ts.playerSeqs { - out = append(out, s) - } - return out -} - -func (ts *TriggerStore) SnapshotGlobalSeqs() []*GlobalTriggerSeq { - ts.mu.Lock() - defer ts.mu.Unlock() - out := make([]*GlobalTriggerSeq, 0, len(ts.globalSeqs)) - for _, s := range ts.globalSeqs { - out = append(out, s) - } - return out -} - -func (ts *TriggerStore) RemovePlayerSeq(playerName, triggerID string) { - ts.mu.Lock() - defer ts.mu.Unlock() - key := playerName + ":" + triggerID - delete(ts.playerSeqs, key) -} - -func (ts *TriggerStore) RemoveGlobalSeq(triggerID string) { - ts.mu.Lock() - defer ts.mu.Unlock() - delete(ts.globalSeqs, triggerID) -} - -func (ts *TriggerStore) AllGlobalTriggers() []*TriggerDef { - ts.mu.Lock() - defer ts.mu.Unlock() - var out []*TriggerDef - for _, triggers := range ts.globalByPlayerFlag { - out = append(out, triggers...) - } - for _, triggers := range ts.globalByGlobalFlag { - out = append(out, triggers...) - } - return out -} - -func (ts *TriggerStore) AllRoomTriggers() map[int][]*TriggerDef { - ts.mu.Lock() - defer ts.mu.Unlock() - out := make(map[int][]*TriggerDef) - for roomID, byFlag := range ts.roomByPlayerFlag { - for _, triggers := range byFlag { - out[roomID] = append(out[roomID], triggers...) - } - } - for roomID, byFlag := range ts.roomByGlobalFlag { - for _, triggers := range byFlag { - out[roomID] = append(out[roomID], triggers...) - } - } - return out -} |
