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 Objects, long RangeFirst, int RangeCount, byte[] RangeRecord) Capture(GfxState gfx) { GfxPersistenceSnapshot snapshot = gfx.CapturePersistenceSnapshot(); byte[] surfaces = new byte[NativeNumberedSaveState.SurfaceRecordsSize]; for (int slot = 0; slot < SurfaceCount; slot++) WriteInt(surfaces, slot * SurfaceRecordSize, -1); foreach (GfxSurfacePersistenceState surface in snapshot.Surfaces) { if ((uint)surface.Slot >= SurfaceCount) continue; int at = surface.Slot * SurfaceRecordSize; WriteInt(surfaces, at, surface.Created ? -1 : unchecked((int)surface.ResourceId)); WriteInt(surfaces, at + 4, PackNativeColorKey(surface.ColorKey)); WriteInt(surfaces, at + 8, surface.ReloadOnRestore ? 1 : 0); WriteInt(surfaces, at + 0x10, surface.Created ? 1 : 0); } 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(); for (int slot = 0; slot < SurfaceCount; slot++) { int at = slot * SurfaceRecordSize; int resourceId = ReadInt(state.SurfaceRecords, at); bool reload = ReadInt(state.SurfaceRecords, at + 8) == 1; bool created = ReadInt(state.SurfaceRecords, at + 0x10) == 1; if (resourceId >= 0 || reload || created) surfaces.Add(new GfxSurfacePersistenceState( slot, resourceId < 0 ? resourceId : unchecked((uint)resourceId), UnpackNativeColorKey(ReadInt(state.SurfaceRecords, at + 4)), created, reload)); } var objects = state.GfxObjects .Select(item => (item.Handle, DecodeObject(item.Record, state.LegacyTightGfxLayout))) .ToArray(); return new GfxPersistenceSnapshot( surfaces, objects, state.RangeTransformFirst, state.RangeTransformCount, DecodeObject(state.RangeTransformRecord, state.LegacyTightGfxLayout)); } private static byte[] EncodeObject(GfxState.GfxObject value) { byte[] raw = value.NativePersistenceRecord is { Length: NativeNumberedSaveState.GfxRecordSize } ? value.NativePersistenceRecord.ToArray() : CreateDefaultObjectRecord(); int flags = ReadInt(raw, 0); flags = value.Visible ? flags | 1 : flags & ~1; flags = value.OneShotColorEnabled || value.ScaleEnabled || value.RotationChannelEnabled || value.TranslationEnabled ? flags | 2 : flags & ~2; flags = value.RotationEnabled || value.SrcAnim || value.ColorAnim ? flags | 4 : 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, EncodeNativeStart(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); WriteRotationMatrix(raw, 0xec, value.RotationCurrent); WriteRotationMatrix(raw, 0x12c, value.RotationTarget); WriteTranslationMatrix(raw, 0x16c, value.TranslationCurrent); WriteTranslationMatrix(raw, 0x1ac, value.TranslationTarget); WriteFloat(raw, 0x1ec, value.RotationCurrent.X); WriteFloat(raw, 0x1f0, value.RotationCurrent.Y); WriteFloat(raw, 0x1f4, value.RotationCurrent.Z); WriteFloat(raw, 0x1f8, value.RotationTarget.X); WriteFloat(raw, 0x1fc, value.RotationTarget.Y); WriteFloat(raw, 0x200, value.RotationTarget.Z); WriteFloat(raw, 0x204, value.RotationCurrent.Angle); WriteFloat(raw, 0x208, value.RotationTarget.Angle); WriteInt(raw, 0x20c, EncodeNativeStart(value.ColorStart)); WriteInt(raw, 0x214, EncodeNativeStart(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, value.SrcAnim ? unchecked((int)value.SrcFrameCount) : 0); WriteInt(raw, 0x23c, value.SrcAnim ? unchecked((int)value.SrcColumns) : 0); if (value.ColorAnim) 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 raw, bool legacyTightLayout) { 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 { NativePersistenceRecord = legacyTightLayout ? CreateDefaultObjectRecord() : raw.ToArray(), 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 = DecodeNativeStart(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 = ReadMatrixTranslation(raw, 0x16c), V16c = ReadLongMatrixTranslation(raw, 0x16c), TranslationTarget = ReadMatrixTranslation(raw, 0x1ac), RotationCurrent = ( ReadFloat(raw, 0x1ec), ReadFloat(raw, 0x1f0), ReadFloat(raw, 0x1f4), ReadFloat(raw, 0x204)), RotationTarget = ( ReadFloat(raw, 0x1f8), ReadFloat(raw, 0x1fc), ReadFloat(raw, 0x200), ReadFloat(raw, 0x208)), ColorStart = DecodeNativeStart(ReadInt(raw, 0x20c)), RotationStartMs = DecodeNativeStart(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 raw, int offset, (double X, double Y, double Z) scale) { ClearMatrix(raw, offset); 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 raw, int offset) => (ReadFloat(raw, offset), ReadFloat(raw, offset + 0x14), ReadFloat(raw, offset + 0x28)); private static void WriteRotationMatrix( Span raw, int offset, (double X, double Y, double Z, double Angle) rotation) { ClearMatrix(raw, offset); double length = Math.Sqrt( rotation.X * rotation.X + rotation.Y * rotation.Y + rotation.Z * rotation.Z); if (length <= double.Epsilon || rotation.Angle == 0) { WriteFloat(raw, offset, 1); WriteFloat(raw, offset + 0x14, 1); WriteFloat(raw, offset + 0x28, 1); WriteFloat(raw, offset + 0x3c, 1); return; } double x = rotation.X / length; double y = rotation.Y / length; double z = rotation.Z / length; double radians = rotation.Angle * Math.PI / 180.0; double cosine = Math.Cos(radians); double sine = Math.Sin(radians); double complement = 1.0 - cosine; WriteFloat(raw, offset, x * x * complement + cosine); WriteFloat(raw, offset + 0x04, x * y * complement + z * sine); WriteFloat(raw, offset + 0x08, x * z * complement - y * sine); WriteFloat(raw, offset + 0x10, x * y * complement - z * sine); WriteFloat(raw, offset + 0x14, y * y * complement + cosine); WriteFloat(raw, offset + 0x18, x * sine + y * z * complement); WriteFloat(raw, offset + 0x20, y * sine + x * z * complement); WriteFloat(raw, offset + 0x24, y * z * complement - x * sine); WriteFloat(raw, offset + 0x28, z * z * complement + cosine); WriteFloat(raw, offset + 0x3c, 1); } private static void WriteVector(Span 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 raw, int offset) => (ReadInt(raw, offset), ReadInt(raw, offset + 4), ReadInt(raw, offset + 8)); private static void WriteTranslationMatrix( Span raw, int offset, (double X, double Y, double Z) translation) { ClearMatrix(raw, offset); WriteFloat(raw, offset, 1); WriteFloat(raw, offset + 0x14, 1); WriteFloat(raw, offset + 0x28, 1); WriteFloat(raw, offset + 0x30, translation.X); WriteFloat(raw, offset + 0x34, translation.Y); WriteFloat(raw, offset + 0x38, translation.Z); WriteFloat(raw, offset + 0x3c, 1); } private static (long X, long Y, long Z) ReadLongMatrixTranslation( ReadOnlySpan raw, int offset) => ((long)ReadFloat(raw, offset + 0x30), (long)ReadFloat(raw, offset + 0x34), (long)ReadFloat(raw, offset + 0x38)); private static (double X, double Y, double Z) ReadMatrixTranslation( ReadOnlySpan raw, int offset) => (ReadFloat(raw, offset + 0x30), ReadFloat(raw, offset + 0x34), ReadFloat(raw, offset + 0x38)); private static void ClearMatrix(Span raw, int offset) => raw.Slice(offset, 0x40).Clear(); private static int EncodeNativeStart(long value) => value < 0 ? 0 : unchecked((int)value); private static long DecodeNativeStart(int value) => value == 0 ? -1 : value; private static byte[] CreateDefaultObjectRecord() { byte[] raw = new byte[NativeNumberedSaveState.GfxRecordSize]; WriteInt(raw, 0x60, -1); WriteInt(raw, 0x64, -1); foreach (int matrixOffset in new[] { 0x6c, 0xac, 0xec, 0x12c, 0x16c, 0x1ac }) { WriteFloat(raw, matrixOffset, 1); WriteFloat(raw, matrixOffset + 0x14, 1); WriteFloat(raw, matrixOffset + 0x28, 1); WriteFloat(raw, matrixOffset + 0x3c, 1); } WriteInt(raw, 0x240, -1); return raw; } private static void WriteFloat(Span raw, int offset, double value) => WriteInt(raw, offset, BitConverter.SingleToInt32Bits((float)value)); private static float ReadFloat(ReadOnlySpan raw, int offset) => BitConverter.Int32BitsToSingle(ReadInt(raw, offset)); private static int ReadInt(ReadOnlySpan raw, int offset) => BinaryPrimitives.ReadInt32LittleEndian(raw.Slice(offset, 4)); private static void WriteInt(Span raw, int offset, int value) => BinaryPrimitives.WriteInt32LittleEndian(raw.Slice(offset, 4), value); }