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;