using System.Buffers.Binary;
using System.Text;
using Age.Engine.Model;
using Age.Engine.Sys4;
namespace Age.Engine.Persistence;
/// The non-container LZSS tail appended to a layout-3 numbered save.
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 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 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 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 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 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 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 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 destination, int offset, int value)
=> BinaryPrimitives.WriteInt32LittleEndian(destination.Slice(offset, 4), value);
private static Encoding CreateNativeEncoding()
{
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
return Encoding.GetEncoding(932);
}
}