aboutsummaryrefslogtreecommitdiff
path: root/cmd/migrate-messages
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-07-09 19:29:17 -0400
committerhistoria <[not public]>2026-07-09 19:29:17 -0400
commitb3d4c616f59ad2519f3a0b77e3b47d6571cd2486 (patch)
tree6f1f6a314e0550da68264a79381fc269db803312 /cmd/migrate-messages
parent18c7af94b30c2767b6633d050324c1db34074ce2 (diff)
downloadthehouseoficarus-b3d4c616f59ad2519f3a0b77e3b47d6571cd2486.tar.gz
feat: message actions are now sequences of messages with settable delays
Diffstat (limited to 'cmd/migrate-messages')
-rw-r--r--cmd/migrate-messages/main.go245
1 files changed, 245 insertions, 0 deletions
diff --git a/cmd/migrate-messages/main.go b/cmd/migrate-messages/main.go
new file mode 100644
index 0000000..81400a4
--- /dev/null
+++ b/cmd/migrate-messages/main.go
@@ -0,0 +1,245 @@
+package main
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "sort"
+ "strings"
+
+ "gopkg.in/yaml.v3"
+)
+
+func main() {
+ dataDir := os.Getenv("DATA_DIR")
+ if dataDir == "" {
+ dataDir = "data"
+ }
+
+ var changed []string
+ err := filepath.WalkDir(dataDir, func(path string, d os.DirEntry, err error) error {
+ if err != nil {
+ return err
+ }
+ if d.IsDir() || (!strings.HasSuffix(path, ".yaml") && !strings.HasSuffix(path, ".yml")) {
+ return nil
+ }
+ if migrated(path) {
+ changed = append(changed, path)
+ }
+ return nil
+ })
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "walk error: %v\n", err)
+ os.Exit(1)
+ }
+
+ sort.Strings(changed)
+ if len(changed) == 0 {
+ fmt.Println("No files needed migration.")
+ } else {
+ fmt.Printf("Migrated %d file(s):\n", len(changed))
+ for _, f := range changed {
+ fmt.Printf(" %s\n", f)
+ }
+ }
+}
+
+func migrated(path string) bool {
+ data, err := os.ReadFile(path)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "read %s: %v\n", path, err)
+ return false
+ }
+
+ var root any
+ if err := yaml.Unmarshal(data, &root); err != nil {
+ fmt.Fprintf(os.Stderr, "parse %s: %v\n", path, err)
+ return false
+ }
+
+ modified := false
+ root = walk(root, "", &modified)
+
+ if !modified {
+ return false
+ }
+
+ out, err := yaml.Marshal(root)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "marshal %s: %v\n", path, err)
+ return false
+ }
+ if err := os.WriteFile(path, out, 0644); err != nil {
+ fmt.Fprintf(os.Stderr, "write %s: %v\n", path, err)
+ return false
+ }
+ return true
+}
+
+// walk recursively walks a YAML tree with a parent-key context.
+// The context tells us what array type we're inside (on_enter, on_use, sequences, etc.)
+func walk(v any, parentKey string, modified *bool) any {
+ switch val := v.(type) {
+ case map[string]any:
+ return migrateMap(val, parentKey, modified)
+ case []any:
+ return migrateSlice(val, parentKey, modified)
+ }
+ return v
+}
+
+func migrateSlice(arr []any, parentKey string, modified *bool) any {
+ for i, el := range arr {
+ arr[i] = walk(el, parentKey, modified)
+ }
+ return arr
+}
+
+// stepActionKeys are fields unique to StepAction that are NOT found on
+// ModTriggerStep or Interaction. A map with any of these is definitely a
+// StepAction, regardless of context.
+var stepActionKeys = map[string]bool{
+ "broadcast": true,
+ "broadcast_global": true,
+ "spawn_mob": true,
+ "despawn_mob": true,
+ "give_item": true,
+ "take_item": true,
+ "teleport": true,
+ "heal": true,
+ "credits": true,
+ "aps_node": true,
+ "set_global_flags": true,
+ "set_player_flags": true,
+}
+
+func hasStepActionKey(m map[string]any) bool {
+ for k := range m {
+ if stepActionKeys[k] {
+ return true
+ }
+ }
+ return false
+}
+
+// contextIsStepAction returns true when the parent key indicates this array
+// contains StepAction entries (as opposed to ModTriggerStep or Interaction).
+func contextIsStepAction(parentKey string) bool {
+ switch parentKey {
+ case "on_enter":
+ return true
+ case "action":
+ return true
+ case "on_traverse":
+ // on_traverse entries have action: but entries themselves are Interactions.
+ // The action: inside them is a StepAction, handled by the "action" case above.
+ // The top-level entry message is handled by interaction logic.
+ return false
+ }
+ return false
+}
+
+// isSequenceArray returns true for keys whose array entries are ModTriggerStep
+// (which keeps the legacy message field — do not migrate).
+func isSequenceArray(key string) bool {
+ return key == "sequence"
+}
+
+// isInteractionArray returns true for keys that contain Interaction entries.
+func isInteractionArray(key string) bool {
+ switch key {
+ case "on_use", "on_look", "on_kill", "on_traverse":
+ return true
+ }
+ return false
+}
+
+func migrateMap(m map[string]any, parentKey string, modified *bool) any {
+ _, hasMsg := m["message"].(string)
+
+ // --- Step 1: migrate legacy message: on StepAction maps ---
+ // A map is a StepAction if:
+ // a) It has a unique StepAction key (set_player_flags, broadcast, etc.), OR
+ // b) The parent array is on_enter or action (our context tells us)
+ // We exclude sequence arrays (ModTriggerStep keeps its message field).
+ if hasMsg && (hasStepActionKey(m) || contextIsStepAction(parentKey)) && !isSequenceArray(parentKey) {
+ m["messages"] = []any{map[string]any{"message": m["message"], "delay": 0}}
+ delete(m, "message")
+ *modified = true
+ }
+
+ // --- Step 2: recurse into children ---
+ // For map values, recurse with the key as context. For arrays under a key,
+ // the child will use that key.
+ for k, v := range m {
+ m[k] = walk(v, k, modified)
+ }
+
+ // --- Step 3: interaction-level migration ---
+ // If this is an interaction entry (identified by being an element of an
+ // on_use/on_look/on_kill/on_traverse array) that still has a top-level
+ // message: string, fold it into the action's messages list.
+ if hasMsg && isInteractionArray(parentKey) {
+ topMsg := m["message"].(string)
+ delete(m, "message")
+ *modified = true
+
+ act, ok := m["action"].(map[string]any)
+ if !ok {
+ act = map[string]any{}
+ m["action"] = act
+ }
+
+ var msgs []any
+ if existing, ok2 := act["messages"].([]any); ok2 {
+ msgs = existing
+ } else if existingMsg, ok2 := act["message"].(string); ok2 {
+ msgs = []any{map[string]any{"message": existingMsg, "delay": 0}}
+ delete(act, "message")
+ }
+ msgs = append([]any{map[string]any{"message": topMsg, "delay": 0}}, msgs...)
+ act["messages"] = msgs
+ *modified = true
+ }
+
+ // --- Step 4: trigger steps under "steps" key ---
+ // TriggerDef.Steps is []StepAction. But the key "steps" could also be
+ // ItemDef.CraftStep (craft items). We detect trigger steps by looking for
+ // trigger-specific parent fields (on_player_flag, on_global_flag, room, id).
+ // Actually we already recursed into children and migrated individual entries
+ // if they had stepActionKeys. The remaining case: a trigger step with only
+ // delay+message. We detect this after the children walk: if we're under
+ // a "steps" key and the parent map has trigger-specific keys, migrate the
+ // step's message. Since we've already recursed into the array entries,
+ // this step handles the case where individual step entries had only
+ // delay+message and weren't caught by hasStepActionKey.
+ // We detect this case in step 1 already: contextIsStepAction returns false
+ // for "steps". But if the parent map itself has trigger-like fields, we
+ // should treat "steps" as StepAction context.
+ // This is handled below.
+
+ // --- Step 5: "steps" key whose parent is a trigger ---
+ // If this map has a "steps" key AND has trigger-specific fields, re-walk
+ // the steps array with StepAction context. This is done via the recursive
+ // walk already — but contextIsStepAction("steps") returns false. We need
+ // to detect trigger-def parentage and treat steps as StepAction context.
+ //
+ // TriggerDef keys: id, on_player_flag, on_global_flag, value, room, steps
+ // CraftDef keys: type, level, skill, ingredients, success, output_qty, fail,
+ // success_message, fail_message, start_message, end_message, steps
+ //
+ // If this map has trigger-specific keys, treat "steps" as StepAction context.
+ if hasTriggerLikeKeys(m) {
+ if stepsArr, ok := m["steps"].([]any); ok {
+ m["steps"] = walk(stepsArr, "on_enter", modified)
+ }
+ }
+
+ return m
+}
+
+func hasTriggerLikeKeys(m map[string]any) bool {
+ return m["on_player_flag"] != nil || m["on_global_flag"] != nil ||
+ m["room"] != nil
+}