package world import ( "fmt" "os" "path/filepath" "strings" "sync" "gopkg.in/yaml.v3" ) 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 []TriggerStep Wait int Started bool FlagValue any } type GlobalTriggerSeq struct { TriggerID string RoomID int Steps []TriggerStep 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([]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) 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([]TriggerStep, 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 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 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.roomByGlobalFlag { for _, triggers := range byFlag { out[roomID] = append(out[roomID], triggers...) } } return out }