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
8 changes: 4 additions & 4 deletions DALib/Data/MapFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -142,15 +142,15 @@ public sealed class MapTile
/// The id of the background part of the tile. This id references a <see cref="Tile" /> from a <see cref="Tileset" />
/// loaded from Seo.dat
/// </summary>
public int Background { get; init; }
public short Background { get; init; }

/// <summary>
/// The id of the left foreground part of the tile. This id references an HPF image loaded from ia.dat
/// </summary>
public int LeftForeground { get; set; }
public short LeftForeground { get; set; }

/// <summary>
/// The id of the right foreground part of the tile. This id references an HPF image loaded from ia.dat
/// </summary>
public int RightForeground { get; set; }
public short RightForeground { get; set; }
}
22 changes: 22 additions & 0 deletions DALib/Definitions/Enums.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,28 @@ public enum MpfFormatType
SingleAttack = 0
}

/// <summary>
/// Describes how a creature sprite's idle/standing animation plays back.
/// </summary>
public enum MpfIdleType
{
/// <summary>
/// The creature shows a single unchanging frame and never animates while idle.
/// </summary>
StaticNoIdle = 0,

/// <summary>
/// The creature plays one continuous standing loop at a fixed per-frame delay.
/// </summary>
NormalIdle = 1,

/// <summary>
/// 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.
/// </summary>
NormalPlusOptional = 2
}

/// <summary>
/// Represents the different types of SPF formats
/// </summary>
Expand Down
1 change: 0 additions & 1 deletion DALib/Drawing/EpfFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 0 additions & 1 deletion DALib/Drawing/FntFile.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using System;
using System.IO;
using DALib.Data;
using DALib.Extensions;
Expand Down
17 changes: 13 additions & 4 deletions DALib/Drawing/Graphics.cs
Original file line number Diff line number Diff line change
Expand Up @@ -430,14 +430,18 @@ public static SKImage RenderDarknessOverlay(HeaFile hea, byte darknessOpacity =
/// <param name="palette">
/// A palette containing colors used by the frame
/// </param>
public static SKImage RenderImage(EpfFrame frame, Palette palette)
/// <param name="alphaType">
/// 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(
frame.Left,
frame.Top,
frame.PixelWidth,
frame.PixelHeight,
frame.Data,
palette);
palette,
alphaType);

/// <summary>
/// Renders an MpfFrame
Expand Down Expand Up @@ -1115,15 +1119,20 @@ 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);
var dstOffsetY = Math.Max(0, top);
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<SKColor>();
Expand Down
4 changes: 2 additions & 2 deletions DALib/Drawing/HeaFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -193,7 +193,7 @@ public void DecodeScanline(int layerIndex, int scanlineIndex, Span<byte> 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];
Expand Down
92 changes: 82 additions & 10 deletions DALib/Drawing/MpfFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ namespace DALib.Drawing;
/// </summary>
public sealed class MpfFile : Collection<MpfFrame>, 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;

/// <summary>
/// The number of frames for the second attack animation
/// </summary>
Expand Down Expand Up @@ -61,17 +65,34 @@ public sealed class MpfFile : Collection<MpfFrame>, ISavable
public MpfHeaderType HeaderType { get; set; }

/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// Always populated after load and always reflects the interval that should actually be used at playback time,
/// regardless of <see cref="MpfIdleType" />. Only serialized back to disk for <see cref="MpfIdleType.NormalIdle" />
/// where the on-disk byte is stored in units of 100 ms; values not divisible by 100 ms are floored on save.
/// </remarks>
public int AnimationIntervalMs { get; set; }

/// <summary>
/// The number of optional frames appended to the standing animation for the
/// <see cref="MpfIdleType.NormalPlusOptional" /> type.
/// </summary>
/// <remarks>
/// Together with <see cref="StandingFrameCount" />, this value determines the <see cref="MpfIdleType" /> returned
/// by <see cref="DetectIdleType" />. A value of zero means the sprite has no idle animation — see
/// <see cref="MpfIdleType.StaticNoIdle" />.
/// </remarks>
public byte OptionalAnimationFrameCount { get; set; }

/// <summary>
/// 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.
/// </summary>
public byte OptionalAnimationRatio { get; set; }
/// <remarks>
/// Applies only when <see cref="DetectIdleType" /> returns <see cref="MpfIdleType.NormalPlusOptional" />. Values
/// set for any other type are ignored on save.
/// </remarks>
public byte OptionalAnimationProbability { get; set; }

/// <summary>
/// The palette number used to colorize this image
Expand Down Expand Up @@ -141,6 +162,57 @@ public MpfFile(
PixelHeight = height;
}

/// <summary>
/// Determines the <see cref="MpfIdleType" /> implied by the given standing and optional-animation frame counts.
/// </summary>
/// <param name="standingFrameCount">The <see cref="StandingFrameCount" /> value to classify.</param>
/// <param name="optionalAnimationFrameCount">The <see cref="OptionalAnimationFrameCount" /> value to classify.</param>
/// <returns>The idle type that governs how the ratio byte is interpreted.</returns>
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);
Expand Down Expand Up @@ -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();
Expand All @@ -211,7 +283,7 @@ private MpfFile(Stream stream)
StandingFrameIndex = reader.ReadByte();
StandingFrameCount = reader.ReadByte();
OptionalAnimationFrameCount = reader.ReadByte();
OptionalAnimationRatio = reader.ReadByte();
ApplyRawOptionalAnimationRatio(reader.ReadByte());

break;
}
Expand Down Expand Up @@ -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);
Expand All @@ -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;
Expand Down
47 changes: 35 additions & 12 deletions DALib/Drawing/Virtualized/MpfView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,17 +51,22 @@ public sealed class MpfView
public byte AttackFrameIndex { get; }

/// <summary>
/// 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
/// <see cref="MpfFile.AnimationIntervalMs" />.
/// </summary>
public int AnimationIntervalMs { get; }

/// <summary>
/// The number of optional frames appended to the standing animation for the
/// <see cref="MpfIdleType.NormalPlusOptional" /> type. See <see cref="MpfFile.OptionalAnimationFrameCount" />.
/// </summary>
public byte OptionalAnimationFrameCount { get; }

/// <summary>
/// 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 cref="MpfIdleType.NormalPlusOptional" />. See <see cref="MpfFile.OptionalAnimationProbability" />.
/// </summary>
public byte OptionalAnimationRatio { get; }
public byte OptionalAnimationProbability { get; }

/// <summary>
/// The palette number used to colorize this image
Expand Down Expand Up @@ -121,7 +126,7 @@ private MpfView(
byte standingFrameIndex,
byte standingFrameCount,
byte optionalAnimationFrameCount,
byte optionalAnimationRatio)
byte rawOptionalAnimationRatio)
{
Entry = entry;
DataSectionOffset = dataSectionOffset;
Expand All @@ -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;
}
}

/// <summary>
Expand Down Expand Up @@ -203,7 +226,7 @@ public static MpfView FromEntry(DataArchiveEntry entry)
byte standingFrameIndex,
standingFrameCount,
optionalAnimationFrameCount,
optionalAnimationRatio,
rawOptionalAnimationRatio,
attackFrameIndex,
attackFrameCount,
attack2StartIndex = 0,
Expand All @@ -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();
Expand All @@ -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;
}
Expand Down Expand Up @@ -289,7 +312,7 @@ public static MpfView FromEntry(DataArchiveEntry entry)
standingFrameIndex,
standingFrameCount,
optionalAnimationFrameCount,
optionalAnimationRatio);
rawOptionalAnimationRatio);
}

/// <summary>
Expand Down
2 changes: 1 addition & 1 deletion DALib/Extensions/IntExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
/// </remarks>
public static bool IsRenderedTileIndex(this int tileIndex) => (tileIndex > 10012) || ((tileIndex % 10000) > 12);
public static bool IsRenderedTileIndex(this short tileIndex) => (tileIndex > 10012) || ((tileIndex % 10000) > 12);
}
1 change: 0 additions & 1 deletion DALib/Extensions/PalettizedExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using System;
using System.Collections.Frozen;
using System.Diagnostics;
using System.Linq;
using DALib.Definitions;
using DALib.Drawing;
Expand Down
Loading
Loading