Skip to content
Merged
3 changes: 2 additions & 1 deletion DALib.Tests/DALib.Tests.csproj
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<LangVersion>14</LangVersion>
</PropertyGroup>

<ItemGroup>
Expand Down
7 changes: 4 additions & 3 deletions DALib/DALib.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@
<Nullable>enable</Nullable>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TargetFrameworks>net9.0;net10.0</TargetFrameworks>
<LangVersion>latestmajor</LangVersion>
<TargetFrameworks>net10.0;net10.0</TargetFrameworks>
<LangVersion>14</LangVersion>
<ManagePackageVersionsCentrally>false</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<None Include="$(MSBuildThisFileDirectory)README.md" Pack="true" PackagePath=""/>
Expand All @@ -39,4 +40,4 @@
<SupportedPlatform Include="macOS"/>
<SupportedPlatform Include="Windows"/>
</ItemGroup>
</Project>
</Project>
12 changes: 7 additions & 5 deletions DALib/Data/MapFile.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
using DALib.Abstractions;
using DALib.Drawing;
using DALib.Extensions;
#region
using System.IO;
using System.Text;
using DALib.Abstractions;
using DALib.Drawing;
using DALib.Extensions;
#endregion

namespace DALib.Data;

Expand Down Expand Up @@ -145,10 +147,10 @@ public sealed class MapTile
/// <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; init; }
public int 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; init; }
public int RightForeground { get; set; }
}
24 changes: 16 additions & 8 deletions DALib/Definitions/Enums.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,25 +23,33 @@ public enum Endianness
}

/// <summary>
/// Represents the different types of alpha blending used by EFA files
/// Represents the different blend modes used by EFA files. All EFA rendering operates in RGB555
/// </summary>
public enum EfaBlendingType : byte
{
/// <summary>
/// Transparency is added by the client based on the luminance of the pixel
/// Saturated additive blend: each channel is added to the destination and clamped to max (min(srcCh + dstCh, 31) in
/// RGB555). Use SKBlendMode.Plus for accurate blending
/// </summary>
Luminance = 1,
Additive = 1,

/// <summary>
/// Transparency is added by the client based on the luminance of the pixel. Slightly less transparent than "Luminance"
/// Per-channel self-alpha blend: each channel's value serves as its own alpha. Bright channels are opaque, dark
/// channels are transparent. Formula: result_ch = (srcCh * 32 + dstCh * (32 - srcCh)) >> 5. Use SKBlendMode.Screen for accurate blending
/// </summary>
LessLuminance = 2,
SelfAlpha = 2,

/// <summary>
/// Transparency is added by the client through some mechanism. Appears to be flood fill, but it only seems to work on
/// like 3 effects. Other effects that try to use this option will not render at all.
/// Standard alpha blend using a separate per-pixel alpha surface encoded in the EFA data (RLE or raw). The alpha value
/// is a single scalar applied uniformly to all three channels
/// </summary>
NotSure = 3
SeparateAlpha = 3,

/// <summary>
/// Alpha blend using a separate per-channel alpha surface. Like SeparateAlpha but the alpha surface provides
/// independent alpha values for each color channel
/// </summary>
PerChannelAlpha = 4
}

/// <summary>
Expand Down
4 changes: 3 additions & 1 deletion DALib/Definitions/Flags.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ public enum TileFlags : byte
Wall = 15,

/// <summary>
/// Tile has luminosity based transparency (dark = more transparent)
/// Tile uses screen blend compositing (output = src + dst * (1 - src) per channel). The client renders these with
/// mode 0x6D, where each color channel's value acts as its own alpha. Black pixels are fully transparent, white
/// pixels fully opaque
/// </summary>
Transparent = 128
}
38 changes: 25 additions & 13 deletions DALib/Drawing/EfaFile.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#region
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
Expand All @@ -11,13 +12,16 @@
using DALib.Extensions;
using DALib.IO;
using DALib.Memory;
using DALib.Utility;
using SkiaSharp;
#endregion

namespace DALib.Drawing;

/// <summary>
/// Represents an image file with the ".efa" extension. This image format supports one or more fully colorized images
/// encoded in RGB565, ZLIB compressed image data, luminance-based alpha blending, and a hard coded frame interval
/// encoded in RGB565 (rendered in RGB555), ZLIB compressed image data, multiple blend modes, and a hard coded frame
/// interval
/// </summary>
public sealed class EfaFile : Collection<EfaFrame>, ISavable
{
Expand Down Expand Up @@ -47,7 +51,7 @@ public sealed class EfaFile : Collection<EfaFrame>, ISavable
public EfaFile()
{
Unknown1 = 0;
BlendingType = EfaBlendingType.Luminance;
BlendingType = EfaBlendingType.Additive;
FrameIntervalMs = 50;
Unknown2 = new byte[51];
}
Expand Down Expand Up @@ -205,6 +209,11 @@ public static EfaFile FromImages(params SKImage[] orderedFrames)

using var bitmap = SKBitmap.FromImage(image);

// Only preserve blacks for non-additive effects. For Additive, black pixels
// are correctly "add nothing" and should not be converted to near-black.
if (efaFile.BlendingType != EfaBlendingType.Additive)
ImageProcessor.PreserveNonTransparentBlacks(bitmap);

byte[] rawBytes;

if (image.ColorType == SKColorType.Rgb565)
Expand Down Expand Up @@ -266,18 +275,16 @@ private static void DecompressToFrame(Stream dataStream, EfaFrame frame)
decompressed[..frame.ByteCount]
.CopyTo(frame.Data);

/*
//Not sure what these numbers are
var reader = new SpanReader(Encoding.Default, decompressed[frame.ByteCount..], Endianness.LittleEndian);
//alpha surface data follows the RGB pixel data for SeparateAlpha and PerChannelAlpha blend types
var alphaLength = frame.DecompressedSize - frame.ByteCount;

//not even sure if these should be ushort
//could they be colors? transparency map?
//the length of the tail data seems relevant to the size of the image
var nums = new List<byte>();
if (alphaLength > 0)
{
frame.AlphaData = new byte[alphaLength];

while (!reader.EndOfSpan)
nums.Add(reader.ReadByte());
*/
decompressed[frame.ByteCount..]
.CopyTo(frame.AlphaData);
}
}
#endregion

Expand Down Expand Up @@ -317,12 +324,17 @@ public void Save(Stream stream)
{
var frame = this[i];

//compress the frame data and store it to be written later
//compress the frame data (and alpha surface if present) and store it to be written later
using var compressed = new MemoryStream();

using (var compressor = new ZLibStream(compressed, CompressionLevel.Optimal, true))
{
compressor.Write(frame.Data, 0, frame.Data.Length);

if (frame.AlphaData is { Length: > 0 })
compressor.Write(frame.AlphaData, 0, frame.AlphaData.Length);
}

var compressedBytes = compressed.ToArray();
var compressedSize = compressedBytes.Length;
compressedFrames[i] = compressedBytes;
Expand Down
12 changes: 11 additions & 1 deletion DALib/Drawing/EfaFrame.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
namespace DALib.Drawing;
using DALib.Definitions;

namespace DALib.Drawing;

/// <summary>
/// Represents a frame in an EfaFile
/// </summary>
public sealed class EfaFrame
{
/// <summary>
/// Optional alpha surface data that follows the RGB pixel data in the decompressed stream. Present for
/// <see cref="EfaBlendingType.SeparateAlpha" /> and <see cref="EfaBlendingType.PerChannelAlpha" /> blend types. For
/// SeparateAlpha, this is either raw or RLE-encoded per-pixel alpha (0-31 range). For PerChannelAlpha, this is
/// per-channel alpha encoded as RGB555 (2 bytes per pixel)
/// </summary>
public byte[]? AlphaData { get; set; }

/// <summary>
/// The number of bytes of colorized data this frame contains
/// </summary>
Expand Down
113 changes: 113 additions & 0 deletions DALib/Drawing/FntFile.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
using System;
using System.IO;
using DALib.Data;
using DALib.Extensions;

namespace DALib.Drawing;

/// <summary>
/// Represents a bitmap font file with the ".fnt" extension. Headerless 1-bit-per-pixel glyph bitmaps stored
/// contiguously. English fonts use 8x12 cells, Korean fonts use 16x12 cells
/// </summary>
public sealed class FntFile
{
private readonly int BytesPerRow;
private readonly int BytesPerGlyph;

/// <summary>
/// The raw 1bpp glyph bitmap data
/// </summary>
public byte[] Data { get; }

/// <summary>
/// The pixel width of each glyph cell
/// </summary>
public int GlyphWidth { get; }

/// <summary>
/// The pixel height of each glyph cell
/// </summary>
public int GlyphHeight { get; }

/// <summary>
/// The total number of glyphs in the font
/// </summary>
public int GlyphCount { get; }

private FntFile(byte[] data, int glyphWidth, int glyphHeight)
{
Data = data;
GlyphWidth = glyphWidth;
GlyphHeight = glyphHeight;
BytesPerRow = (glyphWidth + 7) / 8;
BytesPerGlyph = BytesPerRow * glyphHeight;
GlyphCount = data.Length / BytesPerGlyph;
}

/// <summary>
/// Returns true if the specified glyph index is within the valid range
/// </summary>
public bool IsValidIndex(int index) => (uint)index < (uint)GlyphCount;

#region LoadFrom
/// <summary>
/// Loads an FntFile with the specified fileName from the specified archive
/// </summary>
/// <param name="fileName">
/// The name of the FNT file to extract from the archive
/// </param>
/// <param name="archive">
/// The DataArchive from which to retrieve the FNT file
/// </param>
/// <param name="glyphWidth">
/// The pixel width of each glyph cell (8 for English, 16 for Korean)
/// </param>
/// <param name="glyphHeight">
/// The pixel height of each glyph cell (typically 12)
/// </param>
/// <exception cref="FileNotFoundException">
/// Thrown if the FNT file with the specified name is not found in the archive
/// </exception>
public static FntFile FromArchive(string fileName, DataArchive archive, int glyphWidth, int glyphHeight)
{
if (!archive.TryGetValue(fileName.WithExtension(".fnt"), out var entry))
throw new FileNotFoundException($"FNT file \"{fileName}\" was not found in the archive");

return FromEntry(entry, glyphWidth, glyphHeight);
}

/// <summary>
/// Loads an FntFile from the specified archive entry
/// </summary>
/// <param name="entry">
/// The DataArchiveEntry to load the FntFile from
/// </param>
/// <param name="glyphWidth">
/// The pixel width of each glyph cell
/// </param>
/// <param name="glyphHeight">
/// The pixel height of each glyph cell
/// </param>
public static FntFile FromEntry(DataArchiveEntry entry, int glyphWidth, int glyphHeight)
=> new(entry.ToSpan().ToArray(), glyphWidth, glyphHeight);

/// <summary>
/// Loads an FntFile from the specified path
/// </summary>
/// <param name="path">
/// The path of the file to be read
/// </param>
/// <param name="glyphWidth">
/// The pixel width of each glyph cell
/// </param>
/// <param name="glyphHeight">
/// The pixel height of each glyph cell
/// </param>
public static FntFile FromFile(string path, int glyphWidth, int glyphHeight)
{
var data = File.ReadAllBytes(path);

return new FntFile(data, glyphWidth, glyphHeight);
}
#endregion
}
Loading
Loading