Implement native-backed SC0000 SFX lifecycle
This commit is contained in:
@@ -10,6 +10,7 @@ public sealed class GodotAdvHost : IHost
|
||||
private readonly ResourceMap _res;
|
||||
private readonly string _scene; // e.g. "SC0000" — for section_base
|
||||
private readonly Dictionary<int, string?> _slotBmp = new(); // slot -> pre-converted BMP path
|
||||
private readonly string?[] _sfxPaths = new string?[10]; // SC0000 native channel subset
|
||||
// slot -> dims. Slot 0 is the primary/screen surface (800x600), normally created at engine boot which
|
||||
// the single-scene harness skips; seed it so the first CG's anchor math stays correct (not 0x0).
|
||||
private readonly Dictionary<int, (int W, int H)> _slotDims = new() { { 0, (800, 600) } };
|
||||
@@ -275,6 +276,47 @@ public sealed class GodotAdvHost : IHost
|
||||
var path = asset != null ? ResourceMap.AudioPath(asset) : null;
|
||||
if (path != null) _main.CallDeferred("PlayVoice", path);
|
||||
}
|
||||
|
||||
public void LoadSoundEffect(long resourceId, int channel)
|
||||
{
|
||||
if ((uint)channel >= (uint)_sfxPaths.Length) return;
|
||||
var asset = _res.Resolve(_scene, resourceId);
|
||||
var path = asset != null ? ResourceMap.AudioPath(asset) : null;
|
||||
_sfxPaths[channel] = path;
|
||||
_timeline?.Event("sfx-load", new() { ["resource"] = resourceId, ["channel"] = channel,
|
||||
["file"] = path != null ? System.IO.Path.GetFileName(path) : null });
|
||||
if (path != null) _main.CallDeferred("LoadSoundEffect", path, channel);
|
||||
}
|
||||
|
||||
public void StartSoundEffect(int channel)
|
||||
{
|
||||
if ((uint)channel >= (uint)_sfxPaths.Length || _sfxPaths[channel] == null) return;
|
||||
_timeline?.Event("sfx-start", new() { ["channel"] = channel,
|
||||
["file"] = System.IO.Path.GetFileName(_sfxPaths[channel]) });
|
||||
_main.CallDeferred("StartSoundEffect", channel);
|
||||
}
|
||||
|
||||
public void ReleaseSoundEffect(int channel)
|
||||
{
|
||||
if ((uint)channel >= (uint)_sfxPaths.Length) return;
|
||||
_timeline?.Event("sfx-release", new() { ["channel"] = channel,
|
||||
["file"] = _sfxPaths[channel] != null ? System.IO.Path.GetFileName(_sfxPaths[channel]) : null });
|
||||
_sfxPaths[channel] = null;
|
||||
_main.CallDeferred("ReleaseSoundEffect", channel);
|
||||
}
|
||||
|
||||
public void FadeBgm(int targetPercent, long durationMs)
|
||||
{
|
||||
long ms = System.Math.Clamp(durationMs, 0, 60_000);
|
||||
double realSeconds = ms / 1000.0 / System.Math.Max(0.05, _clock.Speed);
|
||||
_timeline?.State("bgm-fade", new() { ["target_percent"] = targetPercent, ["duration_ms"] = ms });
|
||||
_main.CallDeferred("FadeBgm", targetPercent, realSeconds);
|
||||
long deadline = _clock.NowMs + ms;
|
||||
IsSleeping = true;
|
||||
while (_clock.NowMs < deadline && !_stopping) _frameSignal.WaitOne(50);
|
||||
IsSleeping = false;
|
||||
_timeline?.State("running", new() { ["bgm_fade_complete"] = true });
|
||||
}
|
||||
}
|
||||
|
||||
public readonly record struct SurfaceTextDraw(int X, int Y, string Text);
|
||||
|
||||
@@ -18,6 +18,7 @@ public partial class Main : Godot.Control
|
||||
private Label _status = null!;
|
||||
private AudioStreamPlayer _bgm = null!; // looping background music
|
||||
private AudioStreamPlayer _voice = null!; // interrupt-on-new voice
|
||||
private readonly AudioStreamPlayer[] _sfx = new AudioStreamPlayer[10]; // SC0000 channels 0..9
|
||||
private VirtualMachine _vm = null!;
|
||||
private GodotAdvHost _host = null!;
|
||||
private readonly Age.Engine.Hosting.FrameClock _clock = new();
|
||||
@@ -100,6 +101,11 @@ public partial class Main : Godot.Control
|
||||
_voice = new AudioStreamPlayer();
|
||||
AddChild(_bgm);
|
||||
AddChild(_voice);
|
||||
for (int i = 0; i < _sfx.Length; i++)
|
||||
{
|
||||
_sfx[i] = new AudioStreamPlayer();
|
||||
AddChild(_sfx[i]);
|
||||
}
|
||||
|
||||
var userArgs = OS.GetCmdlineUserArgs();
|
||||
_selftest = System.Array.IndexOf(userArgs, "--selftest") >= 0;
|
||||
@@ -168,6 +174,10 @@ public partial class Main : Godot.Control
|
||||
foreach (var kv in session.GlobalStrings) _vm.GlobalStrings[kv.Key] = kv.Value;
|
||||
GD.Print($"[boot] system boot done: {session.Globals.Count} globals seeded");
|
||||
}
|
||||
// Native AGE owns this transient secondary-SFX channel outside script-visible writes.
|
||||
// The matching SC0000 trace has value 4 at 0xc31; seed only this proven profile/slice.
|
||||
if (!_selftest && scene.Equals("SC0000", System.StringComparison.OrdinalIgnoreCase))
|
||||
_vm.ExternalGlobals[0x6242d] = 4;
|
||||
foreach (var (addr, val) in seeds) _vm.Globals[addr] = val; // --seed overrides boot state
|
||||
_ = Task.Run(() => { _vm.Run(); _done = true; });
|
||||
|
||||
@@ -214,7 +224,7 @@ public partial class Main : Godot.Control
|
||||
return;
|
||||
}
|
||||
// --shot: once the target page is composed and parked at wait-for-input, settle a few frames then grab it.
|
||||
if (_shotPath != null && !_shotDone && (_host.Pages >= _shotPage && _host.IsWaiting || _done))
|
||||
if (_shotPath != null && !_shotDone && _host != null && (_host.Pages >= _shotPage && _host.IsWaiting || _done))
|
||||
{
|
||||
if (++_shotSettle >= _shotSettleTarget)
|
||||
{
|
||||
@@ -507,6 +517,7 @@ public partial class Main : Godot.Control
|
||||
var stream = AudioStreamOggVorbis.LoadFromBuffer(System.IO.File.ReadAllBytes(oggPath));
|
||||
if (stream == null) { GD.Print($"OGG load failed {oggPath}"); return; }
|
||||
stream.Loop = true;
|
||||
_bgm.VolumeDb = 0;
|
||||
_bgm.Stream = stream;
|
||||
_bgm.Play();
|
||||
}
|
||||
@@ -520,6 +531,38 @@ public partial class Main : Godot.Control
|
||||
_voice.Play();
|
||||
}
|
||||
|
||||
public void LoadSoundEffect(string wavPath, int channel)
|
||||
{
|
||||
if ((uint)channel >= (uint)_sfx.Length) return;
|
||||
var stream = AudioStreamWav.LoadFromBuffer(System.IO.File.ReadAllBytes(wavPath));
|
||||
if (stream == null) { GD.Print($"WAV load failed {wavPath}"); return; }
|
||||
stream.LoopMode = AudioStreamWav.LoopModeEnum.Disabled;
|
||||
_sfx[channel].Stop();
|
||||
_sfx[channel].VolumeDb = 0;
|
||||
_sfx[channel].Stream = stream;
|
||||
}
|
||||
|
||||
public void StartSoundEffect(int channel)
|
||||
{
|
||||
if ((uint)channel < (uint)_sfx.Length && _sfx[channel].Stream != null)
|
||||
_sfx[channel].Play();
|
||||
}
|
||||
|
||||
public void ReleaseSoundEffect(int channel)
|
||||
{
|
||||
if ((uint)channel >= (uint)_sfx.Length) return;
|
||||
_sfx[channel].Stop();
|
||||
_sfx[channel].Stream = null;
|
||||
}
|
||||
|
||||
public void FadeBgm(int targetPercent, double realDurationSeconds)
|
||||
{
|
||||
float linear = System.Math.Clamp(targetPercent / 100.0f, 0.0f, 1.0f);
|
||||
float targetDb = linear <= 0 ? -80.0f : Mathf.LinearToDb(linear);
|
||||
if (realDurationSeconds <= 0) { _bgm.VolumeDb = targetDb; return; }
|
||||
CreateTween().TweenProperty(_bgm, "volume_db", targetDb, realDurationSeconds);
|
||||
}
|
||||
|
||||
public void AppendLine(string text) => _text.Text += text + "\n";
|
||||
public void PageBreak() { _pageCount++; _status.Text = ""; }
|
||||
public void ClearPage() { _text.Text = ""; _status.Text = ""; }
|
||||
|
||||
Reference in New Issue
Block a user