using Age.Engine.Model; namespace Age.Engine.Vm; /// /// Replays the data-only leading ADV layout block from an engine system script. This lets a bounded /// single-scene runner inherit script-owned layout state without hard-coding one game's coordinates or /// entering the system script's later menu/session control flow. /// public static class AdvTextLayoutBootstrap { public static int ApplyLeadingDefinitionsAndResets( Script systemScript, OpcodeTable table, AdvTextHistory history) { int? defineOpcode = table.ByLabel("define-adv-text-layout"); int? resetOpcode = table.ByLabel("reset-adv-text-layout"); int? resetCursorOpcode = table.ByLabel("set-adv-text-reset-cursor"); int? boundsOpcode = table.ByLabel("set-adv-text-bounds"); int? waitIndicatorHandleOpcode = table.ByLabel("set-adv-wait-indicator-handle"); int? textObjectRangeOpcode = table.ByLabel("set-adv-text-object-range"); int? addOpcode = table.ByLabel("add"); if (defineOpcode == null || resetOpcode == null) return 0; bool foundDefinition = false; bool resetPhase = false; int definitions = 0; foreach (var instruction in systemScript.Instructions) { if (!foundDefinition && instruction.Opcode != defineOpcode.Value) continue; if (instruction.Opcode == defineOpcode.Value && !resetPhase) { if (instruction.Args.Count != 5 || instruction.Args.Any(arg => arg.Type != 0)) throw new InvalidDataException( $"{systemScript.Name}@0x{instruction.Offset:x}: leading ADV layout must use five immediates"); history.DefineLayout( checked((int)instruction.Args[0].Value), checked((int)instruction.Args[1].Value), checked((int)instruction.Args[2].Value), checked((int)instruction.Args[3].Value), checked((int)instruction.Args[4].Value)); foundDefinition = true; definitions++; continue; } if (foundDefinition && instruction.Opcode == resetOpcode.Value) { if (instruction.Args.Count != 1 || instruction.Args[0].Type != 0) throw new InvalidDataException( $"{systemScript.Name}@0x{instruction.Offset:x}: leading ADV reset must use one immediate"); resetPhase = true; history.ResetLayout(checked((int)instruction.Args[0].Value)); continue; } break; } // SYSTEM4 configures the cursor restored by future resets and the right/bottom overflow bounds // after the initial define/reset run. Most operands are immediate; slot 1 computes its bounds with // two constant add instructions. Interpret only that small, data-only expression vocabulary rather // than entering SYSTEM4's later menu/session flow. if (resetCursorOpcode != null && boundsOpcode != null) { var localInts = new Dictionary(); bool inLayoutBlock = false; int cursorConfigurations = 0; int boundConfigurations = 0; int objectRangeConfigurations = 0; foreach (var instruction in systemScript.Instructions) { if (!inLayoutBlock) { if (instruction.Opcode != defineOpcode.Value) continue; inLayoutBlock = true; } if (addOpcode != null && instruction.Opcode == addOpcode.Value) { if (instruction.Args.Count == 3 && instruction.Args[0].Type == 9 && TryResolveConstant(instruction.Args[1], localInts, out long left) && TryResolveConstant(instruction.Args[2], localInts, out long right)) localInts[checked((int)instruction.Args[0].Value)] = left + right; continue; } if (instruction.Opcode == resetCursorOpcode.Value) { var values = ResolveConfiguration(instruction, localInts, systemScript.Name); history.SetResetCursor(values[0], values[1], values[2]); cursorConfigurations++; } else if (instruction.Opcode == boundsOpcode.Value) { var values = ResolveConfiguration(instruction, localInts, systemScript.Name); history.SetBounds(values[0], values[1], values[2]); boundConfigurations++; } else if (waitIndicatorHandleOpcode != null && instruction.Opcode == waitIndicatorHandleOpcode.Value) { var values = ResolveConfiguration( instruction, localInts, systemScript.Name, expectedCount: 2); history.SetWaitIndicatorObjectHandle(values[0], values[1]); } else if (textObjectRangeOpcode != null && instruction.Opcode == textObjectRangeOpcode.Value) { var values = ResolveConfiguration(instruction, localInts, systemScript.Name); history.SetTextObjectRange(values[0], values[1], values[2]); objectRangeConfigurations++; } if (cursorConfigurations >= definitions && boundConfigurations >= definitions && (textObjectRangeOpcode == null || objectRangeConfigurations >= definitions)) break; } } return definitions; } private static int[] ResolveConfiguration( Instruction instruction, IReadOnlyDictionary localInts, string scriptName, int expectedCount = 3) { if (instruction.Args.Count != expectedCount) throw new InvalidDataException( $"{scriptName}@0x{instruction.Offset:x}: ADV layout configuration must use " + $"{expectedCount} operands"); var values = new int[expectedCount]; for (int i = 0; i < values.Length; i++) { if (!TryResolveConstant(instruction.Args[i], localInts, out long value)) throw new InvalidDataException( $"{scriptName}@0x{instruction.Offset:x}: ADV layout configuration operand {i + 1} " + "is not an immediate or constant local integer"); values[i] = checked((int)value); } return values; } private static bool TryResolveConstant( Operand operand, IReadOnlyDictionary localInts, out long value) { if (operand.Type == 0) { value = operand.Value; return true; } if (operand.Type == 9 && localInts.TryGetValue(checked((int)operand.Value), out value)) return true; value = 0; return false; } }