From a0df85825ef2cf47cd79ee9979fc28ed0ab84a0a Mon Sep 17 00:00:00 2001 From: gamer147 Date: Tue, 21 Jul 2026 11:56:04 -0400 Subject: [PATCH] Implement indexed party roster sort --- docs/engine-re.md | 20 +++++ docs/global-reference.md | 8 +- docs/opcode-reference.md | 11 ++- docs/phase-b-framework.md | 12 +++ .../Age.Engine.Tests/IndexedSortOpsTests.cs | 73 +++++++++++++++++++ .../NaturalBootIntegrationTests.cs | 66 ++++++++++++++++- engine/Age.Engine/Vm/VirtualMachine.cs | 30 ++++++++ vm-map/globals.toml | 36 +++++++-- vm-map/opcodes.toml | 23 +++--- 9 files changed, 251 insertions(+), 28 deletions(-) create mode 100644 engine/Age.Engine.Tests/IndexedSortOpsTests.cs diff --git a/docs/engine-re.md b/docs/engine-re.md index 5bbffbd..be19d1f 100644 --- a/docs/engine-re.md +++ b/docs/engine-re.md @@ -2347,6 +2347,26 @@ teardown. The port now retains this engine-owned selector in `GfxState.DefaultOb selects slot 8 in the regression. The surveyed MENU/INFO scripts do not themselves call op `0x1d9`, so this state was not the cause of the former missing launch. +### Indexed two-key sort — `0x12f` (2026-07-21) + +`op_0x12f_sort_indices_by_key_sum@0x429360` takes `(out_indices, key_a, key_b, count)`. It resolves the +first three operands as integer-array bases, seeds output index zero, and performs a stable insertion sort +over source indices `0..count-1`. The comparison is signed +`unchecked(key_a[index] + key_b[index])`; a new index moves left only when its sum is strictly smaller, so +equal sums preserve source order. The native handler's final loop does not change that result: it encodes +the directly written output cells back into AGE's protected integer-bank representation. + +This is the cause of the first observed character-menu discrepancy. Natural New Game correctly runs +UNITECH and enters SC0000 with party slot 2 carrying flags `0x13` and character id 2. CHMENU constructs +100 party-slot sort keys, calls `0x12f` at `0x1c9b`, and reads the populated tail of the resulting index +permutation. The port's fallback no-op left that permutation zero-filled, so CHMENU selected slot zero and +rendered an apparently empty party even though the authoritative unit state already existed. The port now +implements the generic sort with addressed local/global arrays, native signed 32-bit addition and overflow, +stable equal-key ordering, repeated count reads, and the native unconditional output-index-zero write. +Focused tests cover stability, overflow, and zero count. A real-script regression carries natural +SYSTEM4-to-SC0000 state into CHMENU and stops after `0x1db8`, proving selected slot 2 survives the first +release roster sort without a fallback. No startup seed or menu-specific roster injection is involved. + --- ## Native walls backlog (targets for this loop) diff --git a/docs/global-reference.md b/docs/global-reference.md index f020c2e..4eee341 100644 --- a/docs/global-reference.md +++ b/docs/global-reference.md @@ -1,7 +1,7 @@ # Global Variable Reference (generated) -16417 globals (73 curated, 16344 auto shape-inferred). Source of truth: `vm-map/globals.toml`. +16417 globals (75 curated, 16342 auto shape-inferred). Source of truth: `vm-map/globals.toml`. ## choice-output @@ -27,6 +27,8 @@ | address | name | conf | source | usage | |---|---|---|---|---| | `0x2e49` | character_voice_suppressed | high | investigation | Base of the per-character voice enable/suppress settings. INITCONFIG zero-fills all 13 cells and registers each with the shared profile service; LOADCONFIG restores them. CONFIG indexes the table to preview a character voice and write 0/1. ROOM reads cell 0 before assigning its selected greeter's greeting/farewell voice ids, so the port's former scalar interpretation of zero-int-range (writing 13 into the base cell) suppressed those voices on every natural boot. This names the script-visible setting array without choosing a persistence backend for op 0x1a2/0x1a3. | +| `0x673c` | party_slot_flags | high | investigation | Per-party-slot state flags for slots 0..99. UNITECH creates the initial unit by setting slot 2 to 0x13; CALCARR counts slots whose flags intersect 0x6, and CHMENU includes slots with bit 1 set. Exact meanings of the remaining bits are not yet classified. | +| `0x67a0` | party_slot_character_id | high | investigation | Character/unit definition id stored for each party slot. UNITECH writes character id 2 into initial slot 2 on a natural New Game; CHMENU reads this table for every active party_slot_flags entry when constructing its roster. | | `0x5` | — | low | auto-shape | array | | `0xd2` | — | low | auto-shape | array | | `0xd7` | — | low | auto-shape | array | @@ -75,8 +77,6 @@ | `0x671c` | — | low | auto-shape | array | | `0x671f` | — | low | auto-shape | array | | `0x6727` | — | low | auto-shape | array | -| `0x673c` | — | low | auto-shape | array | -| `0x67a0` | — | low | auto-shape | array | | `0x6804` | — | low | auto-shape | array | | `0x6868` | — | low | auto-shape | array | | `0x6930` | — | low | auto-shape | array | @@ -337,6 +337,7 @@ | address | name | conf | source | usage | |---|---|---|---|---| +| `0x6718` | selected_party_slot | high | investigation | Current/selected slot in the 100-entry party-unit arrays. UNITECH chooses a free slot here before populating it; CHMENU replaces it with the selected sorted roster slot, then uses it to index party_slot_flags, party_slot_character_id, and companion per-slot tables. A natural New Game enters SC0000 with slot 2 selected. | | `0xeff75` | current_entity_index_hi | med | inference | High-purity current-entity row index (purity 0.95 in the auto shape map); dominant 2D-table row selector. | | `0x152616` | current_entity_index | med | investigation | Primary current-entity row index (RECOVER-confirmed; purity 0.51, 363 row-index uses). | @@ -352,7 +353,6 @@ | `0x3231` | game_mode | med | inference | Game-mode/phase selector in the 0x3231-0x3234 progression-state cluster (chapter_mode is 0x3234). Enum 1..9, written by the gameplay scripts (AIM/ALCHEMY/BTL/BUNKI), branch-read in 136 scenes. Distinct from chapter; likely current sub-mode/screen. INFERENCE — confirm with a listing/sweep before relying on the exact meaning. | | `0x3234` | chapter_mode | high | investigation | Progression chapter/mode selector. SCJUMP's top-level switch keys on it; branch-read by progression scripts (FIELD etc.), not directly by SC/SP scenes. | | `0x3275` | — | med | auto-shape | TODO: confirm. Branch-read in 136 scenes / 143 scripts; compared against [0]; writers=['CAMP.BIN', 'DEBUGADV.BIN', 'DEBUGADV2.BIN', 'FIELD.BIN']. | -| `0x6718` | — | med | auto-shape | TODO: confirm. Branch-read in 0 scenes / 6 scripts; compared against [0, 2]; writers=['ADDSKILL.BIN', 'CHMENU.BIN', 'DELCH.BIN', 'DRAWCHP.BIN']. | | `0x671b` | — | med | auto-shape | TODO: confirm. Branch-read in 0 scenes / 5 scripts; compared against [999]; writers=['ALCHEMY.BIN', 'DEBUGMAP.BIN', 'DEBUGMAP2.BIN', 'DEBUGMAP3.BIN']. | | `0x671e` | — | med | auto-shape | TODO: confirm. Branch-read in 0 scenes / 3 scripts; compared against [5, 6]; writers=['USEMAGIC.BIN']. | | `0x6722` | — | med | auto-shape | TODO: confirm. Branch-read in 13 scenes / 18 scripts; compared against [0, 1, 5, 99]; writers=['GAMESTART.BIN', 'SALLY.BIN', 'SC0010.BIN', 'SC0140.BIN']. | diff --git a/docs/opcode-reference.md b/docs/opcode-reference.md index 03e60a6..09ff845 100644 --- a/docs/opcode-reference.md +++ b/docs/opcode-reference.md @@ -215,6 +215,13 @@ Operand 2 names the base cell itself: a global-bank operand produces a global re - **grounding:** source=investigation, confidence=high - **evidence:** Ghidra /v2: op_0x6c_handler@0x426d90 resolves operand 1 as a writable VM pointer and fills operand-2 consecutive dwords with ctx->anti_tamper_b, the native encoded representation of logical integer zero. INITCONFIG@0x30 clears G[0x2e49..0x2e55] before registering those 13 profile cells; treating operand 2 as a scalar source incorrectly wrote 13 to G[0x2e49] and disabled ROOM's character greeting/farewell voices. /v2 annotated and saved 2026-07-21. +### 0x12f `sort-indices-by-key-sum` (sort-indices-by-key-sum, argc 4) +- **summary:** (out_indices, key_a, key_b, count) - write a stable ascending permutation of indices 0..count-1, ordered by the signed 32-bit sum key_a[index] + key_b[index]. +- **grounding:** source=investigation, confidence=high +- **evidence:** Ghidra /v2: op_0x12f_sort_indices_by_key_sum@0x429360 resolves operands 1-3 as integer-array bases, seeds out_indices[0]=0, then performs insertion sort. It shifts a prior index only when signed unchecked(key_a[new]+key_b[new]) is strictly less than the prior sum, so equal keys retain source order. Operand 4 is fetched as the exclusive count. The final native loop merely re-encodes direct writes into AGE's protected integer representation. Release corpus: 40 sites. CHMENU@0x1c9b sorts 100 party-slot keys and reads the populated tail; a no-op leaves the initial party slot undiscoverable even though UNITECH created it. + +Implemented with domain-preserving addressed-array access, native signed 32-bit key addition/overflow, stable insertion ordering, repeated count reads, and the native unconditional out_indices[0]=0 write. Focused tests lock stability/overflow/zero-count behavior; a natural SYSTEM4-to-SC0000 state carried into release CHMENU proves the initial slot remains selected after the real roster sort. + ### 0x135 `bit-set` (bit-set, argc 2) - **summary:** (value)(bit_index) - set the indexed bit in the destination integer. - **grounding:** source=investigation, confidence=high @@ -1022,10 +1029,6 @@ op 0x90 (u0041BEB0, argc 7): `0x90 x y w h tgt_a tgt_b tgt_c`. Kelebek left it " - **summary:** — - **grounding:** source=kelebek, confidence=med -### 0x12f `u0041ECB0` (u0041ECB0, argc 4) -- **summary:** — -- **grounding:** source=kelebek, confidence=low - ### 0x132 `u0041EF00` (u0041EF00, argc 1) - **summary:** — - **grounding:** source=kelebek, confidence=low diff --git a/docs/phase-b-framework.md b/docs/phase-b-framework.md index 48dc95d..1aea23a 100644 --- a/docs/phase-b-framework.md +++ b/docs/phase-b-framework.md @@ -293,6 +293,18 @@ SYSTEM4's same 16 immediate configuration calls. This is independent of profile/ Godot currently releases a parked ADV wait before process teardown, so the page map may contain one trailing shutdown-only page; the final timeline `input-wait` is the authoritative stop. +**SC0000 character-menu roster discrepancy (2026-07-21; investigated).** A natural-boot state probe at +SC0000 entry shows that GAMESTART/UNITECH already created party slot 2 with flags `0x13`, character id 2, +and selected slot 2. The empty Character Info page is therefore not missing boot data. CHMENU gathers that +active slot, constructs two 100-cell sort-key arrays, calls opcode `0x12f` at `0x1c9b`, and reads the +populated tail of the resulting index permutation. The port had been falling through `0x12f`, leaving the +output zero-filled and making CHMENU select empty slot zero. Native RE fully identifies `0x12f` as a stable +ascending index sort by the signed sum of two key arrays. The generic opcode is now implemented with native +32-bit overflow and stable ordering. Focused regressions cover equal keys, overflow, and zero count; a +real-script regression carries the naturally booted state into CHMENU and proves the first roster sort keeps +slot 2 selected with no `0x12f` fallback. This uses no seed or menu-specific injection. Remaining acceptance +is manual: open Character Info at the first stable SC0000 page and confirm the initial character is visible. + ## Stage B2 — Faithful full boot Replace `--boot`'s diagnostic seeding and separately injected inherited surfaces with normal boot execution. diff --git a/engine/Age.Engine.Tests/IndexedSortOpsTests.cs b/engine/Age.Engine.Tests/IndexedSortOpsTests.cs new file mode 100644 index 0000000..6c9d4e4 --- /dev/null +++ b/engine/Age.Engine.Tests/IndexedSortOpsTests.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Age.Engine.Model; +using Age.Engine.Sys4; +using Age.Engine.Vm; +using Xunit; + +public class IndexedSortOpsTests +{ + private const int Immediate = 0, GlobalInt = 3, LocalInt = 9; + + [Fact] + public void SortIndicesByKeySum_IsStableAndUsesSignedInt32Overflow() + { + var table = OpcodeTableJson.Load(Paths.OpcodesJson); + int move = table.ByLabel("mov")!.Value; + var ops = new List<(int, Operand[])>(); + + long[] secondaryKeys = { -1, 1, -1, -2, 1 }; + for (int i = 0; i < secondaryKeys.Length; i++) + ops.Add((move, new[] { new Operand(LocalInt, 20 + i), new Operand(Immediate, secondaryKeys[i]) })); + + ops.Add((0x12f, new[] + { + new Operand(LocalInt, 0), + new Operand(GlobalInt, 0x100), + new Operand(LocalInt, 20), + new Operand(Immediate, secondaryKeys.Length), + })); + for (int i = 0; i < secondaryKeys.Length; i++) + ops.Add((move, new[] { new Operand(GlobalInt, 0x200 + i), new Operand(LocalInt, i) })); + ops.Add((0x2, Array.Empty())); + + var vm = new VirtualMachine( + ScriptAssembler.Assemble(table, "INDEX_SORT", ops, Array.Empty()), + table, new RecordingHost()); + long[] primaryKeys = { 4, -2, 4, 1, int.MaxValue }; + for (int i = 0; i < primaryKeys.Length; i++) vm.Globals[0x100 + i] = primaryKeys[i]; + vm.Globals[0x104] = 0; + vm.ExternalGlobals[0x104] = int.MaxValue; // addressed global reads honor the host-owned overlay + + vm.Run(); + + // Sums are 3, -1, 3, -1, and int.MinValue after native signed 32-bit overflow. + // Equal -1 and 3 pairs retain their original relative order. + Assert.Equal(new long[] { 4, 1, 3, 0, 2 }, + Enumerable.Range(0, 5).Select(i => vm.Globals[0x200 + i]).ToArray()); + } + + [Fact] + public void SortIndicesByKeySum_WritesInitialIndexWhenCountIsZero() + { + var table = OpcodeTableJson.Load(Paths.OpcodesJson); + int move = table.ByLabel("mov")!.Value; + var script = ScriptAssembler.Assemble(table, "EMPTY_INDEX_SORT", new List<(int, Operand[])> + { + (move, new[] { new Operand(LocalInt, 0), new Operand(Immediate, 99) }), + (0x12f, new[] + { + new Operand(LocalInt, 0), new Operand(LocalInt, 10), + new Operand(LocalInt, 20), new Operand(Immediate, 0), + }), + (move, new[] { new Operand(GlobalInt, 0x200), new Operand(LocalInt, 0) }), + (0x2, Array.Empty()), + }, Array.Empty()); + var vm = new VirtualMachine(script, table, new RecordingHost()); + + vm.Run(); + + Assert.Equal(0, vm.Globals[0x200]); + } +} diff --git a/engine/Age.Engine.Tests/NaturalBootIntegrationTests.cs b/engine/Age.Engine.Tests/NaturalBootIntegrationTests.cs index 88ab2b0..3d6ae08 100644 --- a/engine/Age.Engine.Tests/NaturalBootIntegrationTests.cs +++ b/engine/Age.Engine.Tests/NaturalBootIntegrationTests.cs @@ -1,4 +1,5 @@ using Age.Engine.Diagnostics; +using Age.Engine.Model; using Age.Engine.Sys4; using Age.Engine.Vm; using Xunit; @@ -6,6 +7,11 @@ using Xunit; public class NaturalBootIntegrationTests { private sealed class ReachedSc0000Exception : Exception { } + private sealed class ReachedChmenuRosterException : Exception { } + + private sealed record NaturalBootResult( + OpcodeTable Table, Sys4ScriptProvider Scripts, VirtualMachine Vm, + NewGameInputHost Host, StopAtSc0000Sink Sink); private sealed class StopAtSc0000Sink : ITraceSink { @@ -31,6 +37,29 @@ public class NaturalBootIntegrationTests } } + private sealed class StopAfterChmenuRosterSink : ITraceSink + { + public VirtualMachine Vm = null!; + public bool SawIndexSort; + public bool SawIndexSortFallback; + public bool TracingSteps => true; + + public void Emit(in TraceEvent e) + { + if (e.Kind == TraceEventKind.Stub && e.Opcode == 0x12f) + SawIndexSortFallback = true; + if (e.Kind != TraceEventKind.Step || e.Ins == null) return; + if (e.Opcode == 0x12f) SawIndexSort = true; + + // The step event is emitted before execution. At CHMENU@0x1dbd, the first sort has + // completed and 0x1db8 has copied the chosen roster slot back to G[0x6718]. + if (e.Ins.Offset == 0x1dbd + && string.Equals(Vm.DebugFrame?.CurrentScript, "CHMENU.BIN", + StringComparison.OrdinalIgnoreCase)) + throw new ReachedChmenuRosterException(); + } + } + private sealed class NewGameInputHost : RecordingHost { public VirtualMachine Vm = null!; @@ -82,8 +111,7 @@ public class NaturalBootIntegrationTests } } - [Fact] - public void System4Root_NewGameSelectionNaturallyCallsSc0000() + private static NaturalBootResult RunNaturalNewGameToSc0000() { var table = OpcodeTableJson.Load(Paths.OpcodesJson); var scripts = Sys4ScriptProvider.Load(table); @@ -100,6 +128,16 @@ public class NaturalBootIntegrationTests var exception = Record.Exception(() => vm.Run()); Assert.True(exception is ReachedSc0000Exception, $"halt={vm.HaltReason}; title_sleeps={host.TitlePollSleeps}; entered={string.Join(",", sink.Entered)}"); + return new NaturalBootResult(table, scripts, vm, host, sink); + } + + [Fact] + public void System4Root_NewGameSelectionNaturallyCallsSc0000() + { + var boot = RunNaturalNewGameToSc0000(); + var vm = boot.Vm; + var host = boot.Host; + var sink = boot.Sink; Assert.Equal(new[] { @@ -123,4 +161,28 @@ public class NaturalBootIntegrationTests Assert.False(sink.SawStringEqualsStub); Assert.False(sink.SawUnitDataCopyStub); } + + [Fact] + public void NaturalNewGamePartyStateSurvivesRealChmenuRosterSort() + { + var boot = RunNaturalNewGameToSc0000(); + Assert.Equal(2, boot.Vm.Globals.GetValueOrDefault(0x6718)); + Assert.Equal(0x13, boot.Vm.Globals.GetValueOrDefault(0x673c + 2)); + Assert.Equal(2, boot.Vm.Globals.GetValueOrDefault(0x67a0 + 2)); + + var sink = new StopAfterChmenuRosterSink(); + var menuVm = new VirtualMachine(boot.Scripts.RequireByName("CHMENU.BIN"), boot.Table, + new RecordingHost(), new VmOptions(MaxSteps: 5_000_000), boot.Scripts, sink); + sink.Vm = menuVm; + foreach (var (address, value) in boot.Vm.Globals) menuVm.Globals[address] = value; + foreach (var (address, value) in boot.Vm.ExternalGlobals) menuVm.ExternalGlobals[address] = value; + foreach (var (address, value) in boot.Vm.GlobalStrings) menuVm.GlobalStrings[address] = value; + + var exception = Record.Exception(() => menuVm.Run()); + + Assert.IsType(exception); + Assert.True(sink.SawIndexSort); + Assert.False(sink.SawIndexSortFallback); + Assert.Equal(2, menuVm.Globals.GetValueOrDefault(0x6718)); + } } diff --git a/engine/Age.Engine/Vm/VirtualMachine.cs b/engine/Age.Engine/Vm/VirtualMachine.cs index ebadb4c..7d32881 100644 --- a/engine/Age.Engine/Vm/VirtualMachine.cs +++ b/engine/Age.Engine/Vm/VirtualMachine.cs @@ -735,6 +735,36 @@ public sealed class VirtualMachine Write(a[0], match); return pc + 1; } + case "sort-indices-by-key-sum": // 0x12f: stable ascending permutation by signed key sum + case "u0041ECB0": + { + VmAddress output = BaseAddr(a[0]); + + // Native writes element zero even when count is zero or negative, then builds the + // permutation in place with insertion sort. Read the count for each outer iteration: + // the handler fetches operand 4 repeatedly rather than caching it. + WriteIntCell(output, 0); + for (int sourceIndex = 1; sourceIndex < unchecked((int)Read(a[3])); sourceIndex++) + { + int position = sourceIndex; + while (position > 0) + { + int previousIndex = unchecked((int)ReadIntCell(output.Offset(position - 1))); + int previousKey = unchecked( + unchecked((int)ReadAddressedCell(a[1], previousIndex)) + + unchecked((int)ReadAddressedCell(a[2], previousIndex))); + int sourceKey = unchecked( + unchecked((int)ReadAddressedCell(a[1], sourceIndex)) + + unchecked((int)ReadAddressedCell(a[2], sourceIndex))); + if (sourceKey >= previousKey) break; + + WriteIntCell(output.Offset(position), previousIndex); + position--; + } + WriteIntCell(output.Offset(position), sourceIndex); + } + return pc + 1; + } case "bit-set": { long bit = Read(a[1]); diff --git a/vm-map/globals.toml b/vm-map/globals.toml index 6b1838d..9fdb5ea 100644 --- a/vm-map/globals.toml +++ b/vm-map/globals.toml @@ -316,14 +316,36 @@ depends_on = [] [[global]] address = "0x6718" -name = "" -category = "story-flag" +name = "selected_party_slot" +category = "index-pointer" type = "int" -value_domain = "one of {0, 2}" -usage = "TODO: confirm. Branch-read in 0 scenes / 6 scripts; compared against [0, 2]; writers=['ADDSKILL.BIN', 'CHMENU.BIN', 'DELCH.BIN', 'DRAWCHP.BIN']." -source = "auto-shape" -confidence = "med" -depends_on = [] +value_domain = "0..99" +usage = "Current/selected slot in the 100-entry party-unit arrays. UNITECH chooses a free slot here before populating it; CHMENU replaces it with the selected sorted roster slot, then uses it to index party_slot_flags, party_slot_character_id, and companion per-slot tables. A natural New Game enters SC0000 with slot 2 selected." +source = "investigation" +confidence = "high" +depends_on = ["0x673c", "0x67a0"] + +[[global]] +address = "0x673c" +name = "party_slot_flags" +category = "data-table" +type = "int[100]" +value_domain = "bitfield" +usage = "Per-party-slot state flags for slots 0..99. UNITECH creates the initial unit by setting slot 2 to 0x13; CALCARR counts slots whose flags intersect 0x6, and CHMENU includes slots with bit 1 set. Exact meanings of the remaining bits are not yet classified." +source = "investigation" +confidence = "high" +depends_on = ["0x6718", "0x67a0"] + +[[global]] +address = "0x67a0" +name = "party_slot_character_id" +category = "data-table" +type = "int[100]" +value_domain = "character/unit definition id or 0" +usage = "Character/unit definition id stored for each party slot. UNITECH writes character id 2 into initial slot 2 on a natural New Game; CHMENU reads this table for every active party_slot_flags entry when constructing its roster." +source = "investigation" +confidence = "high" +depends_on = ["0x6718", "0x673c"] [[global]] address = "0x671b" diff --git a/vm-map/opcodes.toml b/vm-map/opcodes.toml index 3f78ca9..133085d 100644 --- a/vm-map/opcodes.toml +++ b/vm-map/opcodes.toml @@ -2578,38 +2578,39 @@ observed_types = ["imm", "g-int", "l-int"] [[opcode]] op = 0x12f -label = "u0041ECB0" +label = "sort-indices-by-key-sum" argc = 4 abi_source = "kelebek+decode-validated" [opcode.semantics] -name = "u0041ECB0" -category = "unknown" -summary = "" +name = "sort-indices-by-key-sum" +category = "compute" +summary = "(out_indices, key_a, key_b, count) - write a stable ascending permutation of indices 0..count-1, ordered by the signed 32-bit sum key_a[index] + key_b[index]." noop_headless = false -source = "kelebek" -confidence = "low" +source = "investigation" +confidence = "high" depends_on = [] -evidence = "" +evidence = "Ghidra /v2: op_0x12f_sort_indices_by_key_sum@0x429360 resolves operands 1-3 as integer-array bases, seeds out_indices[0]=0, then performs insertion sort. It shifts a prior index only when signed unchecked(key_a[new]+key_b[new]) is strictly less than the prior sum, so equal keys retain source order. Operand 4 is fetched as the exclusive count. The final native loop merely re-encodes direct writes into AGE's protected integer representation. Release corpus: 40 sites. CHMENU@0x1c9b sorts 100 party-slot keys and reads the populated tail; a no-op leaves the initial party slot undiscoverable even though UNITECH created it." +details = "Implemented with domain-preserving addressed-array access, native signed 32-bit key addition/overflow, stable insertion ordering, repeated count reads, and the native unconditional out_indices[0]=0 write. Focused tests lock stability/overflow/zero-count behavior; a natural SYSTEM4-to-SC0000 state carried into release CHMENU proves the initial slot remains selected after the real roster sort." [[opcode.semantics.args]] i = 1 -role = "" +role = "out_index_permutation" observed_types = ["l-int"] [[opcode.semantics.args]] i = 2 -role = "" +role = "primary_key_array" observed_types = ["g-int", "l-int"] [[opcode.semantics.args]] i = 3 -role = "" +role = "secondary_key_array" observed_types = ["l-int"] [[opcode.semantics.args]] i = 4 -role = "" +role = "count" observed_types = ["imm", "l-int"] [[opcode]]