Restore complete numbered save state
This commit is contained in:
@@ -14,8 +14,8 @@ public class NativeNumberedSaveCodecTests
|
||||
NativeNumberedSaveState state = NativeNumberedSaveCodec.Empty(frames) with
|
||||
{
|
||||
SavedFrameOwner = 7,
|
||||
EngineState = 9,
|
||||
StateWords = Enumerable.Range(10, 10).ToArray(),
|
||||
BgmTrackId = 9,
|
||||
SoundEffectResourceIds = Enumerable.Range(10, 10).ToArray(),
|
||||
IntegerGlobals = new[] { 12, -3, 0x12345678 },
|
||||
FloatGlobals = new[] { BitConverter.SingleToInt32Bits(1.25f) },
|
||||
StringGlobals = new[] { "姫狩り", "", "save" },
|
||||
@@ -38,8 +38,8 @@ public class NativeNumberedSaveCodecTests
|
||||
NativeNumberedSaveState decoded = NativeNumberedSaveCodec.Decode(encoded);
|
||||
|
||||
Assert.Equal(state.SavedFrameOwner, decoded.SavedFrameOwner);
|
||||
Assert.Equal(state.EngineState, decoded.EngineState);
|
||||
Assert.Equal(state.StateWords, decoded.StateWords);
|
||||
Assert.Equal(state.BgmTrackId, decoded.BgmTrackId);
|
||||
Assert.Equal(state.SoundEffectResourceIds, decoded.SoundEffectResourceIds);
|
||||
Assert.Equal(state.Frames[0].ParentContext, decoded.Frames[0].ParentContext);
|
||||
Assert.Equal(state.Frames[0].ScriptId, decoded.Frames[0].ScriptId);
|
||||
Assert.Equal(state.Frames[0].ReturnIndices, decoded.Frames[0].ReturnIndices);
|
||||
|
||||
@@ -22,6 +22,9 @@ public class NumberedSaveVmTests
|
||||
var store = new DirectoryNativeDatStore(root, Identity);
|
||||
Script script = WithPackedId(ScriptAssembler.Assemble(Table, "SAVE_TEST.BIN",
|
||||
[
|
||||
(0xbf, [new Operand(Immediate, 24)]),
|
||||
(0xc0, [new Operand(GlobalInt, 0x21)]),
|
||||
(0xb4, [new Operand(Immediate, 0x3321), new Operand(Immediate, 1)]),
|
||||
(0x1ad, Array.Empty<Operand>()),
|
||||
(0x19e, [new Operand(GlobalInt, 0x20), new Operand(Immediate, 2)]),
|
||||
(0x2, Array.Empty<Operand>()),
|
||||
@@ -40,10 +43,13 @@ public class NumberedSaveVmTests
|
||||
vm.Run();
|
||||
|
||||
Assert.Equal(0, vm.Globals[0x20]);
|
||||
Assert.Equal(24, vm.Globals[0x21]);
|
||||
NativeNumberedSaveFile file = store.LoadNumberedFile(2)!;
|
||||
NativeNumberedSaveState state = NativeNumberedSaveCodec.Decode(file.Document.Payload);
|
||||
Assert.Equal(0x6241b, state.IntegerGlobals.Count);
|
||||
Assert.Equal(456, state.IntegerGlobals[0x123]);
|
||||
Assert.Equal(24, state.BgmTrackId);
|
||||
Assert.Equal(0x3321, state.SoundEffectResourceIds[1]);
|
||||
Assert.Equal("姫狩り", state.StringGlobals[4]);
|
||||
Assert.Equal(0x77u, state.Frames.Single().ScriptId);
|
||||
Assert.Contains(state.GfxObjects, item => item.Handle == 100);
|
||||
@@ -97,6 +103,12 @@ public class NumberedSaveVmTests
|
||||
new NativeSavedScriptFrame(0, 0x89, Array.Empty<int>(), -1, -1),
|
||||
]) with
|
||||
{
|
||||
BgmTrackId = 24,
|
||||
SoundEffectResourceIds =
|
||||
[
|
||||
0, 0x3321, 0x2aea, 0, 0,
|
||||
0, 0, 0, 0, 0,
|
||||
],
|
||||
IntegerGlobals = DenseIntBank(0x124, (0x123, 456)),
|
||||
FloatGlobals = [BitConverter.SingleToInt32Bits(3.5f)],
|
||||
StringGlobals = ["復帰"],
|
||||
@@ -113,8 +125,10 @@ public class NumberedSaveVmTests
|
||||
1, NativeNumberedSaveCodec.Encode(state), NativeTextHistoryCodec.Encode(history),
|
||||
NativeSystemTime.FromLocalDateTime(DateTime.Now), 123);
|
||||
var liveHistory = new AdvTextHistory();
|
||||
var trace = new RecordingTraceSink();
|
||||
var host = new RecordingHost();
|
||||
var vm = new VirtualMachine(
|
||||
loader, Table, new RecordingHost(), provider: new MapProvider(
|
||||
loader, Table, host, provider: new MapProvider(
|
||||
new()
|
||||
{
|
||||
[0x88] = resumed,
|
||||
@@ -124,22 +138,42 @@ public class NumberedSaveVmTests
|
||||
{
|
||||
["CALLBACK_LOAD.BIN"] = callback,
|
||||
}),
|
||||
textHistory: liveHistory, nativeDatStore: store);
|
||||
sink: trace, textHistory: liveHistory, nativeDatStore: store);
|
||||
vm.Globals[0x10] = 999;
|
||||
vm.Globals[0x123] = 999;
|
||||
vm.Globals[0x124] = 777;
|
||||
vm.GlobalStrings[0] = "stale";
|
||||
vm.GlobalStrings[1] = "static-unit-name";
|
||||
|
||||
vm.Run();
|
||||
|
||||
Assert.False(vm.Globals.ContainsKey(0x10));
|
||||
Assert.Equal(456, vm.Globals[0x123]);
|
||||
Assert.Equal(777, vm.Globals[0x124]);
|
||||
Assert.Equal(456, vm.Globals[0x500]);
|
||||
Assert.Equal(456, vm.Globals[0x501]);
|
||||
Assert.Equal(1, vm.Globals[0x502]);
|
||||
Assert.Equal("復帰", vm.GlobalStrings[0]);
|
||||
Assert.Equal(0x123, vm.GlobalPointers[0]);
|
||||
Assert.Equal("static-unit-name", vm.GlobalStrings[1]);
|
||||
Assert.Equal([24L], host.BgmTracks);
|
||||
Assert.Equal(
|
||||
[(0x3321L, 1), (0x2aeaL, 2)],
|
||||
host.SfxLoads);
|
||||
Assert.Equal(Enumerable.Range(0, 10), host.SfxReleases);
|
||||
Assert.Equal("履歴復帰", liveHistory.Records.Single().Text);
|
||||
Assert.Equal(3, vm.Gfx.QuerySlot(100));
|
||||
RenderObject restoredObject = Assert.Single(vm.Gfx.SnapshotVisibleObjects());
|
||||
Assert.Equal(0x1234, restoredObject.SurfaceResId);
|
||||
Assert.Equal((50, 60), (restoredObject.DstX, restoredObject.DstY));
|
||||
Assert.Contains(trace.Events, item =>
|
||||
item.Kind == Age.Engine.Diagnostics.TraceEventKind.FrameEnter
|
||||
&& item.Name == "CHILD.BIN"
|
||||
&& item.Cause == Age.Engine.Diagnostics.FrameCause.SaveRestore);
|
||||
Assert.DoesNotContain(trace.Events, item =>
|
||||
item.Kind == Age.Engine.Diagnostics.TraceEventKind.FrameEnter
|
||||
&& item.Name == "CHILD.BIN"
|
||||
&& item.Cause == Age.Engine.Diagnostics.FrameCause.CallScript);
|
||||
Assert.Equal("exit", vm.HaltReason);
|
||||
}
|
||||
finally
|
||||
|
||||
@@ -9,6 +9,8 @@ public class SaveUiIntegrationTests
|
||||
{
|
||||
private sealed class MenuReadyException : Exception;
|
||||
private sealed class InstalledResumeReachedException : Exception;
|
||||
private sealed class InstalledGameplayPollReachedException : Exception;
|
||||
private sealed class InstalledRootReloadReachedException(string message) : Exception(message);
|
||||
|
||||
private sealed class StopAtFirstMenuPollHost : RecordingHost
|
||||
{
|
||||
@@ -19,7 +21,7 @@ public class SaveUiIntegrationTests
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class ClickFirstSlotHost : RecordingHost
|
||||
private class ClickFirstSlotHost : RecordingHost
|
||||
{
|
||||
private long _now;
|
||||
private bool _pressed;
|
||||
@@ -45,6 +47,53 @@ public class SaveUiIntegrationTests
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class InstalledContinuationState(int expectedRestoreFrames)
|
||||
{
|
||||
public int RestoreFrameCount;
|
||||
public bool TerminalRestoreEntered => RestoreFrameCount >= expectedRestoreFrames;
|
||||
public readonly List<string> Frames = new();
|
||||
}
|
||||
|
||||
private sealed class ContinueInstalledLoadHost(InstalledContinuationState state) : ClickFirstSlotHost
|
||||
{
|
||||
public override void WaitForInput(
|
||||
int layoutSlot, Func<bool> serviceInputCallback, Func<AdvAutoWaitState> autoWaitState)
|
||||
{
|
||||
if (state.TerminalRestoreEntered) throw new InstalledGameplayPollReachedException();
|
||||
base.WaitForInput(layoutSlot, serviceInputCallback, autoWaitState);
|
||||
}
|
||||
|
||||
public override void Sleep(long duration)
|
||||
{
|
||||
base.Sleep(duration);
|
||||
if (state.TerminalRestoreEntered) throw new InstalledGameplayPollReachedException();
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class InstalledContinuationSink(InstalledContinuationState state) : ITraceSink
|
||||
{
|
||||
public bool TracingSteps => false;
|
||||
|
||||
public void Emit(in TraceEvent item)
|
||||
{
|
||||
if (item.Kind == TraceEventKind.FrameEnter)
|
||||
{
|
||||
state.Frames.Add($"+{item.Name}:{item.Cause}");
|
||||
if (item.Cause == FrameCause.SaveRestore
|
||||
&& !string.Equals(item.Name, "CALLBACK_LOAD.BIN",
|
||||
StringComparison.OrdinalIgnoreCase))
|
||||
state.RestoreFrameCount++;
|
||||
if (item.Cause == FrameCause.RootReload)
|
||||
throw new InstalledRootReloadReachedException(
|
||||
string.Join(", ", state.Frames));
|
||||
}
|
||||
else if (item.Kind == TraceEventKind.FrameExit)
|
||||
{
|
||||
state.Frames.Add($"-{item.Name}:{item.Text}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class SaveUiProvider(
|
||||
Sys4ScriptProvider native,
|
||||
Script? resumed,
|
||||
@@ -224,12 +273,50 @@ public class SaveUiIntegrationTests
|
||||
sharedProfile: sharedProfile,
|
||||
nativeDatStore: store);
|
||||
host.Vm = vm;
|
||||
vm.Globals[0x4] = unchecked((uint)sharedProfile.LoadInteger(0x5c3));
|
||||
vm.Globals[0x6241b] = 1;
|
||||
vm.Globals[0x696] = 0;
|
||||
|
||||
Assert.Throws<InstalledResumeReachedException>(() => vm.Run());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InstalledSave00ContinuesToRestoredGameplayPollWhenPresent()
|
||||
{
|
||||
string root = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
"Eushully", "姫狩りダンジョンマイスター", "SAVE");
|
||||
if (!File.Exists(Path.Combine(root, "SAVE00.DAT"))) return;
|
||||
|
||||
var scripts = Sys4ScriptProvider.Load(Table);
|
||||
var store = new DirectoryNativeDatStore(root, Identity);
|
||||
NativeNumberedSaveState numbered = NativeNumberedSaveCodec.Decode(
|
||||
store.LoadNumberedFile(0)!.Document.Payload);
|
||||
var state = new InstalledContinuationState(numbered.Frames.Count);
|
||||
var host = new ContinueInstalledLoadHost(state);
|
||||
var sharedProfile = new SharedProfile();
|
||||
Assert.True(sharedProfile.Load(store));
|
||||
var vm = new VirtualMachine(
|
||||
scripts.RequireByName("SAVE.BIN"), Table, host,
|
||||
new VmOptions(MaxSteps: 1_000_000), scripts,
|
||||
new InstalledContinuationSink(state),
|
||||
sharedProfile: sharedProfile,
|
||||
nativeDatStore: store);
|
||||
host.Vm = vm;
|
||||
vm.Globals[0x4] = unchecked((uint)sharedProfile.LoadInteger(0x5c3));
|
||||
vm.Globals[0x6241b] = 1;
|
||||
vm.Globals[0x696] = 0;
|
||||
|
||||
Exception? outcome = Record.Exception(() => vm.Run());
|
||||
Assert.True(outcome is InstalledGameplayPollReachedException,
|
||||
$"outcome={outcome?.GetType().Name ?? "<none>"}:{outcome?.Message}; " +
|
||||
$"halt={vm.HaltReason}; steps={vm.Steps}; waits={host.Waits}; " +
|
||||
$"movies={string.Join(",", host.ModalMovies.Select(item => item.Resource))}; " +
|
||||
$"saved={string.Join(", ", numbered.Frames.Select((frame, index) =>
|
||||
$"{index}:0x{frame.ScriptId:x}/resume={frame.ResumeIndex}/call={frame.CallTargetIndex}"))}; " +
|
||||
$"frames={string.Join(", ", state.Frames)}");
|
||||
}
|
||||
|
||||
private static Script WithPackedId(Script source, uint packedId)
|
||||
=> new()
|
||||
{
|
||||
|
||||
@@ -41,6 +41,7 @@ internal class RecordingHost : IHost
|
||||
public readonly List<int> SfxStarts = new();
|
||||
public readonly List<(int Channel, int StartMode, long DelayMs)> ScheduledSfxStarts = new();
|
||||
public readonly List<int> SfxReleases = new();
|
||||
public readonly List<long> BgmTracks = new();
|
||||
public readonly List<(int Target, long Duration)> BgmFades = new();
|
||||
public readonly List<(long Resource, int Surface, long Flags, long SyncMask)> Movies = new();
|
||||
public System.Action? OnPlayMovie;
|
||||
@@ -149,7 +150,7 @@ internal class RecordingHost : IHost
|
||||
public void DrawTexture(int slot, int sx, int sy, int w, int h, int dx, int dy)
|
||||
=> TextureDraws.Add((slot, sx, sy, w, h, dx, dy));
|
||||
public (int Width, int Height) GetTextureSize(int slot) => (0, 0);
|
||||
public void PlayBgm(long id) { }
|
||||
public void PlayBgm(long id) => BgmTracks.Add(id);
|
||||
public void PlayVoice(long id) => Voices.Add(id);
|
||||
public void PlayVoice(long id, int playbackVariant)
|
||||
{
|
||||
|
||||
@@ -14,8 +14,8 @@ public sealed record NativeSavedGfxObject(long Handle, byte[] Record);
|
||||
|
||||
public sealed record NativeNumberedSaveState(
|
||||
int SavedFrameOwner,
|
||||
int EngineState,
|
||||
IReadOnlyList<int> StateWords,
|
||||
int BgmTrackId,
|
||||
IReadOnlyList<int> SoundEffectResourceIds,
|
||||
byte[] ResourceRecords,
|
||||
byte[] SurfaceRecords,
|
||||
IReadOnlyList<NativeSavedScriptFrame> Frames,
|
||||
@@ -30,7 +30,7 @@ public sealed record NativeNumberedSaveState(
|
||||
int RangeTransformCount,
|
||||
byte[] RangeTransformRecord)
|
||||
{
|
||||
public const int StateWordCount = 10;
|
||||
public const int SoundEffectChannelCount = 10;
|
||||
public const int ResourceRecordsSize = 300 * 4;
|
||||
public const int SurfaceRecordsSize = 20_000;
|
||||
public const int GfxRecordSize = 0x2d4;
|
||||
@@ -65,8 +65,8 @@ public static class NativeNumberedSaveCodec
|
||||
|
||||
WriteInt(payload, 0, cutoff);
|
||||
WriteInt(payload, 4, state.SavedFrameOwner);
|
||||
WriteInt(payload, 8, state.EngineState);
|
||||
WriteIntList(payload, 0x0c, state.StateWords);
|
||||
WriteInt(payload, 8, state.BgmTrackId);
|
||||
WriteIntList(payload, 0x0c, state.SoundEffectResourceIds);
|
||||
state.ResourceRecords.CopyTo(payload, 0x34);
|
||||
state.SurfaceRecords.CopyTo(payload, 0x4e4);
|
||||
|
||||
@@ -119,7 +119,8 @@ public static class NativeNumberedSaveCodec
|
||||
int fixedBytes = checked(0x5718 + cutoff * FrameSize);
|
||||
Require(payload, 0, fixedBytes, "numbered-save fixed state");
|
||||
|
||||
int[] stateWords = ReadInts(payload, 0x0c, NativeNumberedSaveState.StateWordCount);
|
||||
int[] soundEffects = ReadInts(
|
||||
payload, 0x0c, NativeNumberedSaveState.SoundEffectChannelCount);
|
||||
byte[] resources = payload.Slice(0x34, NativeNumberedSaveState.ResourceRecordsSize).ToArray();
|
||||
byte[] surfaces = payload.Slice(0x4e4, NativeNumberedSaveState.SurfaceRecordsSize).ToArray();
|
||||
var frames = new NativeSavedScriptFrame[cutoff + 1];
|
||||
@@ -167,14 +168,14 @@ public static class NativeNumberedSaveCodec
|
||||
byte[] rangeRecord = payload.Slice(at + 8, gfxRecordSize).ToArray();
|
||||
|
||||
return new NativeNumberedSaveState(
|
||||
ReadInt(payload, 4), ReadInt(payload, 8), stateWords, resources, surfaces, frames,
|
||||
ReadInt(payload, 4), ReadInt(payload, 8), soundEffects, resources, surfaces, frames,
|
||||
integers, floats, strings, pointers, pointerStrings, localPointerScratch, objects,
|
||||
rangeFirst, rangeCount, rangeRecord);
|
||||
}
|
||||
|
||||
public static NativeNumberedSaveState Empty(IReadOnlyList<NativeSavedScriptFrame> frames)
|
||||
=> new(
|
||||
0, 0, new int[NativeNumberedSaveState.StateWordCount],
|
||||
0, 0, new int[NativeNumberedSaveState.SoundEffectChannelCount],
|
||||
new byte[NativeNumberedSaveState.ResourceRecordsSize],
|
||||
new byte[NativeNumberedSaveState.SurfaceRecordsSize],
|
||||
frames, Array.Empty<int>(), Array.Empty<int>(), Array.Empty<string>(),
|
||||
@@ -239,8 +240,8 @@ public static class NativeNumberedSaveCodec
|
||||
private static void ValidateState(NativeNumberedSaveState state)
|
||||
{
|
||||
if (state.Frames.Count == 0) throw new InvalidDataException("A numbered save requires at least one frame.");
|
||||
if (state.StateWords.Count != NativeNumberedSaveState.StateWordCount)
|
||||
throw new InvalidDataException("Numbered-save state word count must be 10.");
|
||||
if (state.SoundEffectResourceIds.Count != NativeNumberedSaveState.SoundEffectChannelCount)
|
||||
throw new InvalidDataException("Numbered-save sound-effect channel count must be 10.");
|
||||
if (state.ResourceRecords.Length != NativeNumberedSaveState.ResourceRecordsSize)
|
||||
throw new InvalidDataException("Numbered-save resource table must be 1,200 bytes.");
|
||||
if (state.SurfaceRecords.Length != NativeNumberedSaveState.SurfaceRecordsSize)
|
||||
|
||||
@@ -40,6 +40,9 @@ public sealed class VirtualMachine
|
||||
private NativeNumberedSaveState? _loadedNumberedState;
|
||||
private NativeNumberedSaveState? _retainedNativeNumberedState;
|
||||
private int _restoreFrameIndex = -1;
|
||||
private long _currentBgmTrackId;
|
||||
private readonly long[] _loadedSoundEffectResourceIds =
|
||||
new long[NativeNumberedSaveState.SoundEffectChannelCount];
|
||||
private uint _accumulatedPlaySeconds;
|
||||
private readonly long _sessionStartTimestamp;
|
||||
private ExecFrame? _debugActiveFrame;
|
||||
@@ -819,6 +822,9 @@ public sealed class VirtualMachine
|
||||
var gfx = NativeGfxPersistenceCodec.Capture(Gfx);
|
||||
return basis with
|
||||
{
|
||||
BgmTrackId = unchecked((int)_currentBgmTrackId),
|
||||
SoundEffectResourceIds = _loadedSoundEffectResourceIds
|
||||
.Select(id => unchecked((int)id)).ToArray(),
|
||||
Frames = frames,
|
||||
IntegerGlobals = DenseValues(Globals, himegariIntegerCount),
|
||||
FloatGlobals = DenseValues(GlobalFloats, himegariFloatCount),
|
||||
@@ -870,21 +876,24 @@ public sealed class VirtualMachine
|
||||
|
||||
private void ApplyNumberedState(NativeNumberedSaveState state)
|
||||
{
|
||||
Globals.Clear();
|
||||
for (int i = 0; i < state.IntegerGlobals.Count; i++)
|
||||
if (state.IntegerGlobals[i] != 0) Globals[i] = state.IntegerGlobals[i];
|
||||
GlobalFloats.Clear();
|
||||
for (int i = 0; i < state.FloatGlobals.Count; i++)
|
||||
if (state.FloatGlobals[i] != 0) GlobalFloats[i] = state.FloatGlobals[i];
|
||||
GlobalStrings.Clear();
|
||||
for (int i = 0; i < state.StringGlobals.Count; i++)
|
||||
if (state.StringGlobals[i].Length != 0) GlobalStrings[i] = state.StringGlobals[i];
|
||||
GlobalPointers.Clear();
|
||||
for (int i = 0; i < state.PointerGlobals.Count; i++)
|
||||
if (state.PointerGlobals[i] != 0) GlobalPointers[i] = state.PointerGlobals[i];
|
||||
GlobalStringPointers.Clear();
|
||||
for (int i = 0; i < state.PointerStrings.Count; i++)
|
||||
if (state.PointerStrings[i] != 0) GlobalStringPointers[i] = state.PointerStrings[i];
|
||||
ReplaceDensePrefix(
|
||||
Globals, state.IntegerGlobals.Select(value => (long)value).ToArray(), value => value != 0);
|
||||
ReplaceDensePrefix(
|
||||
GlobalFloats, state.FloatGlobals.Select(value => (long)value).ToArray(), value => value != 0);
|
||||
ReplaceDensePrefix(GlobalStrings, state.StringGlobals, value => value.Length != 0);
|
||||
ReplaceDensePrefix(GlobalPointers, state.PointerGlobals, value => value != 0);
|
||||
ReplaceDensePrefix(GlobalStringPointers, state.PointerStrings, value => value != 0);
|
||||
|
||||
_currentBgmTrackId = unchecked((uint)state.BgmTrackId);
|
||||
if (_currentBgmTrackId == 0) _host.FadeBgm(0, 0);
|
||||
else _host.PlayBgm(_currentBgmTrackId);
|
||||
for (int channel = 0; channel < _loadedSoundEffectResourceIds.Length; channel++)
|
||||
{
|
||||
_host.ReleaseSoundEffect(channel);
|
||||
long resourceId = unchecked((uint)state.SoundEffectResourceIds[channel]);
|
||||
_loadedSoundEffectResourceIds[channel] = resourceId;
|
||||
if (resourceId != 0) _host.LoadSoundEffect(resourceId, channel);
|
||||
}
|
||||
|
||||
GfxPersistenceSnapshot gfxSnapshot = NativeGfxPersistenceCodec.Decode(state);
|
||||
for (int slot = 0; slot < 1000; slot++) _host.ReleaseSurface(slot);
|
||||
@@ -895,6 +904,15 @@ public sealed class VirtualMachine
|
||||
}
|
||||
}
|
||||
|
||||
private static void ReplaceDensePrefix<T>(
|
||||
Dictionary<int, T> bank, IReadOnlyList<T> values, Func<T, bool> retain)
|
||||
{
|
||||
foreach (int key in bank.Keys.Where(key => (uint)key < (uint)values.Count).ToArray())
|
||||
bank.Remove(key);
|
||||
for (int i = 0; i < values.Count; i++)
|
||||
if (retain(values[i])) bank[i] = values[i];
|
||||
}
|
||||
|
||||
private Script ResolveSavedScript(NativeSavedScriptFrame frame)
|
||||
{
|
||||
if (_s.PackedId == frame.ScriptId) return _s;
|
||||
@@ -2193,7 +2211,13 @@ public sealed class VirtualMachine
|
||||
case "release-transient-surfaces": // 0x23d: native fixed range [42,1000)
|
||||
Gfx.ReleaseSurfaceRange(42, 1000 - 42);
|
||||
_host.ReleaseSurfaceRange(42, 1000 - 42); return pc + 1;
|
||||
case "play-bgm": _host.PlayBgm(Read(a[0])); return pc + 1;
|
||||
case "play-bgm":
|
||||
_currentBgmTrackId = Read(a[0]);
|
||||
_host.PlayBgm(_currentBgmTrackId);
|
||||
return pc + 1;
|
||||
case "get-current-bgm-track":
|
||||
Write(a[0], _currentBgmTrackId);
|
||||
return pc + 1;
|
||||
case "play-voice":
|
||||
_autoVoicePending = true;
|
||||
TextHistory.AppendVoice(Read(a[0]), 0, _advTextStyle);
|
||||
@@ -2208,15 +2232,33 @@ public sealed class VirtualMachine
|
||||
case "schedule-voice-playback": // 0x2c0: replace the pending delayed combat voice request
|
||||
_host.ScheduleVoicePlayback(Read(a[0]), (int)Read(a[1]), Read(a[2])); return pc + 1;
|
||||
case "play-sound-effect": // 0xb4 / semantics: sfx-load
|
||||
_host.LoadSoundEffect(Read(a[0]), (int)Read(a[1])); return pc + 1;
|
||||
{
|
||||
long resourceId = Read(a[0]);
|
||||
int channel = (int)Read(a[1]);
|
||||
if ((uint)channel < (uint)_loadedSoundEffectResourceIds.Length)
|
||||
_loadedSoundEffectResourceIds[channel] = resourceId;
|
||||
_host.LoadSoundEffect(resourceId, channel);
|
||||
return pc + 1;
|
||||
}
|
||||
case "u0041D050": // 0xb5 / semantics: sfx-start
|
||||
_host.StartSoundEffect((int)Read(a[0])); return pc + 1;
|
||||
case "u0041D080": // 0xb6 / semantics: sfx-release
|
||||
_host.ReleaseSoundEffect((int)Read(a[0])); return pc + 1;
|
||||
{
|
||||
int channel = (int)Read(a[0]);
|
||||
if ((uint)channel < (uint)_loadedSoundEffectResourceIds.Length)
|
||||
_loadedSoundEffectResourceIds[channel] = 0;
|
||||
_host.ReleaseSoundEffect(channel);
|
||||
return pc + 1;
|
||||
}
|
||||
case "schedule-sfx-start": // 0x2bf / native SetDelay(channel, start mode, delay ms)
|
||||
_host.ScheduleSoundEffectStart((int)Read(a[0]), (int)Read(a[1]), Read(a[2])); return pc + 1;
|
||||
case "u0041D2B0": // 0xc2 / semantics: fade-bgm
|
||||
_host.FadeBgm((int)Read(a[0]), Read(a[1])); return pc + 1;
|
||||
{
|
||||
int targetPercent = (int)Read(a[0]);
|
||||
_host.FadeBgm(targetPercent, Read(a[1]));
|
||||
if (targetPercent == 0) _currentBgmTrackId = 0;
|
||||
return pc + 1;
|
||||
}
|
||||
case "u00415880": // 0xd9 / semantics: clear-run-state-0x1000
|
||||
return pc + 1;
|
||||
case "get-initial-root-run": // 0x130 (out)
|
||||
|
||||
Reference in New Issue
Block a user