diff --git a/docs/engine-re.md b/docs/engine-re.md index ae0b6df..7f92dbe 100644 --- a/docs/engine-re.md +++ b/docs/engine-re.md @@ -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. - 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. -- 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 correction. - 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 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 gaps. None changes backlog ownership or requires choosing a save/profile backend. diff --git a/docs/opcode-reference.md b/docs/opcode-reference.md index b36b673..3834b0a 100644 --- a/docs/opcode-reference.md +++ b/docs/opcode-reference.md @@ -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. ### 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 -- **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) - **summary:** (out_found)(out_value)(direction)(record_index)(value_type) - find typed metadata within one retained ADV message group. diff --git a/docs/phase-a-slice-plan.md b/docs/phase-a-slice-plan.md index f65efeb..a4d751f 100644 --- a/docs/phase-a-slice-plan.md +++ b/docs/phase-a-slice-plan.md @@ -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 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. diff --git a/engine/Age.Engine.Tests/AdvTextHistoryTests.cs b/engine/Age.Engine.Tests/AdvTextHistoryTests.cs index fbf663a..aa1208b 100644 --- a/engine/Age.Engine.Tests/AdvTextHistoryTests.cs +++ b/engine/Age.Engine.Tests/AdvTextHistoryTests.cs @@ -18,7 +18,7 @@ public class AdvTextHistoryTests (0x75, new[] { I(24) }), (0x76, new[] { I(0xf0e0d0) }), (0x7a, new[] { I(0), I(12), I(34) }), - (0x1d2, new[] { I(123), I(1) }), + (0x1d2, new[] { I(1), I(123) }), (0xc4, new[] { I(77) }), (0x6e, new[] { I(0), S(0) }), (0x71, new[] { I(1) }), @@ -76,7 +76,7 @@ public class AdvTextHistoryTests { (0x1bb, new[] { I(0) }), (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) }), (0x6e, new[] { I(0), S(0) }), (0x1bb, new[] { I(1) }), diff --git a/engine/Age.Engine.Tests/HistoryInteractionOpsTests.cs b/engine/Age.Engine.Tests/HistoryInteractionOpsTests.cs index 79e36c0..0f0a78d 100644 --- a/engine/Age.Engine.Tests/HistoryInteractionOpsTests.cs +++ b/engine/Age.Engine.Tests/HistoryInteractionOpsTests.cs @@ -13,6 +13,7 @@ public class HistoryInteractionOpsTests private static Operand P(int address) => new(T_LPTR, address); private sealed class StopAfterHistoryReturnsException : Exception { } + private sealed class StopAfterHistoryVoiceException : Exception { } private static void SeedSystem4AdvLayouts(Sys4ScriptProvider scripts, AdvTextHistory history) => 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 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] public void RealHistoryRendersMultipleRowsAfterSeveralSc0000Messages() { @@ -189,4 +239,29 @@ public class HistoryInteractionOpsTests Assert.NotEmpty(visibleRows); 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(() => vm.Run()); + + var batch = Assert.IsType(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(resources.Resolve("SC0000", expectedVoice)); + Assert.Equal("MAN999.OGG", voice.Name); + Assert.NotEmpty(resources.ReadAudio(voice).Bytes); + } } diff --git a/engine/Age.Engine/Vm/VirtualMachine.cs b/engine/Age.Engine/Vm/VirtualMachine.cs index 2a698fe..607026f 100644 --- a/engine/Age.Engine/Vm/VirtualMachine.cs +++ b/engine/Age.Engine/Vm/VirtualMachine.cs @@ -779,8 +779,8 @@ public sealed class VirtualMachine if (enabled) _host.EndTextHistoryPresentation(); } return pc + 1; - case "append-text-history-metadata": // 0x1d2: typed value attached to the current group - TextHistory.AppendMetadata(Read(a[0]), Read(a[1]), _advTextStyle); return pc + 1; + case "append-text-history-metadata": // 0x1d2: (metadata type, value) + TextHistory.AppendMetadata(Read(a[1]), Read(a[0]), _advTextStyle); return pc + 1; case "step-text-history": // 0x1d0: cumulative delta from the latest retained boundary if (TextHistory.TryStepGroup((int)Read(a[2]), out var historyEntry)) { diff --git a/vm-map/opcodes.toml b/vm-map/opcodes.toml index 1345cee..63af5f8 100644 --- a/vm-map/opcodes.toml +++ b/vm-map/opcodes.toml @@ -4349,21 +4349,21 @@ abi_source = "kelebek+decode-validated" [opcode.semantics] name = "append-text-history-metadata" 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 source = "investigation" confidence = "high" 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]] i = 1 -role = "metadata value" +role = "metadata type" observed_types = ["imm"] [[opcode.semantics.args]] i = 2 -role = "metadata type" +role = "metadata value" observed_types = ["imm"] [[opcode]]