Implement native layout-3 save restoration
This commit is contained in:
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