Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 13 additions & 6 deletions DALib/Drawing/EpfFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 14 additions & 1 deletion DALib/Drawing/Graphics.cs
Original file line number Diff line number Diff line change
Expand Up @@ -434,14 +434,27 @@ public static SKImage RenderDarknessOverlay(HeaFile hea, byte darknessOpacity =
/// Alpha blending type. Defaults to Premul. Should be set to Unpremul for palettes >= 1000
/// </param>
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,
frame.PixelHeight,
frame.Data,
palette,
alphaType);
}

/// <summary>
/// Renders an MpfFrame
Expand Down
34 changes: 22 additions & 12 deletions DALib/Drawing/SpfFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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,
Expand Down Expand Up @@ -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();
Expand All @@ -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,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
27 changes: 17 additions & 10 deletions DALib/Drawing/SpfFrame.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,18 @@ public sealed class SpfFrame
/// </remarks>
public uint ByteWidth { get; set; }

/// <summary>
/// The X coordinate of the anchor point in canvas space. Used for alignment when rendering (e.g. projectile sprites).
/// Only valid when <see cref="HasCenterPoint" /> is true. Stored at TOC+0x08 in the file.
/// </summary>
public short CenterX { get; set; }

/// <summary>
/// The Y coordinate of the anchor point in canvas space. Used for alignment when rendering (e.g. projectile sprites).
/// Only valid when <see cref="HasCenterPoint" /> is true. Stored at TOC+0x0A in the file.
/// </summary>
public short CenterY { get; set; }

/// <summary>
/// If colorized, the colorized pixel data of the frame (the RGB565 data scaled to RGB888)
/// </summary>
Expand All @@ -42,6 +54,11 @@ public sealed class SpfFrame
/// </summary>
public byte[]? Data { get; set; }

/// <summary>
/// Whether this frame has valid center point data in <see cref="CenterX" /> and <see cref="CenterY" />.
/// </summary>
public bool HasCenterPoint { get; set; }

/// <summary>
/// The number of byte per image
/// </summary>
Expand Down Expand Up @@ -72,11 +89,6 @@ public sealed class SpfFrame
/// </summary>
public ushort Top { get; set; }

/// <summary>
/// A value that has an unknown use LI: figure out what this is for
/// </summary>
public uint Unknown2 { get; set; }

/// <summary>
/// The pixel height of the frame
/// </summary>
Expand All @@ -86,9 +98,4 @@ public sealed class SpfFrame
/// The pixel width of the frame
/// </summary>
public int PixelWidth => Right - Left;

/// <summary>
/// A value that has an unknown use LI: figure out what this is for
/// </summary>
public static uint Unknown1 => 0xCCCCCCCC; // Every SPF has this value associated with it
}
28 changes: 19 additions & 9 deletions DALib/Drawing/Virtualized/EpfView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand Down
16 changes: 9 additions & 7 deletions DALib/Drawing/Virtualized/SpfView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
5 changes: 5 additions & 0 deletions DALib/Extensions/SKColorExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/// <summary>
/// Generates a random vivid color with high saturation and brightness values.
/// </summary>
/// <param name="random">The random number generator to use. If null, uses Random.Shared.</param>
/// <return>A random SKColor with saturation and value between 80-100% in the HSV color space.</return>
public static SKColor GetRandomVividColor(Random? random = null)
{
random ??= Random.Shared;
Expand Down
45 changes: 26 additions & 19 deletions DALib/IO/Compression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,19 @@ public static void DecompressHpf(ref Span<byte> buffer)

buffer = rawBytes[..m];
}


/// <summary>
/// Compresses data using HPF compression algorithm.
/// </summary>
/// <param name="buffer">
/// The buffer containing the data to compress.
/// </param>
/// <returns>
/// A byte array containing the compressed data with HPF header.
/// </returns>
/// <exception cref="InvalidDataException">
/// Thrown when a node in the compression tree cannot be reached during encoding.
/// </exception>
public static byte[] CompressHpf(Span<byte> buffer)
{
Span<uint> intOdd = stackalloc uint[256];
Expand All @@ -112,14 +124,13 @@ public static byte[] CompressHpf(Span<byte> buffer)

var bits = new List<bool>(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);
Expand All @@ -132,26 +143,23 @@ public static byte[] CompressHpf(Span<byte> 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;
Expand All @@ -169,14 +177,12 @@ public static byte[] CompressHpf(Span<byte> 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;
Expand All @@ -192,6 +198,7 @@ private static bool IsNodeInSubtree(uint target, uint root, Span<uint> 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);
}
Expand Down
Loading
Loading