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 }