diff --git a/DALib/Drawing/EpfFile.cs b/DALib/Drawing/EpfFile.cs index 9ec801c..11bb6ed 100644 --- a/DALib/Drawing/EpfFile.cs +++ b/DALib/Drawing/EpfFile.cs @@ -84,14 +84,21 @@ private EpfFile(Stream stream) var startAddress = reader.ReadInt32(); var endAddress = reader.ReadInt32(); - segment.Seek(startAddress, SeekOrigin.Begin); - - var data = (endAddress - startAddress) == (width * height) - ? reader.ReadBytes(endAddress - startAddress) - : reader.ReadBytes(tocAddress - startAddress); + //empty frames (width==0 || height==0) are preserved with an empty Data array so that + //direct-index access by animation-frame index stays stable. Callers should check + //PixelWidth/PixelHeight before rendering. + byte[] data; if ((width == 0) || (height == 0)) - continue; + data = []; + else + { + segment.Seek(startAddress, SeekOrigin.Begin); + + data = (endAddress - startAddress) == (width * height) + ? reader.ReadBytes(endAddress - startAddress) + : reader.ReadBytes(tocAddress - startAddress); + } Add( new EpfFrame diff --git a/DALib/Drawing/Graphics.cs b/DALib/Drawing/Graphics.cs index a2ab88b..1fc4104 100644 --- a/DALib/Drawing/Graphics.cs +++ b/DALib/Drawing/Graphics.cs @@ -434,7 +434,19 @@ public static SKImage RenderDarknessOverlay(HeaFile hea, byte darknessOpacity = /// Alpha blending type. Defaults to Premul. Should be set to Unpremul for palettes >= 1000 /// public static SKImage RenderImage(EpfFrame frame, Palette palette, SKAlphaType alphaType = SKAlphaType.Premul) - => SimpleRender( + { + //empty-frame marker (PixelWidth==0 || PixelHeight==0): return a 1x1 transparent image so + //callers that iterate all frames of an EPF don't crash on SKBitmap(0,0). Equipment + //renderers should short-circuit on PixelWidth/PixelHeight before reaching here. + if ((frame.PixelWidth <= 0) || (frame.PixelHeight <= 0)) + { + using var emptyBitmap = new SKBitmap(1, 1, SKColorType.Bgra8888, alphaType); + emptyBitmap.Erase(CONSTANTS.Transparent); + + return SKImage.FromBitmap(emptyBitmap); + } + + return SimpleRender( frame.Left, frame.Top, frame.PixelWidth, @@ -442,6 +454,7 @@ public static SKImage RenderImage(EpfFrame frame, Palette palette, SKAlphaType a frame.Data, palette, alphaType); + } /// /// Renders an MpfFrame diff --git a/DALib/Drawing/SpfFile.cs b/DALib/Drawing/SpfFile.cs index bb1853b..7517c7a 100644 --- a/DALib/Drawing/SpfFile.cs +++ b/DALib/Drawing/SpfFile.cs @@ -104,8 +104,9 @@ private void ReadColorized(BinaryReader reader) var top = reader.ReadUInt16(); var right = reader.ReadUInt16(); var bottom = reader.ReadUInt16(); - _ = reader.ReadUInt32(); - var reserved = reader.ReadUInt32(); + var centerX = reader.ReadInt16(); + var centerY = reader.ReadInt16(); + var flags = reader.ReadUInt32(); var startAddress = reader.ReadUInt32(); var byteWidth = reader.ReadUInt32(); var byteCount = reader.ReadUInt32(); @@ -118,7 +119,9 @@ private void ReadColorized(BinaryReader reader) Top = top, Right = right, Bottom = bottom, - Unknown2 = reserved, + CenterY = centerY, + CenterX = centerX, + HasCenterPoint = (flags & 1) != 0, StartAddress = startAddress, ByteWidth = byteWidth, ByteCount = byteCount, @@ -164,8 +167,9 @@ private void ReadPalettized(BinaryReader reader) var top = reader.ReadUInt16(); var right = reader.ReadUInt16(); var bottom = reader.ReadUInt16(); - _ = reader.ReadUInt32(); - var unknown2 = reader.ReadUInt32(); + var centerX = reader.ReadInt16(); + var centerY = reader.ReadInt16(); + var flags = reader.ReadUInt32(); var startAddress = reader.ReadUInt32(); var byteWidth = reader.ReadUInt32(); var byteCount = reader.ReadUInt32(); @@ -178,7 +182,9 @@ private void ReadPalettized(BinaryReader reader) Top = top, Right = right, Bottom = bottom, - Unknown2 = unknown2, + CenterY = centerY, + CenterX = centerX, + HasCenterPoint = (flags & 1) != 0, StartAddress = startAddress, ByteWidth = byteWidth, ByteCount = byteCount, @@ -255,8 +261,9 @@ private void SaveColorized(BinaryWriter writer) writer.Write(frame.Top); writer.Write(frame.Right); writer.Write(frame.Bottom); - writer.Write(SpfFrame.Unknown1); - writer.Write(frame.Unknown2); + writer.Write(frame.CenterX); + writer.Write(frame.CenterY); + writer.Write(frame.HasCenterPoint ? 1u : 0u); writer.Write(frame.StartAddress); writer.Write(frame.ByteWidth); writer.Write(frame.ByteCount); @@ -314,8 +321,9 @@ private void SavePalettized(BinaryWriter writer) writer.Write(frame.Top); writer.Write(frame.Right); writer.Write(frame.Bottom); - writer.Write(SpfFrame.Unknown1); - writer.Write(frame.Unknown2); + writer.Write(frame.CenterX); + writer.Write(frame.CenterY); + writer.Write(frame.HasCenterPoint ? 1u : 0u); writer.Write(frame.StartAddress); writer.Write(frame.ByteWidth); writer.Write(frame.ByteCount); @@ -365,7 +373,8 @@ public static SpfFile FromImages(params SKImage[] orderedFrames) Top = 0, Right = (ushort)image.Width, Bottom = (ushort)image.Height, - Unknown2 = 0, + CenterX = unchecked((short)0xCCCC), + CenterY = unchecked((short)0xCCCC), StartAddress = 0, ByteWidth = (uint)image.Width * 2, ByteCount = (uint)(image.Width * image.Height * 4), //2 bytes per pixel, 2 copies of image @@ -429,7 +438,8 @@ public static SpfFile FromImages(QuantizerOptions options, params SKImage[] orde Top = 0, Right = (ushort)image.Width, Bottom = (ushort)image.Height, - Unknown2 = 0, + CenterX = unchecked((short)0xCCCC), + CenterY = unchecked((short)0xCCCC), StartAddress = 0, ByteWidth = (uint)image.Width, ByteCount = (uint)image.Width * (uint)image.Height, diff --git a/DALib/Drawing/SpfFrame.cs b/DALib/Drawing/SpfFrame.cs index ac18bdc..93afa2a 100644 --- a/DALib/Drawing/SpfFrame.cs +++ b/DALib/Drawing/SpfFrame.cs @@ -32,6 +32,18 @@ public sealed class SpfFrame /// public uint ByteWidth { get; set; } + /// + /// The X coordinate of the anchor point in canvas space. Used for alignment when rendering (e.g. projectile sprites). + /// Only valid when is true. Stored at TOC+0x08 in the file. + /// + public short CenterX { get; set; } + + /// + /// The Y coordinate of the anchor point in canvas space. Used for alignment when rendering (e.g. projectile sprites). + /// Only valid when is true. Stored at TOC+0x0A in the file. + /// + public short CenterY { get; set; } + /// /// If colorized, the colorized pixel data of the frame (the RGB565 data scaled to RGB888) /// @@ -42,6 +54,11 @@ public sealed class SpfFrame /// public byte[]? Data { get; set; } + /// + /// Whether this frame has valid center point data in and . + /// + public bool HasCenterPoint { get; set; } + /// /// The number of byte per image /// @@ -72,11 +89,6 @@ public sealed class SpfFrame /// public ushort Top { get; set; } - /// - /// A value that has an unknown use LI: figure out what this is for - /// - public uint Unknown2 { get; set; } - /// /// The pixel height of the frame /// @@ -86,9 +98,4 @@ public sealed class SpfFrame /// The pixel width of the frame /// public int PixelWidth => Right - Left; - - /// - /// A value that has an unknown use LI: figure out what this is for - /// - public static uint Unknown1 => 0xCCCCCCCC; // Every SPF has this value associated with it } \ No newline at end of file diff --git a/DALib/Drawing/Virtualized/EpfView.cs b/DALib/Drawing/Virtualized/EpfView.cs index 9d3e0f6..e2d8fdc 100644 --- a/DALib/Drawing/Virtualized/EpfView.cs +++ b/DALib/Drawing/Virtualized/EpfView.cs @@ -101,12 +101,10 @@ public static EpfView FromEntry(DataArchiveEntry entry) var startAddress = reader.ReadInt32(); var endAddress = reader.ReadInt32(); - var width = right - left; - var height = bottom - top; - - if ((width == 0) || (height == 0)) - continue; - + //empty frames (width==0 || height==0) are preserved in the TOC so that direct-index + //access by animation-frame index stays stable. Weapons/equipment use 0x0 frames as a + //"no visual on this pose" marker; dropping them would shift all subsequent indices and + //either mis-render or mask later frames. tocEntries.Add( new TocEntry( top, @@ -137,14 +135,26 @@ public EpfFrame this[int index] var toc = Toc[index]; + var width = toc.Right - toc.Left; + var height = toc.Bottom - toc.Top; + + //empty-frame marker: preserve the TOC entry but return an empty Data array — callers + //should check PixelWidth/PixelHeight before rendering. + if ((width == 0) || (height == 0)) + return new EpfFrame + { + Top = toc.Top, + Left = toc.Left, + Bottom = toc.Bottom, + Right = toc.Right, + Data = [] + }; + using var stream = Entry.ToStreamSegment(); using var reader = new BinaryReader(stream, Encoding.Default, true); stream.Seek(HEADER_LENGTH + toc.StartAddress, SeekOrigin.Begin); - var width = toc.Right - toc.Left; - var height = toc.Bottom - toc.Top; - var data = (toc.EndAddress - toc.StartAddress) == (width * height) ? reader.ReadBytes(toc.EndAddress - toc.StartAddress) : reader.ReadBytes(TocAddress - toc.StartAddress); diff --git a/DALib/Drawing/Virtualized/SpfView.cs b/DALib/Drawing/Virtualized/SpfView.cs index 52a0c97..5405713 100644 --- a/DALib/Drawing/Virtualized/SpfView.cs +++ b/DALib/Drawing/Virtualized/SpfView.cs @@ -115,8 +115,9 @@ public static SpfView FromEntry(DataArchiveEntry entry) reader.ReadUInt16(), reader.ReadUInt16(), reader.ReadUInt16(), - reader.ReadUInt32(), - reader.ReadUInt32(), + reader.ReadInt16(), + reader.ReadInt16(), + (reader.ReadUInt32() & 1) != 0, reader.ReadUInt32(), reader.ReadUInt32(), reader.ReadUInt32(), @@ -158,7 +159,9 @@ public SpfFrame this[int index] Top = toc.Top, Right = toc.Right, Bottom = toc.Bottom, - Unknown2 = toc.Unknown2, + CenterX = toc.CenterX, + CenterY = toc.CenterY, + HasCenterPoint = toc.HasCenterPoint, StartAddress = toc.StartAddress, ByteWidth = toc.ByteWidth, ByteCount = toc.ByteCount, @@ -208,10 +211,9 @@ private readonly record struct SpfTocEntry( ushort Top, ushort Right, ushort Bottom, - - // ReSharper disable once NotAccessedPositionalProperty.Local - uint Unknown1, - uint Unknown2, + short CenterX, + short CenterY, + bool HasCenterPoint, uint StartAddress, uint ByteWidth, uint ByteCount, diff --git a/DALib/Extensions/SKColorExtensions.cs b/DALib/Extensions/SKColorExtensions.cs index 18fafe8..1a43740 100644 --- a/DALib/Extensions/SKColorExtensions.cs +++ b/DALib/Extensions/SKColorExtensions.cs @@ -89,6 +89,11 @@ public static float GetLuminance(this SKColor color, float coefficient = 1.0f) return (byte)Math.Clamp(MathF.Round(lumSrgb * 255f * coefficient), 0, 255); } + /// + /// Generates a random vivid color with high saturation and brightness values. + /// + /// The random number generator to use. If null, uses Random.Shared. + /// A random SKColor with saturation and value between 80-100% in the HSV color space. public static SKColor GetRandomVividColor(Random? random = null) { random ??= Random.Shared; diff --git a/DALib/IO/Compression.cs b/DALib/IO/Compression.cs index 410a796..6fb0228 100644 --- a/DALib/IO/Compression.cs +++ b/DALib/IO/Compression.cs @@ -95,7 +95,19 @@ public static void DecompressHpf(ref Span buffer) buffer = rawBytes[..m]; } - + + /// + /// Compresses data using HPF compression algorithm. + /// + /// + /// The buffer containing the data to compress. + /// + /// + /// A byte array containing the compressed data with HPF header. + /// + /// + /// Thrown when a node in the compression tree cannot be reached during encoding. + /// public static byte[] CompressHpf(Span buffer) { Span intOdd = stackalloc uint[256]; @@ -112,14 +124,13 @@ public static byte[] CompressHpf(Span buffer) var bits = new List(buffer.Length * 8); - for (int byteIndex = 0; byteIndex <= buffer.Length; byteIndex++) + for (var byteIndex = 0; byteIndex <= buffer.Length; byteIndex++) { - uint symbol = byteIndex < buffer.Length ? buffer[byteIndex] : 0x100u; - uint targetNode = symbol + 0x100; + var symbol = byteIndex < buffer.Length ? buffer[byteIndex] : 0x100u; + var targetNode = symbol + 0x100; uint currentNode = 0; while (currentNode != targetNode) - { if (IsNodeInSubtree(targetNode, intOdd[(int)currentNode], intOdd, intEven)) { bits.Add(false); @@ -132,26 +143,23 @@ public static byte[] CompressHpf(Span buffer) } else throw new InvalidDataException($"Cannot reach node {targetNode} from {currentNode}"); - } - uint val = targetNode; - uint val3 = val; + var val = targetNode; + var val3 = val; uint val2 = bytePair[(int)val]; while ((val3 != 0) && (val2 != 0)) { - byte idx = bytePair[(int)val2]; - uint j = intOdd[(int)idx]; + var idx = bytePair[(int)val2]; + var j = intOdd[idx]; if (j == val2) { - j = intEven[(int)idx]; - intEven[(int)idx] = val3; + j = intEven[idx]; + intEven[idx] = val3; } else - { - intOdd[(int)idx] = val3; - } + intOdd[idx] = val3; if (intOdd[(int)val2] == val3) intOdd[(int)val2] = j; @@ -169,14 +177,12 @@ public static byte[] CompressHpf(Span buffer) var compressedData = new byte[compressedSize]; for (var i = 0; i < bits.Count; i++) - { if (bits[i]) { - int byteIdx = i / 8; - int bitIdx = i % 8; + var byteIdx = i / 8; + var bitIdx = i % 8; compressedData[byteIdx] |= (byte)(1 << bitIdx); } - } var output = new byte[4 + compressedSize]; output[0] = 0x55; @@ -192,6 +198,7 @@ private static bool IsNodeInSubtree(uint target, uint root, Span intOdd, S { if (root == target) return true; if (root > 0xFF) return false; + return IsNodeInSubtree(target, intOdd[(int)root], intOdd, intEven) || IsNodeInSubtree(target, intEven[(int)root], intOdd, intEven); } diff --git a/DALib/Utility/ImageProcessor.cs b/DALib/Utility/ImageProcessor.cs index fdac689..bce9c8c 100644 --- a/DALib/Utility/ImageProcessor.cs +++ b/DALib/Utility/ImageProcessor.cs @@ -258,7 +258,6 @@ public static SKImage QuantizeToPalette(SKImage image, Palette palette, IDithere } } else - { for (var y = 0; y < image.Height; y++) { for (var x = 0; x < image.Width; x++) @@ -269,7 +268,6 @@ public static SKImage QuantizeToPalette(SKImage image, Palette palette, IDithere .ToSKColor(); } } - } return SKImage.FromBitmap(quantizedBitmap); } diff --git a/DALib/Utility/MapImageCache.cs b/DALib/Utility/MapImageCache.cs index 6d5e267..ff89ebc 100644 --- a/DALib/Utility/MapImageCache.cs +++ b/DALib/Utility/MapImageCache.cs @@ -36,9 +36,6 @@ public MapImageCache() /// /// The left foreground cache /// - /// - /// The right foreground cache - /// public MapImageCache(SKImageCache bgCache, SKImageCache fgCache) { BackgroundCache = bgCache;