diff --git a/DALib/Data/MapFile.cs b/DALib/Data/MapFile.cs
index 89cc9f9..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);
@@ -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
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/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/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();
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/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);
}
///
diff --git a/DALib/Extensions/IntExtensions.cs b/DALib/Extensions/IntExtensions.cs
index 60546ee..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 int tileIndex) => (tileIndex > 10012) || ((tileIndex % 10000) > 12);
+ public static bool IsRenderedTileIndex(this short tileIndex) => (tileIndex > 10012) || ((tileIndex % 10000) > 12);
}
\ No newline at end of file
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;