aboutsummaryrefslogtreecommitdiff
path: root/internal/world/trigger_store.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/world/trigger_store.go')
-rw-r--r--internal/world/trigger_store.go340
1 files changed, 340 insertions, 0 deletions
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
+}