From ba8b172a6b310e75f85fbbe3036a48ff611c613f Mon Sep 17 00:00:00 2001 From: sichii Date: Tue, 31 Mar 2026 11:06:35 -0400 Subject: [PATCH 1/6] palettes with id >=1000 need to be rendered with a diff alpha blending --- DALib/Drawing/Graphics.cs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/DALib/Drawing/Graphics.cs b/DALib/Drawing/Graphics.cs index 9541fba..a2ab88b 100644 --- a/DALib/Drawing/Graphics.cs +++ b/DALib/Drawing/Graphics.cs @@ -430,14 +430,18 @@ public static SKImage RenderDarknessOverlay(HeaFile hea, byte darknessOpacity = /// /// A palette containing colors used by the frame /// - public static SKImage RenderImage(EpfFrame frame, Palette palette) + /// + /// 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( frame.Left, frame.Top, frame.PixelWidth, frame.PixelHeight, frame.Data, - palette); + palette, + alphaType); /// /// Renders an MpfFrame @@ -1115,7 +1119,8 @@ private static SKImage SimpleRender( int width, int height, byte[] data, - Palette palette) + Palette palette, + SKAlphaType alphaType = SKAlphaType.Premul) { //when left/top are negative, skip the padding and shift pixels to 0 var dstOffsetX = Math.Max(0, left); @@ -1123,7 +1128,11 @@ private static SKImage SimpleRender( var bitmapWidth = width + dstOffsetX; var bitmapHeight = height + dstOffsetY; - using var bitmap = new SKBitmap(bitmapWidth, bitmapHeight); + using var bitmap = new SKBitmap( + bitmapWidth, + bitmapHeight, + SKColorType.Bgra8888, + alphaType); using var pixMap = bitmap.PeekPixels(); var pixelBuffer = pixMap.GetPixelSpan(); From da1b8180194e83a7748b07df967f07ff6a6c2077 Mon Sep 17 00:00:00 2001 From: sichii Date: Wed, 1 Apr 2026 11:28:39 -0400 Subject: [PATCH 2/6] fix mapfile save --- DALib/Data/MapFile.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/DALib/Data/MapFile.cs b/DALib/Data/MapFile.cs index 89cc9f9..f6cd4c0 100644 --- a/DALib/Data/MapFile.cs +++ b/DALib/Data/MapFile.cs @@ -142,15 +142,15 @@ public sealed class MapTile /// The id of the background part of the tile. This id references a from a /// loaded from Seo.dat /// - public int Background { get; init; } + public short Background { get; init; } /// /// The id of the left foreground part of the tile. This id references an HPF image loaded from ia.dat /// - public int LeftForeground { get; set; } + public short LeftForeground { get; set; } /// /// The id of the right foreground part of the tile. This id references an HPF image loaded from ia.dat /// - public int RightForeground { get; set; } + public short RightForeground { get; set; } } \ No newline at end of file From f41b4eeb5c49ffab0500212cc60e241261ee0039 Mon Sep 17 00:00:00 2001 From: sichii Date: Wed, 1 Apr 2026 11:32:20 -0400 Subject: [PATCH 3/6] zzz --- DALib/Data/MapFile.cs | 12 ++++++------ DALib/Extensions/IntExtensions.cs | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/DALib/Data/MapFile.cs b/DALib/Data/MapFile.cs index f6cd4c0..f0b3634 100644 --- a/DALib/Data/MapFile.cs +++ b/DALib/Data/MapFile.cs @@ -39,9 +39,9 @@ private MapFile(Stream stream, int width, int height) for (var y = 0; y < Height; ++y) for (var x = 0; x < Width; ++x) { - var background = reader.ReadInt16(); - var leftForeground = reader.ReadInt16(); - var rightForeground = reader.ReadInt16(); + var background = reader.ReadUInt16(); + var leftForeground = reader.ReadUInt16(); + var rightForeground = reader.ReadUInt16(); Tiles[x, y] = new MapTile { @@ -142,15 +142,15 @@ public sealed class MapTile /// The id of the background part of the tile. This id references a from a /// loaded from Seo.dat /// - public short Background { get; init; } + public ushort Background { get; init; } /// /// The id of the left foreground part of the tile. This id references an HPF image loaded from ia.dat /// - public short LeftForeground { get; set; } + public ushort LeftForeground { get; set; } /// /// The id of the right foreground part of the tile. This id references an HPF image loaded from ia.dat /// - public short RightForeground { get; set; } + public ushort RightForeground { get; set; } } \ No newline at end of file diff --git a/DALib/Extensions/IntExtensions.cs b/DALib/Extensions/IntExtensions.cs index 60546ee..5bb95c5 100644 --- a/DALib/Extensions/IntExtensions.cs +++ b/DALib/Extensions/IntExtensions.cs @@ -25,5 +25,5 @@ public static class IntExtensions /// 0-12 and 10000-10012 are not rendered by the client... 20000-20012 are rendered but are kinda buggy if you get /// close to them /// - public static bool IsRenderedTileIndex(this int tileIndex) => (tileIndex > 10012) || ((tileIndex % 10000) > 12); + public static bool IsRenderedTileIndex(this ushort tileIndex) => (tileIndex > 10012) || ((tileIndex % 10000) > 12); } \ No newline at end of file From 6fc785379c6cca0808e2411d14bcd37b4617db66 Mon Sep 17 00:00:00 2001 From: sichii Date: Wed, 1 Apr 2026 14:55:20 -0400 Subject: [PATCH 4/6] mapfile fix again --- DALib/Data/MapFile.cs | 12 ++++++------ DALib/Extensions/IntExtensions.cs | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/DALib/Data/MapFile.cs b/DALib/Data/MapFile.cs index f0b3634..f6cd4c0 100644 --- a/DALib/Data/MapFile.cs +++ b/DALib/Data/MapFile.cs @@ -39,9 +39,9 @@ private MapFile(Stream stream, int width, int height) for (var y = 0; y < Height; ++y) for (var x = 0; x < Width; ++x) { - var background = reader.ReadUInt16(); - var leftForeground = reader.ReadUInt16(); - var rightForeground = reader.ReadUInt16(); + var background = reader.ReadInt16(); + var leftForeground = reader.ReadInt16(); + var rightForeground = reader.ReadInt16(); Tiles[x, y] = new MapTile { @@ -142,15 +142,15 @@ public sealed class MapTile /// The id of the background part of the tile. This id references a from a /// loaded from Seo.dat /// - public ushort Background { get; init; } + public short Background { get; init; } /// /// The id of the left foreground part of the tile. This id references an HPF image loaded from ia.dat /// - public ushort LeftForeground { get; set; } + public short LeftForeground { get; set; } /// /// The id of the right foreground part of the tile. This id references an HPF image loaded from ia.dat /// - public ushort RightForeground { get; set; } + public short RightForeground { get; set; } } \ No newline at end of file diff --git a/DALib/Extensions/IntExtensions.cs b/DALib/Extensions/IntExtensions.cs index 5bb95c5..d2a97d7 100644 --- a/DALib/Extensions/IntExtensions.cs +++ b/DALib/Extensions/IntExtensions.cs @@ -25,5 +25,5 @@ public static class IntExtensions /// 0-12 and 10000-10012 are not rendered by the client... 20000-20012 are rendered but are kinda buggy if you get /// close to them /// - public static bool IsRenderedTileIndex(this ushort tileIndex) => (tileIndex > 10012) || ((tileIndex % 10000) > 12); + public static bool IsRenderedTileIndex(this short tileIndex) => (tileIndex > 10012) || ((tileIndex % 10000) > 12); } \ No newline at end of file From 905cf6c2ebcce17985b5352d7665b7b27a02c950 Mon Sep 17 00:00:00 2001 From: sichii Date: Sat, 11 Apr 2026 19:36:02 -0400 Subject: [PATCH 5/6] misc --- DALib/Data/MapFile.cs | 2 +- DALib/Drawing/EpfFile.cs | 1 - DALib/Drawing/FntFile.cs | 1 - DALib/Drawing/HeaFile.cs | 4 ++-- DALib/Extensions/PalettizedExtensions.cs | 1 - DALib/Utility/ControlFileParser.cs | 2 -- DALib/Utility/SKImageCache.cs | 1 - 7 files changed, 3 insertions(+), 9 deletions(-) diff --git a/DALib/Data/MapFile.cs b/DALib/Data/MapFile.cs index f6cd4c0..75c82ee 100644 --- a/DALib/Data/MapFile.cs +++ b/DALib/Data/MapFile.cs @@ -31,7 +31,7 @@ public sealed class MapFile(int width, int height) : ISavable private MapFile(Stream stream, int width, int height) : this(width, height) { - if (stream.Length != width * height * 6) + if (stream.Length != (width * height * 6)) throw new InvalidDataException("Invalid map file"); using var reader = new BinaryReader(stream, Encoding.Default, true); diff --git a/DALib/Drawing/EpfFile.cs b/DALib/Drawing/EpfFile.cs index e00e1bb..9ec801c 100644 --- a/DALib/Drawing/EpfFile.cs +++ b/DALib/Drawing/EpfFile.cs @@ -4,7 +4,6 @@ using System.Collections.ObjectModel; using System.IO; using System.Linq; -using System.Runtime.InteropServices; using System.Text; using DALib.Abstractions; using DALib.Data; diff --git a/DALib/Drawing/FntFile.cs b/DALib/Drawing/FntFile.cs index 30733e2..f32d0ce 100644 --- a/DALib/Drawing/FntFile.cs +++ b/DALib/Drawing/FntFile.cs @@ -1,4 +1,3 @@ -using System; using System.IO; using DALib.Data; using DALib.Extensions; diff --git a/DALib/Drawing/HeaFile.cs b/DALib/Drawing/HeaFile.cs index 3f167b0..e837013 100644 --- a/DALib/Drawing/HeaFile.cs +++ b/DALib/Drawing/HeaFile.cs @@ -138,7 +138,7 @@ public int GetLayerWidth(int layerIndex) throw new ArgumentOutOfRangeException(nameof(layerIndex)); var start = Thresholds[layerIndex]; - var end = layerIndex < LayerCount - 1 ? Thresholds[layerIndex + 1] : ScanlineWidth; + var end = layerIndex < (LayerCount - 1) ? Thresholds[layerIndex + 1] : ScanlineWidth; return end - start; } @@ -193,7 +193,7 @@ public void DecodeScanline(int layerIndex, int scanlineIndex, Span buffer) var pixelIndex = 0; - for (var i = byteOffset; (i + 1 < RleData.Length) && (pixelIndex < layerWidth); i += 2) + for (var i = byteOffset; ((i + 1) < RleData.Length) && (pixelIndex < layerWidth); i += 2) { var value = RleData[i]; var count = RleData[i + 1]; diff --git a/DALib/Extensions/PalettizedExtensions.cs b/DALib/Extensions/PalettizedExtensions.cs index 858106a..32593dc 100644 --- a/DALib/Extensions/PalettizedExtensions.cs +++ b/DALib/Extensions/PalettizedExtensions.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Frozen; -using System.Diagnostics; using System.Linq; using DALib.Definitions; using DALib.Drawing; diff --git a/DALib/Utility/ControlFileParser.cs b/DALib/Utility/ControlFileParser.cs index 9d6dd09..c259c45 100644 --- a/DALib/Utility/ControlFileParser.cs +++ b/DALib/Utility/ControlFileParser.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.IO; -using System.Linq; using DALib.Definitions; using DALib.Drawing; using KGySoft.CoreLibraries; diff --git a/DALib/Utility/SKImageCache.cs b/DALib/Utility/SKImageCache.cs index bcf2161..5450b93 100644 --- a/DALib/Utility/SKImageCache.cs +++ b/DALib/Utility/SKImageCache.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; -using System.Runtime.InteropServices; using SkiaSharp; namespace DALib.Utility; From d6b1a52e5b5b805eaaa69c9ea24c0e81d93bb581 Mon Sep 17 00:00:00 2001 From: sichii Date: Sun, 12 Apr 2026 19:41:08 -0400 Subject: [PATCH 6/6] mpf stuff --- DALib/Definitions/Enums.cs | 22 +++++++ DALib/Drawing/MpfFile.cs | 92 +++++++++++++++++++++++++--- DALib/Drawing/Virtualized/MpfView.cs | 47 ++++++++++---- 3 files changed, 139 insertions(+), 22 deletions(-) diff --git a/DALib/Definitions/Enums.cs b/DALib/Definitions/Enums.cs index 5ea0272..e99910c 100644 --- a/DALib/Definitions/Enums.cs +++ b/DALib/Definitions/Enums.cs @@ -84,6 +84,28 @@ public enum MpfFormatType SingleAttack = 0 } +/// +/// Describes how a creature sprite's idle/standing animation plays back. +/// +public enum MpfIdleType +{ + /// + /// The creature shows a single unchanging frame and never animates while idle. + /// + StaticNoIdle = 0, + + /// + /// The creature plays one continuous standing loop at a fixed per-frame delay. + /// + NormalIdle = 1, + + /// + /// The creature plays a short standing loop by default and occasionally adds the optional animation frames, + /// chosen at random each time the previous loop ends. + /// + NormalPlusOptional = 2 +} + /// /// Represents the different types of SPF formats /// diff --git a/DALib/Drawing/MpfFile.cs b/DALib/Drawing/MpfFile.cs index 3197fc9..1b81a96 100644 --- a/DALib/Drawing/MpfFile.cs +++ b/DALib/Drawing/MpfFile.cs @@ -20,6 +20,10 @@ namespace DALib.Drawing; /// public sealed class MpfFile : Collection, ISavable { + private const int STATIC_NO_IDLE_INTERVAL_MS = 10_000; + private const int DEFAULT_IDLE_INTERVAL_MS = 300; + private const int MIN_NORMAL_IDLE_INTERVAL_MS = 100; + /// /// The number of frames for the second attack animation /// @@ -61,17 +65,34 @@ public sealed class MpfFile : Collection, ISavable public MpfHeaderType HeaderType { get; set; } /// - /// The number of frames in the standing animation including optional frames. If your normal standing animation has 4 - /// frames, but there are 2 extra frames that should occasionally be played, then you would put 6 here. (4 normal - /// frames + 2 optional frames). If there is no optional animation, this will have a value of 0. + /// The per-frame display interval for the idle animation, in milliseconds. + /// + /// + /// Always populated after load and always reflects the interval that should actually be used at playback time, + /// regardless of . Only serialized back to disk for + /// where the on-disk byte is stored in units of 100 ms; values not divisible by 100 ms are floored on save. + /// + public int AnimationIntervalMs { get; set; } + + /// + /// The number of optional frames appended to the standing animation for the + /// type. /// + /// + /// Together with , this value determines the returned + /// by . A value of zero means the sprite has no idle animation — see + /// . + /// public byte OptionalAnimationFrameCount { get; set; } /// - /// Specifies the ratio of playing the optional standing frames. For example, if this is set to 30, it will play the - /// optional frames 30% of the time + /// The probability, from 0 to 100, that the optional frames are appended to the standing loop on any given cycle. /// - public byte OptionalAnimationRatio { get; set; } + /// + /// Applies only when returns . Values + /// set for any other type are ignored on save. + /// + public byte OptionalAnimationProbability { get; set; } /// /// The palette number used to colorize this image @@ -141,6 +162,57 @@ public MpfFile( PixelHeight = height; } + /// + /// Determines the implied by the given standing and optional-animation frame counts. + /// + /// The value to classify. + /// The value to classify. + /// The idle type that governs how the ratio byte is interpreted. + public static MpfIdleType DetectIdleType(byte standingFrameCount, byte optionalAnimationFrameCount) + { + if (optionalAnimationFrameCount == 0) + return MpfIdleType.StaticNoIdle; + + if ((standingFrameCount == 0) || (standingFrameCount == optionalAnimationFrameCount)) + return MpfIdleType.NormalIdle; + + return MpfIdleType.NormalPlusOptional; + } + + //populates AnimationIntervalMs and OptionalAnimationProbability from the raw on-disk ratio byte + //according to the current idle type. must run after StandingFrameCount and + //OptionalAnimationFrameCount are assigned. + private void ApplyRawOptionalAnimationRatio(byte rawRatio) + { + switch (DetectIdleType(StandingFrameCount, OptionalAnimationFrameCount)) + { + case MpfIdleType.StaticNoIdle: + AnimationIntervalMs = STATIC_NO_IDLE_INTERVAL_MS; + + break; + case MpfIdleType.NormalIdle: + AnimationIntervalMs = rawRatio > 0 ? Math.Max(MIN_NORMAL_IDLE_INTERVAL_MS, rawRatio * 100) : DEFAULT_IDLE_INTERVAL_MS; + + break; + case MpfIdleType.NormalPlusOptional: + AnimationIntervalMs = DEFAULT_IDLE_INTERVAL_MS; + OptionalAnimationProbability = rawRatio; + + break; + } + } + + //projects the semantic properties back to a single on-disk ratio byte for serialization. the + //meaning depends on the current idle type — NormalIdle stores the interval / 100, NormalPlusOptional + //stores the probability, and StaticNoIdle stores nothing. + private byte GetRawOptionalAnimationRatio() + => DetectIdleType(StandingFrameCount, OptionalAnimationFrameCount) switch + { + MpfIdleType.NormalIdle => (byte)(AnimationIntervalMs / 100), + MpfIdleType.NormalPlusOptional => OptionalAnimationProbability, + _ => 0 + }; + private MpfFile(Stream stream) { using var reader = new BinaryReader(stream, Encoding.Default, true); @@ -194,7 +266,7 @@ private MpfFile(Stream stream) StandingFrameIndex = reader.ReadByte(); StandingFrameCount = reader.ReadByte(); OptionalAnimationFrameCount = reader.ReadByte(); - OptionalAnimationRatio = reader.ReadByte(); + ApplyRawOptionalAnimationRatio(reader.ReadByte()); AttackFrameIndex = reader.ReadByte(); AttackFrameCount = reader.ReadByte(); Attack2StartIndex = reader.ReadByte(); @@ -211,7 +283,7 @@ private MpfFile(Stream stream) StandingFrameIndex = reader.ReadByte(); StandingFrameCount = reader.ReadByte(); OptionalAnimationFrameCount = reader.ReadByte(); - OptionalAnimationRatio = reader.ReadByte(); + ApplyRawOptionalAnimationRatio(reader.ReadByte()); break; } @@ -306,7 +378,7 @@ public void Save(Stream stream) writer.Write(StandingFrameIndex); writer.Write(StandingFrameCount); writer.Write(OptionalAnimationFrameCount); - writer.Write(OptionalAnimationRatio); + writer.Write(GetRawOptionalAnimationRatio()); writer.Write(AttackFrameIndex); writer.Write(AttackFrameCount); writer.Write(Attack2StartIndex); @@ -320,7 +392,7 @@ public void Save(Stream stream) writer.Write(StandingFrameIndex); writer.Write(StandingFrameCount); writer.Write(OptionalAnimationFrameCount); - writer.Write(OptionalAnimationRatio); + writer.Write(GetRawOptionalAnimationRatio()); } var startAddress = 0; diff --git a/DALib/Drawing/Virtualized/MpfView.cs b/DALib/Drawing/Virtualized/MpfView.cs index a956e41..369daf2 100644 --- a/DALib/Drawing/Virtualized/MpfView.cs +++ b/DALib/Drawing/Virtualized/MpfView.cs @@ -51,17 +51,22 @@ public sealed class MpfView public byte AttackFrameIndex { get; } /// - /// The number of frames in the standing animation including optional frames. If your normal standing animation has 4 - /// frames, but there are 2 extra frames that should occasionally be played, then you would put 6 here. (4 normal - /// frames + 2 optional frames). If there is no optional animation, this will have a value of 0. + /// The per-frame display interval for the idle animation, in milliseconds. See + /// . + /// + public int AnimationIntervalMs { get; } + + /// + /// The number of optional frames appended to the standing animation for the + /// type. See . /// public byte OptionalAnimationFrameCount { get; } /// - /// Specifies the ratio of playing the optional standing frames. For example, if this is set to 30, it will play the - /// optional frames 30% of the time + /// The probability (0-100) that the optional frames are appended to the standing loop on any given cycle. Populated + /// only for . See . /// - public byte OptionalAnimationRatio { get; } + public byte OptionalAnimationProbability { get; } /// /// The palette number used to colorize this image @@ -121,7 +126,7 @@ private MpfView( byte standingFrameIndex, byte standingFrameCount, byte optionalAnimationFrameCount, - byte optionalAnimationRatio) + byte rawOptionalAnimationRatio) { Entry = entry; DataSectionOffset = dataSectionOffset; @@ -140,7 +145,25 @@ private MpfView( StandingFrameIndex = standingFrameIndex; StandingFrameCount = standingFrameCount; OptionalAnimationFrameCount = optionalAnimationFrameCount; - OptionalAnimationRatio = optionalAnimationRatio; + + //derive AnimationIntervalMs (and OptionalAnimationProbability) from the raw ratio byte, + //matching MpfFile. the interval is always populated regardless of idle type. + switch (MpfFile.DetectIdleType(standingFrameCount, optionalAnimationFrameCount)) + { + case MpfIdleType.StaticNoIdle: + AnimationIntervalMs = 10_000; + + break; + case MpfIdleType.NormalIdle: + AnimationIntervalMs = rawOptionalAnimationRatio > 0 ? Math.Max(100, rawOptionalAnimationRatio * 100) : 300; + + break; + case MpfIdleType.NormalPlusOptional: + AnimationIntervalMs = 300; + OptionalAnimationProbability = rawOptionalAnimationRatio; + + break; + } } /// @@ -203,7 +226,7 @@ public static MpfView FromEntry(DataArchiveEntry entry) byte standingFrameIndex, standingFrameCount, optionalAnimationFrameCount, - optionalAnimationRatio, + rawOptionalAnimationRatio, attackFrameIndex, attackFrameCount, attack2StartIndex = 0, @@ -217,7 +240,7 @@ public static MpfView FromEntry(DataArchiveEntry entry) standingFrameIndex = reader.ReadByte(); standingFrameCount = reader.ReadByte(); optionalAnimationFrameCount = reader.ReadByte(); - optionalAnimationRatio = reader.ReadByte(); + rawOptionalAnimationRatio = reader.ReadByte(); attackFrameIndex = reader.ReadByte(); attackFrameCount = reader.ReadByte(); attack2StartIndex = reader.ReadByte(); @@ -233,7 +256,7 @@ public static MpfView FromEntry(DataArchiveEntry entry) standingFrameIndex = reader.ReadByte(); standingFrameCount = reader.ReadByte(); optionalAnimationFrameCount = reader.ReadByte(); - optionalAnimationRatio = reader.ReadByte(); + rawOptionalAnimationRatio = reader.ReadByte(); break; } @@ -289,7 +312,7 @@ public static MpfView FromEntry(DataArchiveEntry entry) standingFrameIndex, standingFrameCount, optionalAnimationFrameCount, - optionalAnimationRatio); + rawOptionalAnimationRatio); } ///