From 84d74ca4351a97148ad8339676f9df7946bc21fb Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Thu, 25 Jun 2026 20:33:47 -0400 Subject: feat: trigger system for scripted events added. --- internal/world/trigger_store.go | 340 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 340 insertions(+) create mode 100644 internal/world/trigger_store.go (limited to 'internal/world/trigger_store.go') diff --git a/internal/world/trigger_store.go b/internal/world/trigger_store.go new file mode 100644 index 0000000..f00d256 --- /dev/null +++ b/internal/world/trigger_store.go @@ -0,0 +1,340 @@ +package world + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "sync" + + "gopkg.in/yaml.v3" +) + +type TriggerStore struct { + mu sync.Mutex + + globalByPlayerFlag map[string][]*TriggerDef + globalByWorldFlag map[string][]*TriggerDef + roomByPlayerFlag map[int]map[string][]*TriggerDef + roomByWorldFlag map[int]map[string][]*TriggerDef + + playerSeqs map[string]*PlayerTriggerSeq + worldSeqs map[string]*WorldTriggerSeq +} + +type PlayerTriggerSeq struct { + PlayerName string + TriggerID string + RoomID int + Steps []TriggerStep + Wait int + Started bool + FlagValue any +} + +type WorldTriggerSeq struct { + TriggerID string + RoomID int + Steps []TriggerStep + Wait int + FlagValue any +} + +func NewTriggerStore() *TriggerStore { + return &TriggerStore{ + globalByPlayerFlag: make(map[string][]*TriggerDef), + globalByWorldFlag: make(map[string][]*TriggerDef), + roomByPlayerFlag: make(map[int]map[string][]*TriggerDef), + roomByWorldFlag: make(map[int]map[string][]*TriggerDef), + playerSeqs: make(map[string]*PlayerTriggerSeq), + worldSeqs: make(map[string]*WorldTriggerSeq), + } +} + +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.IsWorldFlagTrigger() { + if ts.roomByWorldFlag[roomID] == nil { + ts.roomByWorldFlag[roomID] = make(map[string][]*TriggerDef) + } + t.Room = roomID + ts.roomByWorldFlag[roomID][t.OnFlag] = append( + ts.roomByWorldFlag[roomID][t.OnFlag], 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.IsWorldFlagTrigger() { + ts.globalByWorldFlag[t.OnFlag] = append( + ts.globalByWorldFlag[t.OnFlag], 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) FireWorld(flagName string, flagValue any) []*WorldTriggerSeq { + ts.mu.Lock() + defer ts.mu.Unlock() + + var started []*WorldTriggerSeq + + for _, t := range ts.globalByWorldFlag[flagName] { + if !ts.valueMatches(t.Value, flagValue) { + continue + } + seq := ts.startWorldSeqLocked(t, flagValue) + if seq != nil { + started = append(started, seq) + } + } + + for roomID, roomFlags := range ts.roomByWorldFlag { + for _, t := range roomFlags[flagName] { + if !ts.valueMatches(t.Value, flagValue) { + continue + } + if t.Room == 0 { + t.Room = roomID + } + seq := ts.startWorldSeqLocked(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([]TriggerStep, 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) startWorldSeqLocked(t *TriggerDef, flagValue any) *WorldTriggerSeq { + key := t.ID + if _, exists := ts.worldSeqs[key]; exists { + return nil + } + seq := &WorldTriggerSeq{ + TriggerID: t.ID, + RoomID: t.Room, + Steps: make([]TriggerStep, len(t.Steps)), + FlagValue: flagValue, + } + copy(seq.Steps, t.Steps) + if len(seq.Steps) > 0 { + seq.Wait = seq.Steps[0].Delay + } + ts.worldSeqs[key] = seq + return seq +} + +func (ts *TriggerStore) valueMatches(want, got any) bool { + if want == nil { + return true + } + return 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) SnapshotWorldSeqs() []*WorldTriggerSeq { + ts.mu.Lock() + defer ts.mu.Unlock() + out := make([]*WorldTriggerSeq, 0, len(ts.worldSeqs)) + for _, s := range ts.worldSeqs { + 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) RemoveWorldSeq(triggerID string) { + ts.mu.Lock() + defer ts.mu.Unlock() + delete(ts.worldSeqs, 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.globalByWorldFlag { + out = append(out, triggers...) + } + return out +} + +func valuesEqual(a, b any) bool { + ai, aok := numericValue(a) + bi, bok := numericValue(b) + if aok && bok { + return ai == bi + } + return a == b +} + +func numericValue(v any) (float64, bool) { + switch x := v.(type) { + case int: + return float64(x), true + case int64: + return float64(x), true + case float64: + return x, true + } + return 0, false +} + +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.roomByWorldFlag { + for _, triggers := range byFlag { + out[roomID] = append(out[roomID], triggers...) + } + } + return out +} -- cgit v1.2.3