Implement forced BGM opcodes

This commit is contained in:
gamer147
2026-07-29 12:38:54 -04:00
parent cbd1521872
commit e24fcfff1f
14 changed files with 259 additions and 45 deletions

View File

@@ -1584,11 +1584,33 @@ public sealed class GodotAdvHost : IHost
// ---- audio ops (OGG plays natively in Godot) ----
// BGM is addressed by direct name (BGM{id:D3}.OGG); voice uses the universal packed catalog.
public void PlayBgm(long id)
=> DispatchBgm(id, 1, false);
public void RestartBgm(long id, int startMode)
=> DispatchBgm(id, startMode, true);
private void DispatchBgm(long id, int startMode, bool forceRestart)
{
var asset = _res.ResolveBgm(id);
var audio = asset != null ? LoadAudio(asset) : null;
_timeline?.Event("bgm", new() { ["id"] = id, ["file"] = audio?.Name });
if (audio != null) _main.CallDeferred("PlayBgm", audio.Bytes, audio.Name);
_timeline?.Event("bgm", new()
{
["id"] = id,
["file"] = audio?.Name,
["start_mode"] = startMode,
["force_restart"] = forceRestart,
});
if (audio == null) return;
if (forceRestart)
_main.CallDeferred("RestartBgm", audio.Bytes, audio.Name, startMode);
else
_main.CallDeferred("PlayBgm", audio.Bytes, audio.Name);
}
public void StopBgm()
{
_timeline?.Event("bgm-stop", new());
_main.CallDeferred("StopBgm");
}
public void PlayVoice(long id) => PlayVoice(id, 0);

View File

@@ -1829,12 +1829,19 @@ public partial class Main : Godot.Control
px[i + 3] = 0;
}
// Decode VFS-owned bytes in Godot. BGM loops; voice plays once, cutting off any prior line.
// Decode VFS-owned bytes in Godot. Ordinary BGM loops; forced starts retain AGE's
// loop/one-shot mode. Voice plays once, cutting off any prior line.
public void PlayBgm(byte[] oggBytes, string assetName)
=> StartBgm(oggBytes, assetName, 1);
public void RestartBgm(byte[] oggBytes, string assetName, int startMode)
=> StartBgm(oggBytes, assetName, startMode);
private void StartBgm(byte[] oggBytes, string assetName, int startMode)
{
var stream = AudioStreamOggVorbis.LoadFromBuffer(oggBytes);
if (stream == null) { GD.Print($"OGG load failed {assetName}"); return; }
stream.Loop = true;
stream.Loop = startMode != 0;
// FadeBgm is marshalled from the VM thread and starts on the next Godot frame, while the VM's
// blocking deadline begins immediately. Scene startup can consequently request the replacement
// track just before the old fade tween reaches zero. Do not let that orphaned tween mute the new
@@ -1845,6 +1852,15 @@ public partial class Main : Godot.Control
_bgm.Play();
}
public void StopBgm()
{
RestoreVoiceBgmDuck();
CancelBgmFade();
_bgm.Stop();
_bgm.Stream = null;
_bgm.VolumeDb = 0;
}
private void PersistAudioMixerSettings(AudioMixerSettingsSnapshot snapshot)
{
try
@@ -2268,8 +2284,15 @@ public partial class Main : Godot.Control
bool bgmReplacementCancelsFade = bgmFadeStarted
&& _bgmFadeTween == null
&& System.Math.Abs(_bgm.VolumeDb) < 0.001f;
_bgm.Stop();
_bgm.Stream = null;
RestartBgm(bgm.Bytes, bgm.Name, 0);
bool bgmOneShotModeOk = (_bgm.Stream as AudioStreamOggVorbis)?.Loop == false;
RestartBgm(bgm.Bytes, bgm.Name, 1);
bool bgmLoopModeOk = (_bgm.Stream as AudioStreamOggVorbis)?.Loop == true;
StopBgm();
bool bgmStopReleaseOk = _bgm.Stream == null
&& !_bgm.Playing
&& _bgmFadeTween == null
&& System.Math.Abs(_bgm.VolumeDb) < 0.001f;
AdvTextEffectTheme mode1 = ResolveAdvTextEffectTheme(AdvTextStyle.Default with
{
RenderMode = 1,
@@ -2337,18 +2360,22 @@ public partial class Main : Godot.Control
new Sys4LogicalCanvas(_screenWidth, _screenHeight));
textEffectSmoke.QueueFree();
ok &= launcherOk && sleepMinimumOk && inputTranslationOk && cp932WavMetadataOk
&& bgmReplacementCancelsFade && textEffectModesOk && fontCalibrationOk
&& bgmReplacementCancelsFade && bgmOneShotModeOk && bgmLoopModeOk
&& bgmStopReleaseOk && textEffectModesOk && fontCalibrationOk
&& logicalCanvasOk;
if (ok) GD.Print($"SELFTEST OK: threaded host matches headless ({actual.Count} lines, full handling); " +
$"debug launcher catalog/UI smoke ({debugEntries.Count} packed scripts); " +
$"sleep-min=1ms; native-key-translation=ok; cp932-wav-info=ok; " +
$"bgm-fade-replacement=ok; text-effect-modes=ok; font-calibration=ok; " +
$"bgm-fade-replacement=ok; bgm-start-modes-stop=ok; " +
$"text-effect-modes=ok; font-calibration=ok; " +
$"logical-canvas={_screenWidth}x{_screenHeight}; " +
$"window-request={_windowOptions.Width}x{_windowOptions.Height}");
else GD.Print($"SELFTEST FAIL: threaded={actual.Count} vs headless={expected.Count}; " +
$"debug-launcher={launcherOk}; sleep-min={sleepMinimumOk}; " +
$"native-key-translation={inputTranslationOk}; cp932-wav-info={cp932WavMetadataOk}; " +
$"bgm-fade-replacement={bgmReplacementCancelsFade}; " +
$"bgm-one-shot={bgmOneShotModeOk}; bgm-loop={bgmLoopModeOk}; " +
$"bgm-stop-release={bgmStopReleaseOk}; " +
$"text-effect-modes={textEffectModesOk}; font-calibration={fontCalibrationOk}; " +
$"logical-canvas={logicalCanvasOk}({_screenWidth}x{_screenHeight}); " +
$"window-request={_windowOptions.Width}x{_windowOptions.Height}");