Correct ADV History metadata ordering

This commit is contained in:
gamer147
2026-07-19 20:55:16 -04:00
parent 8f94ef86ac
commit 3fa09b8b42
7 changed files with 107 additions and 11 deletions

View File

@@ -1292,7 +1292,7 @@ The write path is part of ordinary ADV execution:
- op `0x6e`'s glyph builder appends normal text chunks with the active geometry/font/color state. - op `0x6e`'s glyph builder appends normal text chunks with the active geometry/font/color state.
- voice op `0xc4` appends a `0x40000000` record containing `{voice_id,0}`; History replay op `0x1bd` - voice op `0xc4` appends a `0x40000000` record containing `{voice_id,0}`; History replay op `0x1bd`
uses the same writer with `{voice_id,1}` when recording is enabled. uses the same writer with `{voice_id,1}` when recording is enabled.
- op `0x1d2` appends a `0x20000000` record with operand 1 as the value and operand 2 as the metadata type. - op `0x1d2` appends a `0x20000000` record with operand 1 as the metadata type and operand 2 as its value.
It was previously misclassified as a safe statement marker; 17,323 corpus uses make this a foundational It was previously misclassified as a safe statement marker; 17,323 corpus uses make this a foundational
correction. correction.
- op `0x1bb(0)` writes suppression bit `0x80000000` at `ctx+0x55110`; op `0x1bb(1)` clears it. HISTORY uses - op `0x1bb(0)` writes suppression bit `0x80000000` at `ctx+0x55110`; op `0x1bb(1)` clears it. HISTORY uses
@@ -1409,6 +1409,13 @@ History's surrounding `0x1bb(0)` suppression prevents the replay from recording
playback. Its precise audible meaning remains unproven, so the Godot host retains it through its queue and playback. Its precise audible meaning remains unproven, so the Godot host retains it through its queue and
timeline rather than inventing different playback behavior. timeline rather than inventing different playback behavior.
A live replay trace exposed a corrected prerequisite on that path. The clicked SC0000 row found voice pair
`{0x24,0}` but returned at `HISTORY.BIN@0x9ee` because its required type-2 metadata lookup failed. Native
`op_0x1d2_append_text_history_metadata@0x41f9c0` fetches operand 2 and then operand 1 before passing them as
`(type,value)` to `text_history_append_typed_metadata@0x455f00`; the helper stores value at record `+0x14`
and type at `+0x18`. Thus SC0000's `0x1d2(2,0x11)` means type 2, character/value `0x11`, not the reverse.
The port now writes that pair in the native order, allowing HISTORY's type-2 gate to reach voice playback.
History's remaining work is the `0xd3/0xd4/0xd5` smooth-scroll callback scheduler and its small supporting History's remaining work is the `0xd3/0xd4/0xd5` smooth-scroll callback scheduler and its small supporting
gaps. None changes backlog ownership or requires choosing a save/profile backend. gaps. None changes backlog ownership or requires choosing a save/profile backend.

View File

@@ -86,9 +86,9 @@
- **evidence:** Ghidra /v2: op_0x1d1_render_text_history@0x41f950 forwards all five operands plus the text manager at ctx+0x14508 to text_history_render_records@0x4526c0. That helper walks 0x48-byte retained text records, applies record flags/colors/font state, measures strings, and rasterizes/binds them to the chosen layout. HISTORY.BIN uses it to draw each visible backlog line. - **evidence:** Ghidra /v2: op_0x1d1_render_text_history@0x41f950 forwards all five operands plus the text manager at ctx+0x14508 to text_history_render_records@0x4526c0. That helper walks 0x48-byte retained text records, applies record flags/colors/font state, measures strings, and rasterizes/binds them to the chosen layout. HISTORY.BIN uses it to draw each visible backlog line.
### 0x1d2 `append-text-history-metadata` (append-text-history-metadata, argc 2) ### 0x1d2 `append-text-history-metadata` (append-text-history-metadata, argc 2)
- **summary:** (value)(metadata_type) - append a typed metadata record to the current retained ADV message group when history recording is enabled. - **summary:** (metadata_type)(value) - append a typed metadata record to the current retained ADV message group when history recording is enabled.
- **grounding:** source=investigation, confidence=high - **grounding:** source=investigation, confidence=high
- **evidence:** Ghidra /v2: op_0x1d2_handler@0x41f9c0 tests ctx+0x55110 and, when recording is enabled, calls text_history_append_typed_metadata@0x455f00. That appends a 0x48-byte record with flag 0x20000000, operand 1 at +0x14, and operand 2 at +0x18, consuming the pending group-start flag if set. The corpus has 17,323 uses; HISTORY.BIN later queries metadata types 1 and 2 through op 0x1d3. This opcode is effectful, not a statement marker. - **evidence:** Ghidra /v2: op_0x1d2_append_text_history_metadata@0x41f9c0 tests ctx+0x55110, fetches operand 2 followed by operand 1, and calls text_history_append_typed_metadata@0x455f00 as (layout=0, type=operand1, value=operand2). The helper appends a 0x48-byte record with flag 0x20000000, value at +0x14, and type at +0x18, consuming the pending group-start flag if set. SC0000's voiced page at 0x96d writes (type=2, value=0x11), and HISTORY.BIN later queries type 2 before replay. The corpus has 17,323 uses. This opcode is effectful, not a statement marker.
### 0x1d3 `find-text-history-value` (find-text-history-value, argc 5) ### 0x1d3 `find-text-history-value` (find-text-history-value, argc 5)
- **summary:** (out_found)(out_value)(direction)(record_index)(value_type) - find typed metadata within one retained ADV message group. - **summary:** (out_found)(out_value)(direction)(record_index)(value_type) - find typed metadata within one retained ADV message group.

View File

@@ -1835,3 +1835,17 @@ indexed voice service and is saved.
**Next:** investigate and implement the `0xd3/0xd4/0xd5` smooth-scroll callback/interpolation family as a **Next:** investigate and implement the `0xd3/0xd4/0xd5` smooth-scroll callback/interpolation family as a
separate fidelity slice, including the remaining scheduler support it depends upon. separate fidelity slice, including the remaining scheduler support it depends upon.
### ADV History live voice gate correction (2026-07-19)
Manual replay remained silent, and the synchronized live trace localized the failure before the audio host:
the clicked row reached `HISTORY.BIN@0x9c7`, found retained voice `{0x24,0}`, then failed its type-2 metadata
query at `0x9e3/0x9ee` and returned without executing `0xc4` or `0x1bd`. The retained writer had reversed
op `0x1d2`'s operands. Native handler/helper dataflow and SC0000's concrete `0x1d2(2,0x11)` site prove the
ABI is `(metadata_type,value)`; the helper stores value at record `+0x14` and type at `+0x18`.
The VM now records operand 2 as the value and operand 1 as the type. The real-script regression clears the
voice request emitted by the original page, clicks the actual visible voiced History row, and requires a new
request for SC0000 `0x24`/`MAN999.OGG`; this prevents a pre-History voice from producing a false positive.
The `/v2` handler and helper comments are corrected and saved. Manual replay of that same retained row now
plays audibly, confirming the full click-to-Godot path.

View File

@@ -18,7 +18,7 @@ public class AdvTextHistoryTests
(0x75, new[] { I(24) }), (0x75, new[] { I(24) }),
(0x76, new[] { I(0xf0e0d0) }), (0x76, new[] { I(0xf0e0d0) }),
(0x7a, new[] { I(0), I(12), I(34) }), (0x7a, new[] { I(0), I(12), I(34) }),
(0x1d2, new[] { I(123), I(1) }), (0x1d2, new[] { I(1), I(123) }),
(0xc4, new[] { I(77) }), (0xc4, new[] { I(77) }),
(0x6e, new[] { I(0), S(0) }), (0x6e, new[] { I(0), S(0) }),
(0x71, new[] { I(1) }), (0x71, new[] { I(1) }),
@@ -76,7 +76,7 @@ public class AdvTextHistoryTests
{ {
(0x1bb, new[] { I(0) }), (0x1bb, new[] { I(0) }),
(0x70, new[] { I(2), I(500), I(100), I(10), I(20) }), (0x70, new[] { I(2), I(500), I(100), I(10), I(20) }),
(0x1d2, new[] { I(9), I(2) }), (0x1d2, new[] { I(2), I(9) }),
(0xc4, new[] { I(88) }), (0xc4, new[] { I(88) }),
(0x6e, new[] { I(0), S(0) }), (0x6e, new[] { I(0), S(0) }),
(0x1bb, new[] { I(1) }), (0x1bb, new[] { I(1) }),

View File

@@ -13,6 +13,7 @@ public class HistoryInteractionOpsTests
private static Operand P(int address) => new(T_LPTR, address); private static Operand P(int address) => new(T_LPTR, address);
private sealed class StopAfterHistoryReturnsException : Exception { } private sealed class StopAfterHistoryReturnsException : Exception { }
private sealed class StopAfterHistoryVoiceException : Exception { }
private static void SeedSystem4AdvLayouts(Sys4ScriptProvider scripts, AdvTextHistory history) private static void SeedSystem4AdvLayouts(Sys4ScriptProvider scripts, AdvTextHistory history)
=> Assert.Equal(9, AdvTextLayoutBootstrap.ApplyLeadingDefinitionsAndResets( => Assert.Equal(9, AdvTextLayoutBootstrap.ApplyLeadingDefinitionsAndResets(
@@ -65,6 +66,55 @@ public class HistoryInteractionOpsTests
} }
} }
private sealed class Sc0000HistoryVoiceHost : RecordingHost
{
public VirtualMachine Vm = null!;
private long _now;
private int _modalSleeps;
private bool _pressedVoiceRow;
public AdvTextHistoryRenderBatch? ClickedBatch;
public override long InputClockMilliseconds => _now;
public override void Sleep(long duration)
{
base.Sleep(duration);
_now += System.Math.Max(16, duration);
if (!Vm.IsRawInputCallbackActive) return;
_modalSleeps++;
if (!_pressedVoiceRow)
{
ClickedBatch = HistoryRenders.LastOrDefault(batch => batch.Layout.OriginY < 600
&& Vm.TextHistory.TryFindVoicePair(batch.FirstRecordIndex, out _, out _));
if (ClickedBatch == null) return;
_pressedVoiceRow = true;
Vm.UpdatePointer(200, ClickedBatch.Layout.OriginY + 50);
Vm.UpdateMouseButtonState(0x1, true);
Vm.QueueInputCallback(4);
return;
}
if (_modalSleeps == 3)
{
Vm.UpdateMouseButtonState(0x1, false);
Vm.QueueInputCallback(10);
}
if (VoiceRequests.Count > 0) throw new StopAfterHistoryVoiceException();
}
public override void WaitForInput(int layoutSlot, Func<bool> serviceInputCallback)
{
Waits++;
bool hasRecentVoice = Vm.TextHistory.Entries.TakeLast(5)
.Any(entry => Vm.TextHistory.TryFindVoicePair(entry.FirstRecordIndex, out _, out _));
if (!hasRecentVoice) return;
Voices.Clear();
VoiceRequests.Clear();
Vm.UpdatePointer(684, 572);
while (serviceInputCallback()) { }
Assert.True(Vm.TryActivatePointer(684, 572));
while (serviceInputCallback()) { }
}
}
[Fact] [Fact]
public void RealHistoryRendersMultipleRowsAfterSeveralSc0000Messages() public void RealHistoryRendersMultipleRowsAfterSeveralSc0000Messages()
{ {
@@ -189,4 +239,29 @@ public class HistoryInteractionOpsTests
Assert.NotEmpty(visibleRows); Assert.NotEmpty(visibleRows);
Assert.All(visibleRows, render => Assert.Equal(65, render.Layout.OriginX)); Assert.All(visibleRows, render => Assert.Equal(65, render.Layout.OriginX));
} }
[Fact]
public void RealHistoryVoicedRowDispatchesItsRetainedVoicePair()
{
var scripts = Sys4ScriptProvider.Load(Table);
var host = new Sc0000HistoryVoiceHost();
var vm = new VirtualMachine(scripts.RequireByName("SC0000.BIN"), Table, host,
new VmOptions(MaxSteps: 2_000_000), scripts);
host.Vm = vm;
SeedSystem4AdvLayouts(scripts, vm.TextHistory);
vm.Globals[0x6c1] = 1;
Assert.Throws<StopAfterHistoryVoiceException>(() => vm.Run());
var batch = Assert.IsType<AdvTextHistoryRenderBatch>(host.ClickedBatch);
Assert.True(vm.TextHistory.TryFindVoicePair(batch.FirstRecordIndex,
out long expectedVoice, out long expectedVariant));
Assert.Equal((0x24L, 0L), (expectedVoice, expectedVariant));
Assert.Equal(new[] { (expectedVoice, checked((int)expectedVariant)) }, host.VoiceRequests);
var resources = ResourceMap.Load();
var voice = Assert.IsType<AssetEntry>(resources.Resolve("SC0000", expectedVoice));
Assert.Equal("MAN999.OGG", voice.Name);
Assert.NotEmpty(resources.ReadAudio(voice).Bytes);
}
} }

View File

@@ -779,8 +779,8 @@ public sealed class VirtualMachine
if (enabled) _host.EndTextHistoryPresentation(); if (enabled) _host.EndTextHistoryPresentation();
} }
return pc + 1; return pc + 1;
case "append-text-history-metadata": // 0x1d2: typed value attached to the current group case "append-text-history-metadata": // 0x1d2: (metadata type, value)
TextHistory.AppendMetadata(Read(a[0]), Read(a[1]), _advTextStyle); return pc + 1; TextHistory.AppendMetadata(Read(a[1]), Read(a[0]), _advTextStyle); return pc + 1;
case "step-text-history": // 0x1d0: cumulative delta from the latest retained boundary case "step-text-history": // 0x1d0: cumulative delta from the latest retained boundary
if (TextHistory.TryStepGroup((int)Read(a[2]), out var historyEntry)) if (TextHistory.TryStepGroup((int)Read(a[2]), out var historyEntry))
{ {

View File

@@ -4349,21 +4349,21 @@ abi_source = "kelebek+decode-validated"
[opcode.semantics] [opcode.semantics]
name = "append-text-history-metadata" name = "append-text-history-metadata"
category = "adv" category = "adv"
summary = "(value)(metadata_type) - append a typed metadata record to the current retained ADV message group when history recording is enabled." summary = "(metadata_type)(value) - append a typed metadata record to the current retained ADV message group when history recording is enabled."
noop_headless = false noop_headless = false
source = "investigation" source = "investigation"
confidence = "high" confidence = "high"
depends_on = [] depends_on = []
evidence = "Ghidra /v2: op_0x1d2_handler@0x41f9c0 tests ctx+0x55110 and, when recording is enabled, calls text_history_append_typed_metadata@0x455f00. That appends a 0x48-byte record with flag 0x20000000, operand 1 at +0x14, and operand 2 at +0x18, consuming the pending group-start flag if set. The corpus has 17,323 uses; HISTORY.BIN later queries metadata types 1 and 2 through op 0x1d3. This opcode is effectful, not a statement marker." evidence = "Ghidra /v2: op_0x1d2_append_text_history_metadata@0x41f9c0 tests ctx+0x55110, fetches operand 2 followed by operand 1, and calls text_history_append_typed_metadata@0x455f00 as (layout=0, type=operand1, value=operand2). The helper appends a 0x48-byte record with flag 0x20000000, value at +0x14, and type at +0x18, consuming the pending group-start flag if set. SC0000's voiced page at 0x96d writes (type=2, value=0x11), and HISTORY.BIN later queries type 2 before replay. The corpus has 17,323 uses. This opcode is effectful, not a statement marker."
[[opcode.semantics.args]] [[opcode.semantics.args]]
i = 1 i = 1
role = "metadata value" role = "metadata type"
observed_types = ["imm"] observed_types = ["imm"]
[[opcode.semantics.args]] [[opcode.semantics.args]]
i = 2 i = 2
role = "metadata type" role = "metadata value"
observed_types = ["imm"] observed_types = ["imm"]
[[opcode]] [[opcode]]