feat: HuneX image formats

This commit is contained in:
scientificworld
2026-04-16 03:07:38 +08:00
parent ebb27bd9f6
commit 9124ce1066
5 changed files with 673 additions and 16 deletions

View File

@@ -157,6 +157,8 @@ namespace GameRes.Formats.BGI
entry.Type = res.Type;
else if (file.View.AsciiEqual (entry.Offset, "BSE 1."))
entry.Type = "image";
else if (file.View.AsciiEqual (entry.Offset, "CompressedBG"))
entry.Type = "image";
else if (file.View.AsciiEqual (entry.Offset+5, "w "))
entry.Type = "audio";
}

View File

@@ -39,7 +39,7 @@ namespace GameRes.Formats.HuneX {
public HfaOpener() {
Extensions = new string[] { "hfa" };
ContainedFormats = new[] { "BGI", "CompressedBG", "BW", "SCR" };
ContainedFormats = new[] { "BGI", "CompressedBG_MT", "BW", "SCR" };
}
public override ArcFile TryOpen(ArcView file) {

320
ArcFormats/HuneX/ArcMZP.cs Normal file
View File

@@ -0,0 +1,320 @@
//! \file ArcMZP.cs
//! \date 2026-02-03
//! \brief HUNEX General Game Engine multi-frame image container.
//
// Copyright (C) 2026 by morkt
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
//
using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.IO;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using GameRes.Utility;
namespace GameRes.Formats.HuneX {
internal class MzpMetaData : ImageMetaData {
public uint TileWidth { get; set; }
public uint TileHeight { get; set; }
public uint TileXCount { get; set; }
public uint TileYCount { get; set; }
public uint Characteristics { get; set; }
public uint Depth { get; set; }
public uint TileCrop { get; set; }
public BitmapPalette Palette { get; set; }
}
internal class MzpArchive : ArcFile {
public MzpMetaData MetaData { get; set; }
public MzpArchive (ArcView arc, ArchiveFormat impl, ICollection<Entry> dir, MzpMetaData metadata) : base (arc, impl, dir) {
MetaData = metadata;
}
}
internal class MzpEntry : Entry {
public int Index { get; set; }
}
[Export(typeof(ArchiveFormat))]
public class MrgOpener : ArchiveFormat {
public override string Tag { get { return "MZP"; } }
public override string Description { get { return "HuneX general game engine multi-frame image"; } }
public override uint Signature { get { return 0x6467726d; } } // "mrgd"
public override bool IsHierarchic { get { return false; } }
public override bool CanWrite { get { return false; } }
public MrgOpener() {
Extensions = new string[] { "mzp" };
}
public override ArcFile TryOpen(ArcView file) {
if (!file.View.AsciiEqual(4, "00"))
return null;
int count = file.View.ReadInt16(6);
if (!IsSaneCount(count))
return null;
var base_name = Path.GetFileNameWithoutExtension(file.Name);
MzpMetaData metadata = null;
var dir = new List<Entry>(count);
uint offset = 8;
for (int i = 0; i < count; i++) {
uint section_offset = file.View.ReadUInt16(offset);
uint file_offset = file.View.ReadUInt16(offset + 2);
uint size_boundary = file.View.ReadUInt16(offset + 4);
uint size = file.View.ReadUInt16(offset + 6);
var entry = new MzpEntry {
Offset = 8 * (count + 1) + section_offset * 0x800 + file_offset,
Size = (size_boundary - 1) / 0x20 * 0x800 * 0x20 + size
};
if (!entry.CheckPlacement(file.MaxOffset))
return null;
if (i == 0)
metadata = ReadInfo(file, entry);
else {
entry.Name = string.Format("{0}#{1:D3}", base_name, i);
entry.Type = "image";
entry.Index = i;
dir.Add(entry);
}
offset += 8;
}
if (metadata == null)
return null;
return new MzpArchive(file, this, dir, metadata);
}
MzpMetaData ReadInfo(ArcView file, Entry entry) {
if (entry.Size < 16)
return null;
MzpMetaData metadata = new MzpMetaData();
metadata.Width = file.View.ReadUInt16(entry.Offset);
metadata.Height = file.View.ReadUInt16(entry.Offset + 2);
metadata.TileWidth = file.View.ReadUInt16(entry.Offset + 4);
metadata.TileHeight = file.View.ReadUInt16(entry.Offset + 6);
metadata.TileXCount = file.View.ReadUInt16(entry.Offset + 8);
metadata.TileYCount = file.View.ReadUInt16(entry.Offset + 10);
ushort type = file.View.ReadUInt16(entry.Offset + 12);
metadata.Characteristics = type;
byte depth = file.View.ReadByte(entry.Offset + 14);
metadata.Depth = depth;
metadata.TileCrop = file.View.ReadByte(entry.Offset + 15);
if (type == 1 && (depth & 0xf) == 0)
metadata.BPP = 4;
else if (type == 1 && (depth & 0xf) == 1)
metadata.BPP = 8;
else if (type == 8 && depth == 0x14)
metadata.BPP = 24;
else if ((type == 0xb && depth == 0x14) || (type == 0xc && depth == 0x11))
metadata.BPP = 32;
else
throw new NotImplementedException("[MZP] Unsupported BPP type");
if (type == 1) {
int palette_size = (depth & 0xf) == 1 ? 256 : 16;
var raw_palette = new byte[0x400];
file.View.Read(entry.Offset + 16, raw_palette, 0, (uint)palette_size * 4);
if (depth == 0x11 || depth == 0x91) {
for (int i = 0; i < palette_size; i += 32) {
var block = new byte[32];
Buffer.BlockCopy(raw_palette, (i + 8) * 4, block, 0, block.Length);
Buffer.BlockCopy(raw_palette, (i + 16) * 4, raw_palette, (i + 8) * 4, block.Length);
Buffer.BlockCopy(block, 0, raw_palette, (i + 16) * 4, block.Length);
}
}
for (int i = palette_size; i < 256; i++)
raw_palette[i * 4 + 3] = 0xFF;
metadata.Palette = MzxImageReader.GetPaletteFromRaw(raw_palette);
}
else {
metadata.Palette = null;
}
return metadata;
}
public override IImageDecoder OpenImage(ArcFile arc, Entry entry) {
var marc = arc as MzpArchive;
byte[] buffer;
if (arc.File.View.AsciiEqual(entry.Offset, "MZX0")) {
buffer = arc.File.View.ReadBytes(entry.Offset + 4, entry.Size - 4);
var decoder = new MzxDecoder(buffer);
buffer = decoder.Unpack();
}
else
buffer = arc.File.View.ReadBytes(entry.Offset, entry.Size);
return new MzxImageReader(new BinMemoryStream(buffer), marc.MetaData);
}
}
internal class MzxImageReader : IImageDecoder {
IBinaryStream m_input;
byte[] m_output;
MzpMetaData m_info;
uint m_height;
uint m_width;
public byte[] Data { get { return m_output; } }
public PixelFormat Format { get; private set; }
public BitmapPalette Palette { get; private set; }
public Stream Source { get { m_input.Position = 0; return m_input.AsStream; } }
public ImageFormat SourceFormat { get { return null; } }
public ImageMetaData Info {
get {
return new ImageMetaData {
Height = m_height,
Width = m_width,
BPP = Format.BitsPerPixel
};
}
}
public ImageData Image {
get {
if (null == m_output)
Unpack();
return ImageData.Create(Info, Format, Palette, Data);
}
}
public MzxImageReader(IBinaryStream input, MzpMetaData info) {
m_input = input;
m_info = info;
m_height = m_info.TileHeight;
m_width = m_info.TileWidth;
Palette = m_info.Palette;
switch (m_info.BPP) {
case 4: // Format = PixelFormats.Indexed4; break;
case 8: Format = PixelFormats.Indexed8; break;
case 24: Format = PixelFormats.Bgr24; break;
case 32: Format = PixelFormats.Bgra32; break;
default: throw new InvalidFormatException();
}
}
public void Unpack() {
if (m_info.Characteristics == 0xC) {
UnpackHep();
return;
}
uint tile_size = m_height * m_width;
m_output = new byte[tile_size * (m_info.BPP + 4) / 8];
uint index = 0;
switch (m_info.BPP) {
case 4:
byte[] temp4 = new byte[(tile_size + 1) / 2];
m_input.Read(temp4, 0, temp4.Length);
for (int i = 0; i < temp4.Length; i++) {
m_output[index++] = (byte)(temp4[i] & 0x0F);
if (index < tile_size)
m_output[index++] = (byte)(temp4[i] >> 4);
}
break;
case 8:
m_input.Read(m_output, 0, m_output.Length);
break;
case 24:
case 32:
byte[] rgb565 = new byte[tile_size * 2];
m_input.Read(rgb565, 0, rgb565.Length);
byte[] offsets = new byte[tile_size];
m_input.Read(offsets, 0, offsets.Length);
byte[] alphas = null;
if (m_info.BPP == 32) {
alphas = new byte[tile_size];
m_input.Read(alphas, 0, alphas.Length);
}
for (int i = 0; i < tile_size; i++) {
ushort pq = BitConverter.ToUInt16(rgb565, i * 2);
byte offset_byte = offsets[i];
byte r = (byte)(((pq & 0xF800) >> 8) | ((offset_byte >> 5) & 7));
byte g = (byte)(((pq & 0x07E0) >> 3) | ((offset_byte >> 3) & 3));
byte b = (byte)(((pq & 0x001F) << 3) | (offset_byte & 7));
m_output[index++] = b;
m_output[index++] = g;
m_output[index++] = r;
if (alphas != null)
m_output[index++] = alphas[i];
}
break;
}
}
void UnpackHep() {
if (m_input.ReadUInt32() != 0x00504548) // 'HEP\0'
throw new InvalidFormatException();
m_input.ReadBytes(0x10);
m_width = m_input.ReadUInt32();
m_height = m_input.ReadUInt32();
m_input.ReadUInt32();
Format = PixelFormats.Indexed8;
m_output = new byte[m_height * m_width];
m_input.Read(m_output, 0, m_output.Length);
var raw_palette = new byte[0x400];
m_input.Read(raw_palette, 0, raw_palette.Length);
Palette = GetPaletteFromRaw(raw_palette);
}
public static BitmapPalette GetPaletteFromRaw(byte[] raw_palette) {
var colors = new Color[raw_palette.Length / 4];
for (int i = 0; i < raw_palette.Length; i += 4) {
byte r = raw_palette[i];
byte g = raw_palette[i + 1];
byte b = raw_palette[i + 2];
byte a = raw_palette[i + 3];
if ((a & 0x80) == 0)
a = (byte)(((a << 1) | (a >> 6)) & 0xFF);
else
a = 0xFF;
colors[i / 4] = Color.FromArgb(a, r, g, b);
}
return new BitmapPalette(colors);
}
#region IDisposable Members
bool m_disposed = false;
public void Dispose() {
if (!m_disposed) {
m_input.Dispose();
m_disposed = true;
}
}
#endregion
}
}

View File

@@ -30,25 +30,25 @@ using System.IO;
using System.Linq;
namespace GameRes.Formats.HuneX {
internal class HuffmanNode {
public int Weight;
public int Index;
public HuffmanNode Parent;
public HuffmanNode Child0;
public HuffmanNode Child1;
}
internal class HuffmanTree {
internal class HuffmanNode {
public int Weight;
public int Index;
public HuffmanNode Parent;
public HuffmanNode Child0;
public HuffmanNode Child1;
}
internal sealed class HuffmanTree {
List<HuffmanNode> m_table;
bool m_invert;
public HuffmanTree(int first_real_entry, Dictionary<int, int> weights, bool invert = false) {
m_table = new List<HuffmanNode>(first_real_entry);
public HuffmanTree(int[] weights, bool invert = false) {
m_table = new List<HuffmanNode>(weights.Length);
for (int i = 0; i < first_real_entry; i++) {
for (int i = 0; i < weights.Length; i++) {
m_table.Add(new HuffmanNode {
Index = i,
Weight = weights.TryGetValue(i, out int w) ? w : 0
Weight = weights[i]
});
}
@@ -84,7 +84,7 @@ namespace GameRes.Formats.HuneX {
}
}
public int DecodeSequence(MsbBitStream input) {
public int DecodeSequence(IBitStream input) {
HuffmanNode node = m_table[m_table.Count - 1];
while (node.Child0 != null || node.Child1 != null) {
@@ -138,7 +138,7 @@ namespace GameRes.Formats.HuneX {
int fill_entries = ReadIntVL(index_bytes);
if (fill_entries == 0)
fill_entries = first_real_entry;
var weights = new Dictionary<int, int>();
var weights = new int[first_real_entry]; // idk why this can work xD
if (first_real_entry * 4 < (index_bits + 4) * fill_entries) {
fill_entries = first_real_entry;
for (int i = 0; i < fill_entries; i++) {
@@ -151,7 +151,7 @@ namespace GameRes.Formats.HuneX {
weights[idx] = ReadIntVL();
}
}
var tree = new HuffmanTree(first_real_entry, weights, true);
var tree = new HuffmanTree(weights, true);
tree.Build(((first_real_entry + 1) * first_real_entry) >> 1);
using (var input = new MsbBitStream(m_input, true)) {
while (offset < m_unpacked.Length) {
@@ -188,4 +188,99 @@ namespace GameRes.Formats.HuneX {
return BitConverter.ToInt32(buffer, 0);
}
}
internal class RingBuffer<T> {
private readonly T[] m_buffer;
private int m_head;
private int m_tail;
private int m_count;
public T this[int index] { get { return m_buffer[index]; } }
public RingBuffer(int capacity) {
m_buffer = new T[capacity];
}
public void Append(T item) {
m_buffer[m_head] = item;
m_head = (m_head + 1) % m_buffer.Length;
if (m_count == m_buffer.Length)
m_tail = (m_tail + 1) % m_buffer.Length;
else
m_count++;
}
public void Append(T[] items) {
foreach (T item in items)
Append(item);
}
}
internal sealed class MzxDecoder {
Stream m_input;
byte[] m_unpacked;
public MzxDecoder(byte[] buffer) {
m_unpacked = new byte[BitConverter.ToUInt32(buffer, 0)];
m_input = new MemoryStream(buffer.Skip(4).ToArray());
}
public byte[] Unpack() {
int offset = 0;
int counter = 0;
var ringbuf = new RingBuffer<byte>(128);
while (offset < m_unpacked.Length) {
if (counter <= 0)
counter = 0x1000;
byte flag = (byte)m_input.ReadByte();
int len = flag >> 2;
var buffer = new byte[2];
switch (flag & 3) {
case 0: // RLE
if (counter != 0x1000) {
buffer[1] = m_unpacked[offset - 1];
buffer[0] = m_unpacked[offset - 2];
}
offset = Write2(buffer, offset, len + 1);
break;
case 1: // BACKREF
int k = m_input.ReadByte() * 2 + 2;
buffer = new byte[len * 2 + 2];
int pos = offset - k;
k = Math.Min(k, buffer.Length);
Buffer.BlockCopy(m_unpacked, pos, buffer, 0, k);
for (pos = k; pos < buffer.Length; pos += k) {
Buffer.BlockCopy(buffer, 0, buffer, pos, Math.Min(k, buffer.Length - pos));
}
offset = Write2(buffer, offset, 1);
break;
case 2: // RINGBUF
buffer[0] = ringbuf[len * 2];
buffer[1] = ringbuf[len * 2 + 1];
offset = Write2(buffer, offset, 1);
counter += len;
break;
case 3: // LITERAL
buffer = new byte[len * 2 + 2];
m_input.Read(buffer, 0, buffer.Length);
offset = Write2(buffer, offset, 1);
ringbuf.Append(buffer);
break;
}
counter -= len + 1;
}
return m_unpacked;
}
int Write2(byte[] buffer, int offset, int count) {
for (int i = 0; i < count; i++) {
int bytesToWrite = Math.Min(buffer.Length, m_unpacked.Length - offset);
if (bytesToWrite <= 0)
break;
Buffer.BlockCopy(buffer, 0, m_unpacked, offset, bytesToWrite);
offset += bytesToWrite;
}
return offset;
}
}
}

View File

@@ -0,0 +1,240 @@
//! \file ImageCBG.cs
//! \date 2026-02-19
//! \brief HUNEX General Game Engine image format.
//
// Copyright (C) 2026 by morkt
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
//
using System;
using System.ComponentModel.Composition;
using System.IO;
using System.Windows.Media;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace GameRes.Formats.HuneX
{
internal class CbgMetaData : ImageMetaData
{
public uint StripeHeight;
}
[Export(typeof(ImageFormat))]
public class CompressedBGFormat : ImageFormat
{
public override string Tag { get { return "CompressedBG_MT"; } }
public override string Description { get { return "HUNEX General Game Engine compressed image format"; } }
public override uint Signature { get { return 0x706D6F43; } }
public CompressedBGFormat ()
{
Extensions = new string[] { "cbg" };
}
public override void Write (Stream file, ImageData image)
{
throw new System.NotImplementedException ("BgiFormat.Write not implemented");
}
public override ImageMetaData ReadMetaData (IBinaryStream stream)
{
var header = stream.ReadHeader (0x30);
if (!header.AsciiEqual ("CompressedBG_MT"))
return null;
return new CbgMetaData
{
Width = header.ToUInt32 (0x10),
Height = header.ToUInt32 (0x14),
StripeHeight = header.ToUInt32 (0x18),
BPP = header.ToInt32 (0x1C),
};
}
public override ImageData Read (IBinaryStream stream, ImageMetaData info)
{
var meta = (CbgMetaData)info as CbgMetaData;
using (var reader = new CbgReader (stream.AsStream, meta))
{
reader.Unpack();
return ImageData.Create (meta, reader.Format, null, reader.Data, reader.Stride);
}
}
}
internal class CbgReader : LsbBitStream
{
byte[] m_output;
CbgMetaData m_info;
int m_pixel_size;
public byte[] Data { get { return m_output; } }
public PixelFormat Format { get; private set; }
public int Stride { get; private set; }
public CbgReader (Stream input, CbgMetaData info) : base (input, true)
{
m_info = info;
m_pixel_size = m_info.BPP / 8;
Stride = (int)info.Width * m_pixel_size;
switch (m_info.BPP)
{
case 32: Format = PixelFormats.Bgra32; break;
case 24: Format = PixelFormats.Bgr24; break;
case 8: Format = PixelFormats.Gray8; break;
default: throw new InvalidFormatException();
}
}
public void Unpack ()
{
uint count = (m_info.Height + m_info.StripeHeight - 1) / m_info.StripeHeight;
var len = new byte[4];
var offsets = new uint[count];
m_output = new byte[Stride * m_info.Height];
m_input.Position = 0x30;
for (int i = 0; i < count; i++)
{
m_input.Read (len, 0, 4);
offsets[i] = BitConverter.ToUInt32 (len, 0);
}
for (int i = 0; i < count; i++)
{
m_input.Seek (offsets[i], SeekOrigin.Begin);
uint height = (uint)Math.Min (m_info.StripeHeight, m_info.Height - m_info.StripeHeight * i);
m_input.Read (len, 0, 4);
var packed = new byte[BitConverter.ToUInt32 (len, 0)];
var weights = ReadWeightTable (m_input, 0x100);
var tree = new HuffmanTree (weights);
tree.Build(0x1ff);
HuffmanDecompress (tree, packed);
var stripe_output = new byte[Stride * height];
UnpackZeros (packed, stripe_output);
ReverseAverageSampling (stripe_output, height);
Buffer.BlockCopy (stripe_output, 0, m_output, (int)(Stride * m_info.StripeHeight * i), stripe_output.Length);
}
}
static internal int ReadInteger (Stream input)
{
int v = 0;
int code;
int code_length = 0;
do
{
code = input.ReadByte();
if (-1 == code || code_length >= 32)
return -1;
v |= (code & 0x7f) << code_length;
code_length += 7;
}
while (0 != (code & 0x80));
return v;
}
static protected int[] ReadWeightTable (Stream input, int length)
{
int[] leaf_nodes_weight = new int[length];
for (int i = 0; i < length; ++i)
{
int weight = ReadInteger (input);
if (-1 == weight)
throw new InvalidFormatException ("Invalid compressed stream");
leaf_nodes_weight[i] = weight;
}
return leaf_nodes_weight;
}
void HuffmanDecompress (HuffmanTree tree, byte[] output)
{
this.Reset();
for (int dst = 0; dst < output.Length; dst++)
{
output[dst] = (byte)tree.DecodeSequence (this);
}
}
void UnpackZeros (byte[] input, byte[] output)
{
int dst = 0;
int dec_zero = 0;
int src = 0;
while (dst < output.Length)
{
int code_length = 0;
int count = 0;
byte code;
do
{
if (src >= input.Length)
return;
code = input[src++];
count |= (code & 0x7f) << code_length;
code_length += 7;
}
while (0 != (code & 0x80));
if (dst + count > output.Length)
break;
if (0 == dec_zero)
{
if (src + count > input.Length)
break;
Buffer.BlockCopy (input, src, output, dst, count);
src += count;
}
else
{
for (int i = 0; i < count; ++i)
output[dst+i] = 0;
}
dec_zero ^= 1;
dst += count;
}
}
void ReverseAverageSampling (byte[] output, uint height)
{
for (int y = 0; y < height; ++y)
{
int line = y * Stride;
for (int x = 0; x < m_info.Width; ++x)
{
int pixel = line + x * m_pixel_size;
for (int p = 0; p < m_pixel_size; p++)
{
int avg = 0;
if (x > 0)
avg += output[pixel + p - m_pixel_size];
if (y > 0)
avg += output[pixel + p - Stride];
if (x > 0 && y > 0)
avg /= 2;
if (0 != avg)
output[pixel + p] += (byte)avg;
}
}
}
}
}
}