Implement native layout-3 save restoration
This commit is contained in:
@@ -43,6 +43,8 @@ public sealed record NativeSaveIdentity(
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record NativeNumberedSaveFile(NativeSaveDocument Document, byte[] HistoryTail);
|
||||
|
||||
public interface INativeDatStore
|
||||
{
|
||||
NativeSaveIdentity Identity { get; }
|
||||
@@ -53,6 +55,15 @@ public interface INativeDatStore
|
||||
NativeSaveMetadata? QueryNumberedMetadata(int slot);
|
||||
NativeSaveDocument? LoadNumbered(int slot);
|
||||
void SaveNumbered(int slot, ReadOnlySpan<byte> payload, NativeSystemTime timestamp, uint accumulatedPlaySeconds);
|
||||
NativeNumberedSaveFile? LoadNumberedFile(int slot)
|
||||
{
|
||||
NativeSaveDocument? document = LoadNumbered(slot);
|
||||
return document == null ? null : new NativeNumberedSaveFile(document, []);
|
||||
}
|
||||
void SaveNumberedFile(
|
||||
int slot, ReadOnlySpan<byte> payload, ReadOnlySpan<byte> historyTail,
|
||||
NativeSystemTime timestamp, uint accumulatedPlaySeconds)
|
||||
=> SaveNumbered(slot, payload, timestamp, accumulatedPlaySeconds);
|
||||
int DeleteNumberedPair(int slot);
|
||||
int CopyNumberedPair(int sourceSlot, int destinationSlot);
|
||||
byte[]? LoadNumberedThumbnail(int slot);
|
||||
@@ -160,6 +171,17 @@ public sealed class DirectoryNativeDatStore : INativeDatStore
|
||||
return File.Exists(path) ? LoadAndValidate(path, numbered: true) : null;
|
||||
}
|
||||
|
||||
public NativeNumberedSaveFile? LoadNumberedFile(int slot)
|
||||
{
|
||||
string path = Path.Combine(_root, NumberedFileName(slot));
|
||||
if (!File.Exists(path)) return null;
|
||||
byte[] source = File.ReadAllBytes(path);
|
||||
NativeSaveDocument document = NativeSaveContainerCodec.Decode(source);
|
||||
_identity.Validate(document.Metadata, numbered: true);
|
||||
return new NativeNumberedSaveFile(
|
||||
document, source.AsSpan(document.BytesConsumed).ToArray());
|
||||
}
|
||||
|
||||
public NativeSaveMetadata? QueryNumberedMetadata(int slot)
|
||||
{
|
||||
string path = Path.Combine(_root, NumberedFileName(slot));
|
||||
@@ -186,6 +208,22 @@ public sealed class DirectoryNativeDatStore : INativeDatStore
|
||||
WriteThrough(Path.Combine(_root, NumberedFileName(slot)), encoded);
|
||||
}
|
||||
|
||||
public void SaveNumberedFile(
|
||||
int slot,
|
||||
ReadOnlySpan<byte> payload,
|
||||
ReadOnlySpan<byte> historyTail,
|
||||
NativeSystemTime timestamp,
|
||||
uint accumulatedPlaySeconds)
|
||||
{
|
||||
byte[] container = NativeSaveContainerCodec.Encode(
|
||||
payload, _identity.CreateMetadata(timestamp, accumulatedPlaySeconds, numbered: true));
|
||||
byte[] encoded = new byte[checked(container.Length + historyTail.Length)];
|
||||
container.CopyTo(encoded, 0);
|
||||
historyTail.CopyTo(encoded.AsSpan(container.Length));
|
||||
Directory.CreateDirectory(_root);
|
||||
WriteThrough(Path.Combine(_root, NumberedFileName(slot)), encoded);
|
||||
}
|
||||
|
||||
public int DeleteNumberedPair(int slot)
|
||||
{
|
||||
bool dataDeleted = TryDelete(Path.Combine(_root, NumberedFileName(slot)));
|
||||
|
||||
200
engine/Age.Engine/Persistence/NativeGfxPersistenceCodec.cs
Normal file
200
engine/Age.Engine/Persistence/NativeGfxPersistenceCodec.cs
Normal file
@@ -0,0 +1,200 @@
|
||||
using System.Buffers.Binary;
|
||||
using Age.Engine.Model;
|
||||
|
||||
namespace Age.Engine.Persistence;
|
||||
|
||||
internal static class NativeGfxPersistenceCodec
|
||||
{
|
||||
private const int SurfaceCount = 1000;
|
||||
private const int SurfaceRecordSize = 20;
|
||||
|
||||
public static (
|
||||
byte[] SurfaceRecords,
|
||||
IReadOnlyList<NativeSavedGfxObject> Objects,
|
||||
long RangeFirst,
|
||||
int RangeCount,
|
||||
byte[] RangeRecord) Capture(GfxState gfx)
|
||||
{
|
||||
GfxPersistenceSnapshot snapshot = gfx.CapturePersistenceSnapshot();
|
||||
byte[] surfaces = new byte[NativeNumberedSaveState.SurfaceRecordsSize];
|
||||
foreach (var (slot, resourceId, colorKey, created) in snapshot.Surfaces)
|
||||
{
|
||||
if ((uint)slot >= SurfaceCount || created || resourceId < 0) continue;
|
||||
int at = slot * SurfaceRecordSize;
|
||||
WriteInt(surfaces, at, unchecked((int)resourceId));
|
||||
WriteInt(surfaces, at + 4, PackNativeColorKey(colorKey));
|
||||
WriteInt(surfaces, at + 8, 1);
|
||||
}
|
||||
NativeSavedGfxObject[] objects = snapshot.Objects
|
||||
.Select(item => new NativeSavedGfxObject(item.Handle, EncodeObject(item.Object)))
|
||||
.ToArray();
|
||||
return (
|
||||
surfaces, objects, snapshot.RangeFirst, unchecked((int)snapshot.RangeCount),
|
||||
EncodeObject(snapshot.RangeTransform));
|
||||
}
|
||||
|
||||
public static GfxPersistenceSnapshot Decode(NativeNumberedSaveState state)
|
||||
{
|
||||
var surfaces = new List<(int Slot, long ResourceId, long ColorKey, bool Created)>();
|
||||
for (int slot = 0; slot < SurfaceCount; slot++)
|
||||
{
|
||||
int at = slot * SurfaceRecordSize;
|
||||
int resourceId = ReadInt(state.SurfaceRecords, at);
|
||||
int present = ReadInt(state.SurfaceRecords, at + 8);
|
||||
if (present == 1 && resourceId >= 0)
|
||||
surfaces.Add((slot, unchecked((uint)resourceId),
|
||||
UnpackNativeColorKey(ReadInt(state.SurfaceRecords, at + 4)), false));
|
||||
}
|
||||
var objects = state.GfxObjects
|
||||
.Select(item => (item.Handle, DecodeObject(item.Record)))
|
||||
.ToArray();
|
||||
return new GfxPersistenceSnapshot(
|
||||
surfaces, objects, state.RangeTransformFirst, state.RangeTransformCount,
|
||||
DecodeObject(state.RangeTransformRecord));
|
||||
}
|
||||
|
||||
private static byte[] EncodeObject(GfxState.GfxObject value)
|
||||
{
|
||||
byte[] raw = new byte[NativeNumberedSaveState.GfxRecordSize];
|
||||
int flags = value.Visible ? 1 : 0;
|
||||
if (value.RotationEnabled || value.SrcAnim || value.ColorAnim) flags |= 4;
|
||||
WriteInt(raw, 0, flags);
|
||||
WriteInt(raw, 4, value.SourceSlot);
|
||||
WriteInt(raw, 8, value.SrcRect.X);
|
||||
WriteInt(raw, 0x0c, value.SrcRect.Y);
|
||||
WriteInt(raw, 0x10, checked(value.SrcRect.X + value.SrcRect.W));
|
||||
WriteInt(raw, 0x14, checked(value.SrcRect.Y + value.SrcRect.H));
|
||||
WriteVector(raw, 0x18, value.V18);
|
||||
WriteVector(raw, 0x24, value.V24);
|
||||
WriteInt(raw, 0x30, unchecked((int)value.StaticColorMode));
|
||||
WriteInt(raw, 0x34, unchecked((int)value.OneShotStartMs));
|
||||
WriteInt(raw, 0x38, unchecked((int)value.ColorDelayMs));
|
||||
WriteInt(raw, 0x3c, unchecked((int)value.ScaleDelayMs));
|
||||
WriteInt(raw, 0x40, unchecked((int)value.RotationDelayMs));
|
||||
WriteInt(raw, 0x44, unchecked((int)value.TranslationDelayMs));
|
||||
WriteInt(raw, 0x4c, unchecked((int)value.ColorDurationMs));
|
||||
WriteInt(raw, 0x50, unchecked((int)value.ScaleDurationMs));
|
||||
WriteInt(raw, 0x54, unchecked((int)value.RotationDurationMs));
|
||||
WriteInt(raw, 0x58, unchecked((int)value.TranslationDurationMs));
|
||||
WriteInt(raw, 0x60, unchecked((int)value.Color));
|
||||
WriteInt(raw, 0x64, unchecked((int)value.OneShotColorTarget));
|
||||
WriteScaleMatrix(raw, 0x6c, value.ScaleCurrent);
|
||||
WriteScaleMatrix(raw, 0xac, value.ScaleTarget);
|
||||
WriteFloat(raw, 0x16c, value.TranslationCurrent.X);
|
||||
WriteFloat(raw, 0x170, value.TranslationCurrent.Y);
|
||||
WriteFloat(raw, 0x174, value.TranslationCurrent.Z);
|
||||
WriteFloat(raw, 0x1ac, value.TranslationTarget.X);
|
||||
WriteFloat(raw, 0x1b0, value.TranslationTarget.Y);
|
||||
WriteFloat(raw, 0x1b4, value.TranslationTarget.Z);
|
||||
WriteInt(raw, 0x20c, unchecked((int)value.ColorStart));
|
||||
WriteInt(raw, 0x214, unchecked((int)value.RotationStartMs));
|
||||
WriteInt(raw, 0x220, unchecked((int)value.ColorPeriod));
|
||||
WriteInt(raw, 0x228, unchecked((int)value.RotationPeriodMs));
|
||||
WriteInt(raw, 0x230, unchecked((int)value.SrcPeriod));
|
||||
WriteInt(raw, 0x234, unchecked((int)value.SrcCell));
|
||||
WriteInt(raw, 0x238, unchecked((int)value.SrcFrameCount));
|
||||
WriteInt(raw, 0x23c, unchecked((int)value.SrcColumns));
|
||||
WriteInt(raw, 0x240, unchecked((int)value.ColorTarget));
|
||||
WriteVector(raw, 0x244, value.RotationAxis);
|
||||
WriteInt(raw, 0x2d0, unchecked((int)value.OneShotAnimationControlFlags));
|
||||
return raw;
|
||||
}
|
||||
|
||||
private static GfxState.GfxObject DecodeObject(ReadOnlySpan<byte> raw)
|
||||
{
|
||||
if (raw.Length != NativeNumberedSaveState.GfxRecordSize)
|
||||
throw new InvalidDataException("Native retained-gfx record has the wrong size.");
|
||||
int left = ReadInt(raw, 8), top = ReadInt(raw, 0x0c);
|
||||
int right = ReadInt(raw, 0x10), bottom = ReadInt(raw, 0x14);
|
||||
int flags = ReadInt(raw, 0);
|
||||
return new GfxState.GfxObject
|
||||
{
|
||||
Visible = (flags & 1) != 0,
|
||||
SourceSlot = ReadInt(raw, 4),
|
||||
SrcRect = (left, top, right - left, bottom - top),
|
||||
V18 = ReadLongVector(raw, 0x18),
|
||||
V24 = ReadLongVector(raw, 0x24),
|
||||
StaticColorMode = ReadInt(raw, 0x30),
|
||||
OneShotStartMs = ReadInt(raw, 0x34),
|
||||
ColorDelayMs = ReadInt(raw, 0x38),
|
||||
ScaleDelayMs = ReadInt(raw, 0x3c),
|
||||
RotationDelayMs = ReadInt(raw, 0x40),
|
||||
TranslationDelayMs = ReadInt(raw, 0x44),
|
||||
ColorDurationMs = ReadInt(raw, 0x4c),
|
||||
ScaleDurationMs = ReadInt(raw, 0x50),
|
||||
RotationDurationMs = ReadInt(raw, 0x54),
|
||||
TranslationDurationMs = ReadInt(raw, 0x58),
|
||||
Color = unchecked((uint)ReadInt(raw, 0x60)),
|
||||
HasColor = ReadInt(raw, 0x60) != -1,
|
||||
OneShotColorTarget = unchecked((uint)ReadInt(raw, 0x64)),
|
||||
ScaleCurrent = ReadScale(raw, 0x6c),
|
||||
ScaleTarget = ReadScale(raw, 0xac),
|
||||
TranslationCurrent = ReadDoubleVector(raw, 0x16c),
|
||||
V16c = ReadLongFloatVector(raw, 0x16c),
|
||||
TranslationTarget = ReadDoubleVector(raw, 0x1ac),
|
||||
ColorStart = ReadInt(raw, 0x20c),
|
||||
RotationStartMs = ReadInt(raw, 0x214),
|
||||
ColorPeriod = ReadInt(raw, 0x220),
|
||||
RotationPeriodMs = ReadInt(raw, 0x228),
|
||||
SrcPeriod = ReadInt(raw, 0x230),
|
||||
SrcCell = ReadInt(raw, 0x234),
|
||||
SrcFrameCount = Math.Max(1, ReadInt(raw, 0x238)),
|
||||
SrcColumns = Math.Max(1, ReadInt(raw, 0x23c)),
|
||||
ColorTarget = unchecked((uint)ReadInt(raw, 0x240)),
|
||||
RotationAxis = ReadLongVector(raw, 0x244),
|
||||
OneShotAnimationControlFlags = unchecked((uint)ReadInt(raw, 0x2d0)),
|
||||
RotationEnabled = (flags & 4) != 0 && ReadInt(raw, 0x228) > 0,
|
||||
SrcAnim = (flags & 4) != 0 && ReadInt(raw, 0x238) > 1,
|
||||
ColorAnim = (flags & 4) != 0 && ReadInt(raw, 0x220) > 0,
|
||||
OneShotColorEnabled = ReadInt(raw, 0x4c) > 0,
|
||||
ScaleEnabled = ReadInt(raw, 0x50) > 0,
|
||||
RotationChannelEnabled = ReadInt(raw, 0x54) > 0,
|
||||
TranslationEnabled = ReadInt(raw, 0x58) > 0,
|
||||
};
|
||||
}
|
||||
|
||||
private static int PackNativeColorKey(long colorKey)
|
||||
=> colorKey < 0 ? 0 : unchecked((int)(0xff000000 | (uint)colorKey & 0xffffff));
|
||||
|
||||
private static long UnpackNativeColorKey(int colorKey)
|
||||
=> colorKey == 0 ? -1 : unchecked((uint)colorKey) & 0xffffff;
|
||||
|
||||
private static void WriteScaleMatrix(Span<byte> raw, int offset, (double X, double Y, double Z) scale)
|
||||
{
|
||||
WriteFloat(raw, offset, scale.X);
|
||||
WriteFloat(raw, offset + 0x14, scale.Y);
|
||||
WriteFloat(raw, offset + 0x28, scale.Z);
|
||||
WriteFloat(raw, offset + 0x3c, 1);
|
||||
}
|
||||
|
||||
private static (double X, double Y, double Z) ReadScale(ReadOnlySpan<byte> raw, int offset)
|
||||
=> (ReadFloat(raw, offset), ReadFloat(raw, offset + 0x14), ReadFloat(raw, offset + 0x28));
|
||||
|
||||
private static void WriteVector(Span<byte> raw, int offset, (long X, long Y, long Z) vector)
|
||||
{
|
||||
WriteInt(raw, offset, unchecked((int)vector.X));
|
||||
WriteInt(raw, offset + 4, unchecked((int)vector.Y));
|
||||
WriteInt(raw, offset + 8, unchecked((int)vector.Z));
|
||||
}
|
||||
|
||||
private static (long X, long Y, long Z) ReadLongVector(ReadOnlySpan<byte> raw, int offset)
|
||||
=> (ReadInt(raw, offset), ReadInt(raw, offset + 4), ReadInt(raw, offset + 8));
|
||||
|
||||
private static (long X, long Y, long Z) ReadLongFloatVector(ReadOnlySpan<byte> raw, int offset)
|
||||
=> ((long)ReadFloat(raw, offset), (long)ReadFloat(raw, offset + 4), (long)ReadFloat(raw, offset + 8));
|
||||
|
||||
private static (double X, double Y, double Z) ReadDoubleVector(ReadOnlySpan<byte> raw, int offset)
|
||||
=> (ReadFloat(raw, offset), ReadFloat(raw, offset + 4), ReadFloat(raw, offset + 8));
|
||||
|
||||
private static void WriteFloat(Span<byte> raw, int offset, double value)
|
||||
=> WriteInt(raw, offset, BitConverter.SingleToInt32Bits((float)value));
|
||||
|
||||
private static float ReadFloat(ReadOnlySpan<byte> raw, int offset)
|
||||
=> BitConverter.Int32BitsToSingle(ReadInt(raw, offset));
|
||||
|
||||
private static int ReadInt(ReadOnlySpan<byte> raw, int offset)
|
||||
=> BinaryPrimitives.ReadInt32LittleEndian(raw.Slice(offset, 4));
|
||||
|
||||
private static void WriteInt(Span<byte> raw, int offset, int value)
|
||||
=> BinaryPrimitives.WriteInt32LittleEndian(raw.Slice(offset, 4), value);
|
||||
}
|
||||
294
engine/Age.Engine/Persistence/NativeNumberedSaveCodec.cs
Normal file
294
engine/Age.Engine/Persistence/NativeNumberedSaveCodec.cs
Normal file
@@ -0,0 +1,294 @@
|
||||
using System.Buffers.Binary;
|
||||
using System.Text;
|
||||
|
||||
namespace Age.Engine.Persistence;
|
||||
|
||||
public sealed record NativeSavedScriptFrame(
|
||||
int ParentContext,
|
||||
uint ScriptId,
|
||||
IReadOnlyList<int> ReturnIndices,
|
||||
int ResumeIndex,
|
||||
int CallTargetIndex);
|
||||
|
||||
public sealed record NativeSavedGfxObject(long Handle, byte[] Record);
|
||||
|
||||
public sealed record NativeNumberedSaveState(
|
||||
int SavedFrameOwner,
|
||||
int EngineState,
|
||||
IReadOnlyList<int> StateWords,
|
||||
byte[] ResourceRecords,
|
||||
byte[] SurfaceRecords,
|
||||
IReadOnlyList<NativeSavedScriptFrame> Frames,
|
||||
IReadOnlyList<int> IntegerGlobals,
|
||||
IReadOnlyList<int> FloatGlobals,
|
||||
IReadOnlyList<string> StringGlobals,
|
||||
IReadOnlyList<int> PointerGlobals,
|
||||
IReadOnlyList<int> PointerStrings,
|
||||
IReadOnlyList<int> LocalPointerScratch,
|
||||
IReadOnlyList<NativeSavedGfxObject> GfxObjects,
|
||||
long RangeTransformFirst,
|
||||
int RangeTransformCount,
|
||||
byte[] RangeTransformRecord)
|
||||
{
|
||||
public const int StateWordCount = 10;
|
||||
public const int ResourceRecordsSize = 300 * 4;
|
||||
public const int SurfaceRecordsSize = 20_000;
|
||||
public const int GfxRecordSize = 0x2d4;
|
||||
}
|
||||
|
||||
/// <summary>AGE SaveVersion1=3 numbered-save logical payload.</summary>
|
||||
public static class NativeNumberedSaveCodec
|
||||
{
|
||||
private const int FixedPrefixSize = 0x5304;
|
||||
private const int FrameSize = 0x414;
|
||||
private const int FixedSuffixSize = 0x414;
|
||||
private const int FrameReturnCapacity = 256;
|
||||
private const int GfxAllocationDwords = 0x2d8;
|
||||
private const int GfxConstantDwords = 0x2e1;
|
||||
private static readonly Encoding NativeEncoding = CreateNativeEncoding();
|
||||
|
||||
public static byte[] Encode(NativeNumberedSaveState state)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(state);
|
||||
ValidateState(state);
|
||||
|
||||
int cutoff = state.Frames.Count - 1;
|
||||
byte[] strings = EncodeStrings(state.StringGlobals);
|
||||
int stringDwords = strings.Length / 4;
|
||||
int bankDwords = checked(
|
||||
state.IntegerGlobals.Count + state.FloatGlobals.Count + state.StringGlobals.Count
|
||||
+ state.PointerGlobals.Count + state.PointerStrings.Count + state.LocalPointerScratch.Count);
|
||||
int totalDwords = checked(
|
||||
cutoff * 0x105 + 0x53ea + bankDwords + stringDwords
|
||||
+ GfxConstantDwords + state.GfxObjects.Count * GfxAllocationDwords);
|
||||
byte[] payload = new byte[checked((totalDwords - 2) * 4)];
|
||||
|
||||
WriteInt(payload, 0, cutoff);
|
||||
WriteInt(payload, 4, state.SavedFrameOwner);
|
||||
WriteInt(payload, 8, state.EngineState);
|
||||
WriteIntList(payload, 0x0c, state.StateWords);
|
||||
state.ResourceRecords.CopyTo(payload, 0x34);
|
||||
state.SurfaceRecords.CopyTo(payload, 0x4e4);
|
||||
|
||||
for (int i = 0; i < state.Frames.Count; i++)
|
||||
WriteFrame(payload, FixedPrefixSize + i * FrameSize, state.Frames[i], i == cutoff);
|
||||
|
||||
int at = checked(0x5718 + cutoff * FrameSize);
|
||||
int[] counts =
|
||||
[
|
||||
state.IntegerGlobals.Count, state.FloatGlobals.Count, state.StringGlobals.Count,
|
||||
state.PointerGlobals.Count, state.PointerStrings.Count, state.LocalPointerScratch.Count,
|
||||
];
|
||||
WriteIntList(payload, at, counts);
|
||||
at += 24;
|
||||
WriteIntList(payload, at, state.IntegerGlobals);
|
||||
at += state.IntegerGlobals.Count * 4;
|
||||
WriteIntList(payload, at, state.FloatGlobals);
|
||||
at += state.FloatGlobals.Count * 4;
|
||||
WriteInt(payload, at, stringDwords);
|
||||
at += 4;
|
||||
strings.CopyTo(payload, at);
|
||||
at += strings.Length;
|
||||
WriteIntList(payload, at, state.PointerGlobals);
|
||||
at += state.PointerGlobals.Count * 4;
|
||||
WriteIntList(payload, at, state.PointerStrings);
|
||||
at += state.PointerStrings.Count * 4;
|
||||
WriteIntList(payload, at, state.LocalPointerScratch);
|
||||
at += state.LocalPointerScratch.Count * 4;
|
||||
|
||||
WriteInt(payload, at, NativeNumberedSaveState.GfxRecordSize);
|
||||
WriteInt(payload, at + 4, state.GfxObjects.Count);
|
||||
at += 8;
|
||||
foreach (NativeSavedGfxObject gfx in state.GfxObjects)
|
||||
{
|
||||
WriteInt(payload, at, unchecked((int)gfx.Handle));
|
||||
gfx.Record.CopyTo(payload, at + 4);
|
||||
at += 4 + NativeNumberedSaveState.GfxRecordSize;
|
||||
}
|
||||
WriteInt(payload, at, unchecked((int)state.RangeTransformFirst));
|
||||
WriteInt(payload, at + 4, state.RangeTransformCount);
|
||||
state.RangeTransformRecord.CopyTo(payload, at + 8);
|
||||
return payload;
|
||||
}
|
||||
|
||||
public static NativeNumberedSaveState Decode(ReadOnlySpan<byte> payload)
|
||||
{
|
||||
if (payload.Length < 0x5718)
|
||||
throw new InvalidDataException("Numbered-save layout 3 payload is truncated.");
|
||||
int cutoff = ReadNonNegative(payload, 0, "saved frame cutoff");
|
||||
int fixedBytes = checked(0x5718 + cutoff * FrameSize);
|
||||
Require(payload, 0, fixedBytes, "numbered-save fixed state");
|
||||
|
||||
int[] stateWords = ReadInts(payload, 0x0c, NativeNumberedSaveState.StateWordCount);
|
||||
byte[] resources = payload.Slice(0x34, NativeNumberedSaveState.ResourceRecordsSize).ToArray();
|
||||
byte[] surfaces = payload.Slice(0x4e4, NativeNumberedSaveState.SurfaceRecordsSize).ToArray();
|
||||
var frames = new NativeSavedScriptFrame[cutoff + 1];
|
||||
for (int i = 0; i < frames.Length; i++)
|
||||
frames[i] = ReadFrame(payload, FixedPrefixSize + i * FrameSize);
|
||||
|
||||
int at = fixedBytes;
|
||||
int[] counts = ReadInts(payload, at, 6);
|
||||
if (counts.Any(count => count < 0))
|
||||
throw new InvalidDataException("Numbered-save layout 3 contains a negative bank count.");
|
||||
at += 24;
|
||||
int[] integers = ReadInts(payload, at, counts[0]);
|
||||
at = checked(at + counts[0] * 4);
|
||||
int[] floats = ReadInts(payload, at, counts[1]);
|
||||
at = checked(at + counts[1] * 4);
|
||||
int stringDwords = ReadNonNegative(payload, at, "string blob length");
|
||||
at += 4;
|
||||
int stringBytes = checked(stringDwords * 4);
|
||||
Require(payload, at, stringBytes, "numbered-save string blob");
|
||||
string[] strings = DecodeStrings(payload.Slice(at, stringBytes), counts[2]);
|
||||
at += stringBytes;
|
||||
int[] pointers = ReadInts(payload, at, counts[3]);
|
||||
at = checked(at + counts[3] * 4);
|
||||
int[] pointerStrings = ReadInts(payload, at, counts[4]);
|
||||
at = checked(at + counts[4] * 4);
|
||||
int[] localPointerScratch = ReadInts(payload, at, counts[5]);
|
||||
at = checked(at + counts[5] * 4);
|
||||
|
||||
int gfxRecordSize = ReadNonNegative(payload, at, "gfx record size");
|
||||
int gfxCount = ReadNonNegative(payload, at + 4, "gfx object count");
|
||||
if (gfxRecordSize != NativeNumberedSaveState.GfxRecordSize)
|
||||
throw new InvalidDataException($"Unsupported native gfx record size 0x{gfxRecordSize:x}.");
|
||||
at += 8;
|
||||
var objects = new NativeSavedGfxObject[gfxCount];
|
||||
for (int i = 0; i < objects.Length; i++)
|
||||
{
|
||||
Require(payload, at, 4 + gfxRecordSize, "numbered-save gfx object");
|
||||
long handle = ReadInt(payload, at);
|
||||
objects[i] = new NativeSavedGfxObject(handle, payload.Slice(at + 4, gfxRecordSize).ToArray());
|
||||
at += 4 + gfxRecordSize;
|
||||
}
|
||||
Require(payload, at, 8 + gfxRecordSize, "numbered-save range transform");
|
||||
long rangeFirst = ReadInt(payload, at);
|
||||
int rangeCount = ReadInt(payload, at + 4);
|
||||
byte[] rangeRecord = payload.Slice(at + 8, gfxRecordSize).ToArray();
|
||||
|
||||
return new NativeNumberedSaveState(
|
||||
ReadInt(payload, 4), ReadInt(payload, 8), stateWords, 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],
|
||||
new byte[NativeNumberedSaveState.ResourceRecordsSize],
|
||||
new byte[NativeNumberedSaveState.SurfaceRecordsSize],
|
||||
frames, Array.Empty<int>(), Array.Empty<int>(), Array.Empty<string>(),
|
||||
Array.Empty<int>(), Array.Empty<int>(), Array.Empty<int>(),
|
||||
Array.Empty<NativeSavedGfxObject>(), 0, 0,
|
||||
new byte[NativeNumberedSaveState.GfxRecordSize]);
|
||||
|
||||
private static void WriteFrame(Span<byte> payload, int offset, NativeSavedScriptFrame frame, bool terminal)
|
||||
{
|
||||
if (frame.ReturnIndices.Count > FrameReturnCapacity)
|
||||
throw new InvalidDataException("A native saved frame cannot hold more than 256 return entries.");
|
||||
WriteInt(payload, offset, frame.ParentContext);
|
||||
WriteInt(payload, offset + 4, unchecked((int)frame.ScriptId));
|
||||
WriteInt(payload, offset + 8, frame.ReturnIndices.Count);
|
||||
for (int i = 0; i < frame.ReturnIndices.Count; i++)
|
||||
WriteInt(payload, offset + 12 + i * 4, frame.ReturnIndices[i]);
|
||||
WriteInt(payload, offset + 0x40c, frame.ResumeIndex);
|
||||
WriteInt(payload, offset + 0x410, terminal ? -1 : frame.CallTargetIndex);
|
||||
}
|
||||
|
||||
private static NativeSavedScriptFrame ReadFrame(ReadOnlySpan<byte> payload, int offset)
|
||||
{
|
||||
int count = ReadNonNegative(payload, offset + 8, "frame return count");
|
||||
if (count > FrameReturnCapacity)
|
||||
throw new InvalidDataException("Numbered-save frame return count exceeds 256.");
|
||||
return new NativeSavedScriptFrame(
|
||||
ReadInt(payload, offset), unchecked((uint)ReadInt(payload, offset + 4)),
|
||||
ReadInts(payload, offset + 12, count),
|
||||
ReadInt(payload, offset + 0x40c), ReadInt(payload, offset + 0x410));
|
||||
}
|
||||
|
||||
private static byte[] EncodeStrings(IReadOnlyList<string> strings)
|
||||
{
|
||||
using var stream = new MemoryStream();
|
||||
foreach (string text in strings)
|
||||
{
|
||||
byte[] encoded = NativeEncoding.GetBytes(text ?? "");
|
||||
stream.Write(encoded);
|
||||
stream.WriteByte(0);
|
||||
}
|
||||
while ((stream.Length & 3) != 0) stream.WriteByte(0);
|
||||
return stream.ToArray();
|
||||
}
|
||||
|
||||
private static string[] DecodeStrings(ReadOnlySpan<byte> blob, int count)
|
||||
{
|
||||
var result = new string[count];
|
||||
int at = 0;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
if (at >= blob.Length)
|
||||
throw new InvalidDataException("Numbered-save string blob ended before its declared string count.");
|
||||
int end = blob[at..].IndexOf((byte)0);
|
||||
if (end < 0)
|
||||
throw new InvalidDataException("Numbered-save string blob ended before its declared string count.");
|
||||
result[i] = NativeEncoding.GetString(blob.Slice(at, end));
|
||||
at += end + 1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
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.ResourceRecords.Length != NativeNumberedSaveState.ResourceRecordsSize)
|
||||
throw new InvalidDataException("Numbered-save resource table must be 1,200 bytes.");
|
||||
if (state.SurfaceRecords.Length != NativeNumberedSaveState.SurfaceRecordsSize)
|
||||
throw new InvalidDataException("Numbered-save surface table must be 20,000 bytes.");
|
||||
if (state.RangeTransformRecord.Length != NativeNumberedSaveState.GfxRecordSize
|
||||
|| state.GfxObjects.Any(item => item.Record.Length != NativeNumberedSaveState.GfxRecordSize))
|
||||
throw new InvalidDataException("Numbered-save gfx records must be 0x2d4 bytes.");
|
||||
}
|
||||
|
||||
private static int[] ReadInts(ReadOnlySpan<byte> source, int offset, int count)
|
||||
{
|
||||
int bytes = checked(count * 4);
|
||||
Require(source, offset, bytes, "numbered-save integer array");
|
||||
var values = new int[count];
|
||||
for (int i = 0; i < count; i++) values[i] = ReadInt(source, offset + i * 4);
|
||||
return values;
|
||||
}
|
||||
|
||||
private static void WriteIntList(Span<byte> destination, int offset, IReadOnlyList<int> values)
|
||||
{
|
||||
for (int i = 0; i < values.Count; i++) WriteInt(destination, offset + i * 4, values[i]);
|
||||
}
|
||||
|
||||
private static int ReadNonNegative(ReadOnlySpan<byte> source, int offset, string name)
|
||||
{
|
||||
int value = ReadInt(source, offset);
|
||||
if (value < 0) throw new InvalidDataException($"Numbered-save {name} is negative.");
|
||||
return value;
|
||||
}
|
||||
|
||||
private static int ReadInt(ReadOnlySpan<byte> source, int offset)
|
||||
{
|
||||
Require(source, offset, 4, "numbered-save dword");
|
||||
return BinaryPrimitives.ReadInt32LittleEndian(source.Slice(offset, 4));
|
||||
}
|
||||
|
||||
private static void WriteInt(Span<byte> destination, int offset, int value)
|
||||
=> BinaryPrimitives.WriteInt32LittleEndian(destination.Slice(offset, 4), value);
|
||||
|
||||
private static void Require(ReadOnlySpan<byte> source, int offset, int count, string name)
|
||||
{
|
||||
if (offset < 0 || count < 0 || offset > source.Length - count)
|
||||
throw new InvalidDataException($"{name} is truncated.");
|
||||
}
|
||||
|
||||
private static Encoding CreateNativeEncoding()
|
||||
{
|
||||
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
||||
return Encoding.GetEncoding(932);
|
||||
}
|
||||
}
|
||||
179
engine/Age.Engine/Persistence/NativeTextHistoryCodec.cs
Normal file
179
engine/Age.Engine/Persistence/NativeTextHistoryCodec.cs
Normal file
@@ -0,0 +1,179 @@
|
||||
using System.Buffers.Binary;
|
||||
using System.Text;
|
||||
using Age.Engine.Model;
|
||||
using Age.Engine.Sys4;
|
||||
|
||||
namespace Age.Engine.Persistence;
|
||||
|
||||
/// <summary>The non-container LZSS tail appended to a layout-3 numbered save.</summary>
|
||||
public static class NativeTextHistoryCodec
|
||||
{
|
||||
private const int HeaderSize = 12;
|
||||
private const int RecordDwords = 11;
|
||||
private static readonly Encoding NativeEncoding = CreateNativeEncoding();
|
||||
|
||||
public static byte[] Encode(AdvTextHistory history)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(history);
|
||||
byte[] logical = EncodeLogical(history);
|
||||
byte[] stored = LzssEncoder.EncodeOrVerbatim(logical);
|
||||
byte[] result = new byte[HeaderSize + stored.Length];
|
||||
WriteInt(result, 0, logical.Length);
|
||||
WriteInt(result, 4, logical.Length);
|
||||
WriteInt(result, 8, stored.Length);
|
||||
stored.CopyTo(result, HeaderSize);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static void DecodeInto(ReadOnlySpan<byte> source, AdvTextHistory history)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(history);
|
||||
if (source.Length < HeaderSize) throw new InvalidDataException("Native text-history tail is truncated.");
|
||||
int logicalLength = ReadNonNegative(source, 0, "logical length");
|
||||
int duplicateLength = ReadNonNegative(source, 4, "duplicate logical length");
|
||||
int storedLength = ReadNonNegative(source, 8, "stored length");
|
||||
if (duplicateLength != logicalLength)
|
||||
throw new InvalidDataException("Native text-history length fields disagree.");
|
||||
if (source.Length < HeaderSize + storedLength)
|
||||
throw new InvalidDataException("Native text-history compressed stream is truncated.");
|
||||
ReadOnlySpan<byte> stored = source.Slice(HeaderSize, storedLength);
|
||||
byte[] logical = storedLength == logicalLength
|
||||
? stored.ToArray()
|
||||
: LzssDecoder.Decode(stored, logicalLength, "native text history");
|
||||
DecodeLogical(logical, history);
|
||||
}
|
||||
|
||||
private static byte[] EncodeLogical(AdvTextHistory history)
|
||||
{
|
||||
using var stream = new MemoryStream();
|
||||
using var writer = new BinaryWriter(stream, Encoding.UTF8, leaveOpen: true);
|
||||
writer.Write(history.Entries.Count);
|
||||
foreach (AdvTextHistoryEntry entry in history.Entries)
|
||||
{
|
||||
writer.Write(entry.LayoutSlot);
|
||||
writer.Write(entry.FirstRecordIndex);
|
||||
}
|
||||
writer.Write(history.Records.Count);
|
||||
foreach (AdvTextHistoryRecord record in history.Records)
|
||||
{
|
||||
writer.Write(record.Layout.Slot);
|
||||
writer.Write(record.Layout.OriginX);
|
||||
writer.Write(record.Layout.OriginY);
|
||||
writer.Write(record.Layout.Width);
|
||||
writer.Write(record.Layout.Height);
|
||||
writer.Write(unchecked((int)record.Value));
|
||||
writer.Write(unchecked((int)record.AuxValue));
|
||||
writer.Write(record.Style.PrimaryFontSize);
|
||||
writer.Write(unchecked((int)record.Style.TextColor));
|
||||
writer.Write(record.Layout.CursorY);
|
||||
writer.Write(unchecked((int)record.Flags));
|
||||
}
|
||||
|
||||
using var strings = new MemoryStream();
|
||||
foreach (AdvTextHistoryRecord record in history.Records)
|
||||
{
|
||||
strings.Write(NativeEncoding.GetBytes(record.Text ?? ""));
|
||||
strings.WriteByte(0);
|
||||
}
|
||||
while ((strings.Length & 3) != 0) strings.WriteByte(0);
|
||||
byte[] blob = strings.ToArray();
|
||||
writer.Write(blob.Length / 4);
|
||||
for (int i = 0; i < blob.Length; i += 4)
|
||||
writer.Write(~BinaryPrimitives.ReadUInt32LittleEndian(blob.AsSpan(i, 4)));
|
||||
return stream.ToArray();
|
||||
}
|
||||
|
||||
private static void DecodeLogical(ReadOnlySpan<byte> source, AdvTextHistory history)
|
||||
{
|
||||
int at = 0;
|
||||
int entryCount = ReadCount(source, ref at, "entry");
|
||||
var entries = new AdvTextHistoryEntry[entryCount];
|
||||
for (int i = 0; i < entryCount; i++)
|
||||
entries[i] = new AdvTextHistoryEntry(ReadNext(source, ref at), ReadNext(source, ref at));
|
||||
|
||||
int recordCount = ReadCount(source, ref at, "record");
|
||||
var raw = new int[recordCount, RecordDwords];
|
||||
for (int i = 0; i < recordCount; i++)
|
||||
for (int dword = 0; dword < RecordDwords; dword++)
|
||||
raw[i, dword] = ReadNext(source, ref at);
|
||||
|
||||
int stringDwords = ReadCount(source, ref at, "string blob dword");
|
||||
int stringBytes = checked(stringDwords * 4);
|
||||
if (at > source.Length - stringBytes)
|
||||
throw new InvalidDataException("Native text-history string blob is truncated.");
|
||||
byte[] blob = source.Slice(at, stringBytes).ToArray();
|
||||
for (int i = 0; i < blob.Length; i += 4)
|
||||
{
|
||||
uint value = ~BinaryPrimitives.ReadUInt32LittleEndian(blob.AsSpan(i, 4));
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(blob.AsSpan(i, 4), value);
|
||||
}
|
||||
string[] texts = DecodeStrings(blob, recordCount);
|
||||
var records = new AdvTextHistoryRecord[recordCount];
|
||||
for (int i = 0; i < recordCount; i++)
|
||||
{
|
||||
var flags = unchecked((AdvTextHistoryRecordFlags)(uint)raw[i, 10]);
|
||||
AdvTextHistoryRecordKind kind =
|
||||
flags.HasFlag(AdvTextHistoryRecordFlags.VoicePair) ? AdvTextHistoryRecordKind.Voice :
|
||||
flags.HasFlag(AdvTextHistoryRecordFlags.TypedMetadata) ? AdvTextHistoryRecordKind.Metadata :
|
||||
AdvTextHistoryRecordKind.Text;
|
||||
var layout = new AdvTextLayoutSnapshot(
|
||||
raw[i, 0], raw[i, 3], raw[i, 4], raw[i, 1], raw[i, 2],
|
||||
0, raw[i, 9], raw[i, 3], raw[i, 4]);
|
||||
var style = AdvTextStyle.Default with
|
||||
{
|
||||
PrimaryFontSize = raw[i, 7],
|
||||
TextColor = unchecked((uint)raw[i, 8]),
|
||||
};
|
||||
records[i] = new AdvTextHistoryRecord(
|
||||
kind, flags, layout, style, texts[i], raw[i, 5], raw[i, 6], -1);
|
||||
}
|
||||
history.RestorePersistenceSnapshot(entries, records);
|
||||
}
|
||||
|
||||
private static string[] DecodeStrings(ReadOnlySpan<byte> blob, int count)
|
||||
{
|
||||
var result = new string[count];
|
||||
int at = 0;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
if (at >= blob.Length)
|
||||
throw new InvalidDataException("Native text-history string blob ended early.");
|
||||
int end = blob[at..].IndexOf((byte)0);
|
||||
if (end < 0) throw new InvalidDataException("Native text-history string blob ended early.");
|
||||
result[i] = NativeEncoding.GetString(blob.Slice(at, end));
|
||||
at += end + 1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static int ReadCount(ReadOnlySpan<byte> source, ref int at, string name)
|
||||
{
|
||||
int value = ReadNext(source, ref at);
|
||||
if (value < 0) throw new InvalidDataException($"Native text-history {name} count is negative.");
|
||||
return value;
|
||||
}
|
||||
|
||||
private static int ReadNext(ReadOnlySpan<byte> source, ref int at)
|
||||
{
|
||||
if (at > source.Length - 4) throw new InvalidDataException("Native text-history logical data is truncated.");
|
||||
int value = BinaryPrimitives.ReadInt32LittleEndian(source.Slice(at, 4));
|
||||
at += 4;
|
||||
return value;
|
||||
}
|
||||
|
||||
private static int ReadNonNegative(ReadOnlySpan<byte> source, int offset, string name)
|
||||
{
|
||||
int value = BinaryPrimitives.ReadInt32LittleEndian(source.Slice(offset, 4));
|
||||
if (value < 0) throw new InvalidDataException($"Native text-history {name} is negative.");
|
||||
return value;
|
||||
}
|
||||
|
||||
private static void WriteInt(Span<byte> destination, int offset, int value)
|
||||
=> BinaryPrimitives.WriteInt32LittleEndian(destination.Slice(offset, 4), value);
|
||||
|
||||
private static Encoding CreateNativeEncoding()
|
||||
{
|
||||
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
||||
return Encoding.GetEncoding(932);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user