Implement indexed party roster sort

This commit is contained in:
gamer147
2026-07-21 11:56:04 -04:00
parent 12e5073918
commit d5f69853d5
9 changed files with 251 additions and 28 deletions

View File

@@ -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 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. 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) ## Native walls backlog (targets for this loop)

View File

@@ -1,7 +1,7 @@
<!-- DO NOT EDIT -- generated from vm-map/globals.toml by tools/globals_build.py --build --> <!-- DO NOT EDIT -- generated from vm-map/globals.toml by tools/globals_build.py --build -->
# Global Variable Reference (generated) # 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 ## choice-output
@@ -27,6 +27,8 @@
| address | name | conf | source | usage | | 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. | | `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 | | `0x5` | — | low | auto-shape | array |
| `0xd2` | — | low | auto-shape | array | | `0xd2` | — | low | auto-shape | array |
| `0xd7` | — | low | auto-shape | array | | `0xd7` | — | low | auto-shape | array |
@@ -75,8 +77,6 @@
| `0x671c` | — | low | auto-shape | array | | `0x671c` | — | low | auto-shape | array |
| `0x671f` | — | low | auto-shape | array | | `0x671f` | — | low | auto-shape | array |
| `0x6727` | — | low | auto-shape | array | | `0x6727` | — | low | auto-shape | array |
| `0x673c` | — | low | auto-shape | array |
| `0x67a0` | — | low | auto-shape | array |
| `0x6804` | — | low | auto-shape | array | | `0x6804` | — | low | auto-shape | array |
| `0x6868` | — | low | auto-shape | array | | `0x6868` | — | low | auto-shape | array |
| `0x6930` | — | low | auto-shape | array | | `0x6930` | — | low | auto-shape | array |
@@ -337,6 +337,7 @@
| address | name | conf | source | usage | | 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. | | `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). | | `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. | | `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. | | `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']. | | `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']. | | `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']. | | `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']. | | `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']. |

View File

@@ -215,6 +215,13 @@ Operand 2 names the base cell itself: a global-bank operand produces a global re
- **grounding:** source=investigation, confidence=high - **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. - **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) ### 0x135 `bit-set` (bit-set, argc 2)
- **summary:** (value)(bit_index) - set the indexed bit in the destination integer. - **summary:** (value)(bit_index) - set the indexed bit in the destination integer.
- **grounding:** source=investigation, confidence=high - **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:** — - **summary:** —
- **grounding:** source=kelebek, confidence=med - **grounding:** source=kelebek, confidence=med
### 0x12f `u0041ECB0` (u0041ECB0, argc 4)
- **summary:** —
- **grounding:** source=kelebek, confidence=low
### 0x132 `u0041EF00` (u0041EF00, argc 1) ### 0x132 `u0041EF00` (u0041EF00, argc 1)
- **summary:** — - **summary:** —
- **grounding:** source=kelebek, confidence=low - **grounding:** source=kelebek, confidence=low

View File

@@ -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 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. 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 ## Stage B2 — Faithful full boot
Replace `--boot`'s diagnostic seeding and separately injected inherited surfaces with normal boot execution. Replace `--boot`'s diagnostic seeding and separately injected inherited surfaces with normal boot execution.

View File

@@ -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<Operand>()));
var vm = new VirtualMachine(
ScriptAssembler.Assemble(table, "INDEX_SORT", ops, Array.Empty<string>()),
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<Operand>()),
}, Array.Empty<string>());
var vm = new VirtualMachine(script, table, new RecordingHost());
vm.Run();
Assert.Equal(0, vm.Globals[0x200]);
}
}

View File

@@ -1,4 +1,5 @@
using Age.Engine.Diagnostics; using Age.Engine.Diagnostics;
using Age.Engine.Model;
using Age.Engine.Sys4; using Age.Engine.Sys4;
using Age.Engine.Vm; using Age.Engine.Vm;
using Xunit; using Xunit;
@@ -6,6 +7,11 @@ using Xunit;
public class NaturalBootIntegrationTests public class NaturalBootIntegrationTests
{ {
private sealed class ReachedSc0000Exception : Exception { } 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 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 private sealed class NewGameInputHost : RecordingHost
{ {
public VirtualMachine Vm = null!; public VirtualMachine Vm = null!;
@@ -82,8 +111,7 @@ public class NaturalBootIntegrationTests
} }
} }
[Fact] private static NaturalBootResult RunNaturalNewGameToSc0000()
public void System4Root_NewGameSelectionNaturallyCallsSc0000()
{ {
var table = OpcodeTableJson.Load(Paths.OpcodesJson); var table = OpcodeTableJson.Load(Paths.OpcodesJson);
var scripts = Sys4ScriptProvider.Load(table); var scripts = Sys4ScriptProvider.Load(table);
@@ -100,6 +128,16 @@ public class NaturalBootIntegrationTests
var exception = Record.Exception(() => vm.Run()); var exception = Record.Exception(() => vm.Run());
Assert.True(exception is ReachedSc0000Exception, Assert.True(exception is ReachedSc0000Exception,
$"halt={vm.HaltReason}; title_sleeps={host.TitlePollSleeps}; entered={string.Join(",", sink.Entered)}"); $"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[] Assert.Equal(new[]
{ {
@@ -123,4 +161,28 @@ public class NaturalBootIntegrationTests
Assert.False(sink.SawStringEqualsStub); Assert.False(sink.SawStringEqualsStub);
Assert.False(sink.SawUnitDataCopyStub); 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<ReachedChmenuRosterException>(exception);
Assert.True(sink.SawIndexSort);
Assert.False(sink.SawIndexSortFallback);
Assert.Equal(2, menuVm.Globals.GetValueOrDefault(0x6718));
}
} }

View File

@@ -735,6 +735,36 @@ public sealed class VirtualMachine
Write(a[0], match); Write(a[0], match);
return pc + 1; 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": case "bit-set":
{ {
long bit = Read(a[1]); long bit = Read(a[1]);

View File

@@ -316,14 +316,36 @@ depends_on = []
[[global]] [[global]]
address = "0x6718" address = "0x6718"
name = "" name = "selected_party_slot"
category = "story-flag" category = "index-pointer"
type = "int" type = "int"
value_domain = "one of {0, 2}" value_domain = "0..99"
usage = "TODO: confirm. Branch-read in 0 scenes / 6 scripts; compared against [0, 2]; writers=['ADDSKILL.BIN', 'CHMENU.BIN', 'DELCH.BIN', 'DRAWCHP.BIN']." 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 = "auto-shape" source = "investigation"
confidence = "med" confidence = "high"
depends_on = [] 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]] [[global]]
address = "0x671b" address = "0x671b"

View File

@@ -2578,38 +2578,39 @@ observed_types = ["imm", "g-int", "l-int"]
[[opcode]] [[opcode]]
op = 0x12f op = 0x12f
label = "u0041ECB0" label = "sort-indices-by-key-sum"
argc = 4 argc = 4
abi_source = "kelebek+decode-validated" abi_source = "kelebek+decode-validated"
[opcode.semantics] [opcode.semantics]
name = "u0041ECB0" name = "sort-indices-by-key-sum"
category = "unknown" category = "compute"
summary = "" 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 noop_headless = false
source = "kelebek" source = "investigation"
confidence = "low" confidence = "high"
depends_on = [] 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]] [[opcode.semantics.args]]
i = 1 i = 1
role = "" role = "out_index_permutation"
observed_types = ["l-int"] observed_types = ["l-int"]
[[opcode.semantics.args]] [[opcode.semantics.args]]
i = 2 i = 2
role = "" role = "primary_key_array"
observed_types = ["g-int", "l-int"] observed_types = ["g-int", "l-int"]
[[opcode.semantics.args]] [[opcode.semantics.args]]
i = 3 i = 3
role = "" role = "secondary_key_array"
observed_types = ["l-int"] observed_types = ["l-int"]
[[opcode.semantics.args]] [[opcode.semantics.args]]
i = 4 i = 4
role = "" role = "count"
observed_types = ["imm", "l-int"] observed_types = ["imm", "l-int"]
[[opcode]] [[opcode]]