Honor ADV skip for BGM fades
This commit is contained in:
@@ -3356,17 +3356,31 @@ accept/cancel `IHost` exchange, so cancellation cannot overwrite operand 1. Godo
|
||||
focus and show AGERC's exact Japanese errors. The helper operates on the C-string prefix and encodes through
|
||||
the configured native code page, avoiding both UTF-16 indexing and accidental splitting of CP932 pairs.
|
||||
|
||||
`0xc2` is BGM rather than SFX: `op_0xc2_bgm_fade@0x4204c0` sets run-state `0x200`, arms the service timer,
|
||||
and calls `bgm_fade_arm@0x464830`. `bgm_fade_tick@0x464960` linearly interpolates current to target percent;
|
||||
durations at least 1000 ms take 100 steps, shorter durations take 10, and target zero releases the source.
|
||||
The VM is parked for the requested duration while the native main loop continues rendering. The Godot host
|
||||
mirrors that ownership split: its VM worker waits on the fade clock only after publishing the completed
|
||||
`0xc2` is BGM rather than SFX. During ordinary execution, `op_0xc2_bgm_fade@0x4204c0` sets run-state `0x200`,
|
||||
arms the service timer, and calls `bgm_fade_arm@0x464830`. `bgm_fade_tick@0x464960` linearly interpolates
|
||||
current to target percent; durations at least 1000 ms take 100 steps, shorter durations take 10, and target
|
||||
zero releases the source. The VM is parked for the requested duration while the native main loop continues
|
||||
rendering. Active ADV fast-forward (`run_state_flags & 0x08000000`) takes a separate opcode-local branch: it
|
||||
arms the requested target, applies the terminal fade tick immediately, and never sets blocking state `0x200`.
|
||||
A normal advance click does not take that branch. `adv_input_service_poll@0x411230` uses action bit `0x10` to
|
||||
release the preceding text wait, while only fast-forward bit `0x40` establishes `0x08000000`; once `0x200` is
|
||||
active, `engine_main_tick_with_exception_policy@0x411e8a` services only elapsed timer steps and does not poll
|
||||
click input for the BGM fade. The Godot host mirrors that ownership split: its VM worker waits on the fade
|
||||
clock only after publishing the completed
|
||||
pre-fade graphics burst and releasing the script presentation barrier. This is required at
|
||||
`SC0000@0x7c1`, where the three-second title-BGM fade begins after the New Game scene has already cleared
|
||||
to black; retaining presentation ownership during the wait incorrectly froze the last menu frame until the
|
||||
opening CG transition began. `0xd9` is adjacent startup control, not audio data: it clears
|
||||
run/service bit `0x1000` in the primary and, when active, secondary context and has no VM-visible result.
|
||||
|
||||
**Skip endpoint correction (2026-08-16).** The port initially sent every `0xc2` request through that blocking
|
||||
host clock, so an already-active all-message or held-Ctrl Skip appeared to stop on SC0000 pages immediately
|
||||
followed by a BGM fade. `SYSTEM4 P046` (`wait SC0000@0x4180`) is followed by a 3000 ms target-zero fade at
|
||||
`SC0000@0x418d`; the captured run also contains the same pattern at P015 (3000 ms) and P090 (1500 ms). VM audio
|
||||
dispatch now sends zero duration only for the same combined active ADV-skip predicate used by native endpoint
|
||||
branches. Non-skip dispatch retains the authored duration, preserving native's deliberately unskippable-click
|
||||
behavior. A focused regression covers both outcomes.
|
||||
|
||||
Opcode `0x1cf` belongs to the voice/BGM envelope rather than SFX. Handler
|
||||
`op_0x1cf_set_voice_bgm_duck_control@0x4209f0` replaces the transient mask at
|
||||
`EngineCtx.voice_bgm_duck_control_flags` (`ctx+0x6dbf0`). Before ordinary or History voice playback,
|
||||
|
||||
@@ -241,11 +241,11 @@ Implemented: the VM shares op 0xb7's retained-track/zero handling but supplies m
|
||||
- **evidence:** Ghidra /v2 op_0xc0_get_current_bgm_track@0x428440 writes EngineCtx.current_bgm_track_id (+0xa0b84) to operand 1. op 0xbf's bgm_play_track retains the selected direct-name id at the same music-facade field; layout-3 serialization copies it to payload +0x008 and restoration copies it back. Installed SAVE00 stores 0x18, matching FORT's BGM024.
|
||||
|
||||
### 0xc2 `fade-bgm` (u0041D2B0, argc 2)
|
||||
- **summary:** (target_percent)(duration_ms) — block script service while linearly fading current BGM volume to 0..100%. Durations >=1000 ms use 100 steps; shorter fades use 10. Target 0 releases the current BGM source at completion.
|
||||
- **summary:** (target_percent)(duration_ms) — block script service while linearly fading current BGM volume to 0..100%. Durations >=1000 ms use 100 steps; shorter fades use 10. Active ADV fast-forward applies the endpoint immediately; an ordinary advance click does not. Target 0 releases the current BGM source at completion.
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
- **evidence:** Ghidra op 0xc2 handler 0x4204c0 sets run-state 0x200, arms the service timer, and calls bgm_fade_arm@0x464830; bgm_fade_tick@0x464960 interpolates current/target percent and applies volume, releasing at target 0. Native SC0000 trace at 0x7c1/0x126c shows target 0, duration 3000, 1% ticks at about 30 ms.
|
||||
- **evidence:** Ghidra op_0xc2_bgm_fade@0x4204c0 checks run-state 0x08000000 first: active ADV fast-forward arms the target and calls bgm_fade_tick@0x464960 with a terminal delta without setting blocking state. Otherwise it sets run-state 0x200, arms the service timer, and calls bgm_fade_arm@0x464830. engine_main_tick_with_exception_policy@0x411e8a services 0x200 only from elapsed timer steps and does not poll action-4 clicks; adv_input_service_poll@0x411230 uses click bit 0x10 only to release the preceding text wait, while fast-forward bit 0x40 establishes 0x08000000. Native SC0000 trace at 0x7c1/0x126c shows target 0, duration 3000, 1% ticks at about 30 ms.
|
||||
|
||||
Implemented through the blocking host fade clock. Target zero clears the VM's retained track after the wait and invokes the shared BGM stop/release seam, so Godot does not retain a silent bound stream.
|
||||
Implemented through the blocking host fade clock. VM dispatch substitutes zero duration only when persistent/read/physical ADV fast-forward is already active, matching the native direct-endpoint branch; ordinary execution, including a click that merely released the preceding text wait, retains the authored duration. Target zero clears the VM's retained track after the wait and invokes the shared BGM stop/release seam, so Godot does not retain a silent bound stream.
|
||||
|
||||
### 0xc4 `play-voice` (play-voice, argc 1)
|
||||
- **summary:** Play a voice clip by universal packed SYS4INI/AAI id with native playback/history variant 0. While all-message Skip is active, retain/replace the queued voice request instead of starting it; playback resumes from the latest queued request after Skip clears. Uses the same packed-id opener as textures/scripts/movies, unlike play-bgm's direct BGM{id:03d} naming.
|
||||
|
||||
@@ -112,6 +112,21 @@ in the reported SC0000 line with its active 24-pixel bold Mincho style. The real
|
||||
backend, and held-Ctrl input then recorded the reported P009 coordinate and continued through P166 without a
|
||||
worker failure; the probe stopped at the later interactive name-entry boundary.
|
||||
|
||||
### SC0000 BGM fades now honor active ADV Skip (2026-08-16)
|
||||
|
||||
The runtime locator captured `SYSTEM4 P046` at `wait SC0000@0x4180`, after which all-message Skip appeared to
|
||||
stall and clicks did not advance anything until execution eventually continued. The next effectful instruction
|
||||
is `fade-bgm SC0000@0x418d`, targeting zero over 3000 ms. The Godot audio host always entered its blocking fade
|
||||
clock, and neither its click path nor a skip wake changed the deadline. The same captured run exposes equivalent
|
||||
post-page fades at P015 (3000 ms) and P090 (1500 ms).
|
||||
|
||||
Native `/v2` separates those inputs. `op_0xc2_bgm_fade@0x4204c0` immediately applies the endpoint without
|
||||
entering run-state `0x200` when ADV fast-forward state `0x08000000` is already active. A normal action-4 click
|
||||
only releases the preceding text wait; the native `0x200` service itself does not poll click input, so an
|
||||
ordinary click intentionally does not skip a BGM fade. VM audio dispatch now substitutes a zero-duration host
|
||||
fade only for active persistent/read/physical fast-forward and preserves the authored duration otherwise. The
|
||||
focused regression locks both outcomes.
|
||||
|
||||
## Stage B0 — Ground-truth reconnaissance
|
||||
|
||||
Before changing runtime architecture, record the original game's path from process start through the first
|
||||
|
||||
@@ -99,4 +99,34 @@ public class SfxOpsTests
|
||||
Assert.Equal(0, Assert.Single(host.SfxReleases));
|
||||
Assert.Equal((25, 3000L), Assert.Single(host.BgmFades));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ActiveAdvSkipForcesBgmFadeEndpointButOrdinaryDispatchKeepsAuthoredDelay()
|
||||
{
|
||||
var table = OpcodeTableJson.Load(Paths.OpcodesJson);
|
||||
var script = ScriptAssembler.Assemble(table, "BGM-FADE-SKIP",
|
||||
new List<(int, Operand[])>
|
||||
{
|
||||
(0xc2, new[] { new Operand(0, 0), new Operand(0, 3000) }),
|
||||
(0x2, Array.Empty<Operand>()),
|
||||
}, Array.Empty<string>());
|
||||
var persistentSkipScript = ScriptAssembler.Assemble(table, "BGM-FADE-PERSISTENT-SKIP",
|
||||
new List<(int, Operand[])>
|
||||
{
|
||||
(0x88, new[] { new Operand(0, 1) }),
|
||||
(0xc2, new[] { new Operand(0, 0), new Operand(0, 3000) }),
|
||||
(0x2, Array.Empty<Operand>()),
|
||||
}, Array.Empty<string>());
|
||||
var ordinaryHost = new RecordingHost();
|
||||
var physicalSkipHost = new RecordingHost { MessageSkip = true };
|
||||
var persistentSkipHost = new RecordingHost();
|
||||
|
||||
new VirtualMachine(script, table, ordinaryHost).Run();
|
||||
new VirtualMachine(script, table, physicalSkipHost).Run();
|
||||
new VirtualMachine(persistentSkipScript, table, persistentSkipHost).Run();
|
||||
|
||||
Assert.Equal((0, 3000L), Assert.Single(ordinaryHost.BgmFades));
|
||||
Assert.Equal((0, 0L), Assert.Single(physicalSkipHost.BgmFades));
|
||||
Assert.Equal((0, 0L), Assert.Single(persistentSkipHost.BgmFades));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +90,12 @@ public sealed partial class VirtualMachine
|
||||
case "u0041D2B0": // 0xc2 / semantics: fade-bgm
|
||||
{
|
||||
int targetPercent = (int)Read(a[0]);
|
||||
_host.FadeBgm(targetPercent, Read(a[1]));
|
||||
long durationMs = Read(a[1]);
|
||||
// Native op 0xc2 enters its timed run-state only during ordinary playback. An
|
||||
// already-active ADV fast-forward applies the fade endpoint immediately; a normal
|
||||
// advance click does not set that skip state and therefore retains the authored delay.
|
||||
bool forceEndpoint = _messageSkipServiceActive || _host.IsMessageSkipActive;
|
||||
_host.FadeBgm(targetPercent, forceEndpoint ? 0 : durationMs);
|
||||
if (targetPercent == 0)
|
||||
{
|
||||
_currentBgmTrackId = 0;
|
||||
|
||||
@@ -26,7 +26,7 @@ INFERRED: dict[int, dict] = {
|
||||
0xb4: dict(name='sfx-load', category='audio', noop=False, confidence='high', source='investigation', summary="(packed_raw_resource_id)(channel) — synchronously open the universal SYS4INI/AAI catalog entry and replace the channel's decoded sound buffer without starting playback. A zero high byte is a raw SYS4INI index; a nonzero high byte selects an append catalog and uses the low 24-bit index. Native manager supports channels 0..12; SC0000 uses 0..9."),
|
||||
0xb5: dict(name='sfx-start', category='audio', noop=False, confidence='high', source='investigation', summary='(channel) — start the already-loaded channel once (logical loop=false). DirectSound publishes synchronously through Play(0,0,DSBPLAY_LOOPING); the low-level flag loops only the streaming ring, while decoder EOF stops logical playback.'),
|
||||
0xb6: dict(name='sfx-release', category='audio', noop=False, confidence='high', source='investigation', summary='(channel) - stop/destroy the channel decoder and DirectSound buffer, clear its retained resource id, and leave the slot empty. Idempotent for an unused channel.'),
|
||||
0xc2: dict(name='fade-bgm', category='audio', noop=False, confidence='high', source='investigation', summary='(target_percent)(duration_ms) — block script service while linearly fading current BGM volume to 0..100%. Durations >=1000 ms use 100 steps; shorter fades use 10. Target 0 releases the current BGM source at completion.'),
|
||||
0xc2: dict(name='fade-bgm', category='audio', noop=False, confidence='high', source='investigation', summary='(target_percent)(duration_ms) — block script service while linearly fading current BGM volume to 0..100%. Durations >=1000 ms use 100 steps; shorter fades use 10. Active ADV fast-forward applies the endpoint immediately; an ordinary advance click does not. Target 0 releases the current BGM source at completion.'),
|
||||
0xcc: dict(name='register-mouse-callback', category='input', noop=False, confidence='high', source='investigation', summary='(poll_interval_ms)(target_pc) - register a timed per-frame mouse callback in the current script.'),
|
||||
0xcd: dict(name='dispatch-mouse-callback', category='input', noop=False, confidence='high', source='investigation', summary='Dispatch the registered mouse callback when its polling interval elapses.'),
|
||||
0xd3: dict(name='begin-timed-callback-sequence', category='control', noop=False, confidence='high', source='investigation', summary="Clear and initialize the current script frame's timed local-callback sequence."),
|
||||
|
||||
@@ -1946,13 +1946,13 @@ abi_source = "kelebek+decode-validated"
|
||||
[opcode.semantics]
|
||||
name = "fade-bgm"
|
||||
category = "audio"
|
||||
summary = "(target_percent)(duration_ms) — block script service while linearly fading current BGM volume to 0..100%. Durations >=1000 ms use 100 steps; shorter fades use 10. Target 0 releases the current BGM source at completion."
|
||||
summary = "(target_percent)(duration_ms) — block script service while linearly fading current BGM volume to 0..100%. Durations >=1000 ms use 100 steps; shorter fades use 10. Active ADV fast-forward applies the endpoint immediately; an ordinary advance click does not. Target 0 releases the current BGM source at completion."
|
||||
noop_headless = false
|
||||
source = "investigation"
|
||||
confidence = "high"
|
||||
depends_on = []
|
||||
evidence = "Ghidra op 0xc2 handler 0x4204c0 sets run-state 0x200, arms the service timer, and calls bgm_fade_arm@0x464830; bgm_fade_tick@0x464960 interpolates current/target percent and applies volume, releasing at target 0. Native SC0000 trace at 0x7c1/0x126c shows target 0, duration 3000, 1% ticks at about 30 ms."
|
||||
details = "Implemented through the blocking host fade clock. Target zero clears the VM's retained track after the wait and invokes the shared BGM stop/release seam, so Godot does not retain a silent bound stream."
|
||||
evidence = "Ghidra op_0xc2_bgm_fade@0x4204c0 checks run-state 0x08000000 first: active ADV fast-forward arms the target and calls bgm_fade_tick@0x464960 with a terminal delta without setting blocking state. Otherwise it sets run-state 0x200, arms the service timer, and calls bgm_fade_arm@0x464830. engine_main_tick_with_exception_policy@0x411e8a services 0x200 only from elapsed timer steps and does not poll action-4 clicks; adv_input_service_poll@0x411230 uses click bit 0x10 only to release the preceding text wait, while fast-forward bit 0x40 establishes 0x08000000. Native SC0000 trace at 0x7c1/0x126c shows target 0, duration 3000, 1% ticks at about 30 ms."
|
||||
details = "Implemented through the blocking host fade clock. VM dispatch substitutes zero duration only when persistent/read/physical ADV fast-forward is already active, matching the native direct-endpoint branch; ordinary execution, including a click that merely released the preceding text wait, retains the authored duration. Target zero clears the VM's retained track after the wait and invokes the shared BGM stop/release seam, so Godot does not retain a silent bound stream."
|
||||
|
||||
[[opcode.semantics.args]]
|
||||
i = 1
|
||||
|
||||
Reference in New Issue
Block a user