From 31a6071462fc3ba2668435c2ce989a9ccc0fbbb4 Mon Sep 17 00:00:00 2001 From: scientificworld Date: Wed, 11 Mar 2026 17:58:20 +0800 Subject: [PATCH] feat: HuneX initial support --- ArcFormats/Ethornell/ArcBGI.cs | 7 +- ArcFormats/Ethornell/AudioBGI.cs | 4 +- ArcFormats/HuneX/ArcHFA.cs | 58 ++++++++++ ArcFormats/HuneX/Decoder.cs | 191 +++++++++++++++++++++++++++++++ 4 files changed, 257 insertions(+), 3 deletions(-) create mode 100644 ArcFormats/HuneX/ArcHFA.cs create mode 100644 ArcFormats/HuneX/Decoder.cs diff --git a/ArcFormats/Ethornell/ArcBGI.cs b/ArcFormats/Ethornell/ArcBGI.cs index 3b972735..3442d1dc 100644 --- a/ArcFormats/Ethornell/ArcBGI.cs +++ b/ArcFormats/Ethornell/ArcBGI.cs @@ -124,6 +124,11 @@ namespace GameRes.Formats.BGI { if (!file.View.AsciiEqual (4, "KO ARC20")) return null; + return Open (file); + } + + protected ArcFile Open (ArcView file) + { int count = file.View.ReadInt32 (12); if (!IsSaneCount (count)) return null; @@ -152,7 +157,7 @@ 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+4, "bw ")) + else if (file.View.AsciiEqual (entry.Offset+5, "w ")) entry.Type = "audio"; } return new ArcFile (file, this, dir); diff --git a/ArcFormats/Ethornell/AudioBGI.cs b/ArcFormats/Ethornell/AudioBGI.cs index 501d27d8..7a0438ae 100644 --- a/ArcFormats/Ethornell/AudioBGI.cs +++ b/ArcFormats/Ethornell/AudioBGI.cs @@ -39,13 +39,13 @@ namespace GameRes.Formats.BGI public BgiAudio () { Signatures = new uint[] { 0x40, 0 }; - Extensions = new string[] { "bw", "", "_bw" }; + Extensions = new string[] { "bw", "", "_bw", "hw" }; } public override SoundInput TryOpen (IBinaryStream file) { var header = file.ReadHeader (8); - if (!header.AsciiEqual (4, "bw ")) + if (!header.AsciiEqual (4, "bw ") && !header.AsciiEqual (4, "hw ")) return null; uint offset = header.ToUInt32 (0); if (offset >= file.Length) diff --git a/ArcFormats/HuneX/ArcHFA.cs b/ArcFormats/HuneX/ArcHFA.cs new file mode 100644 index 00000000..25a4b8bb --- /dev/null +++ b/ArcFormats/HuneX/ArcHFA.cs @@ -0,0 +1,58 @@ +//! \file ArcHFA.cs +//! \date 2026-01-18 +//! \brief HUNEX General Game Engine resource archive. +// +// 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.Collections.Generic; +using System.ComponentModel.Composition; +using System.IO; +using GameRes.Utility; + +namespace GameRes.Formats.HuneX { + [Export(typeof(ArchiveFormat))] + public class HfaOpener : BGI.Arc2Opener { + public override string Tag { get { return "HFA"; } } + public override string Description { get { return "HuneX general game engine resource archive"; } } + public override uint Signature { get { return 0x454e5548; } } // "HUNE" + public override bool IsHierarchic { get { return false; } } + public override bool CanWrite { get { return false; } } + + public HfaOpener() { + Extensions = new string[] { "hfa" }; + ContainedFormats = new[] { "BGI", "CompressedBG", "BW", "SCR" }; + } + + public override ArcFile TryOpen(ArcView file) { + if (!file.View.AsciiEqual(4, "XGGEFA10")) + return null; + return Open(file); + } + + public override Stream OpenEntry(ArcFile arc, Entry entry) { + if (!arc.File.View.AsciiEqual(entry.Offset, "LenZuCompressor")) + return base.OpenEntry(arc, entry); + var decoder = new LenZuDecoder(arc.File.View.ReadBytes(entry.Offset + 0x20, entry.Size - 0x20)); + return new BinMemoryStream(decoder.Unpack()); + } + } +} diff --git a/ArcFormats/HuneX/Decoder.cs b/ArcFormats/HuneX/Decoder.cs new file mode 100644 index 00000000..beab6c52 --- /dev/null +++ b/ArcFormats/HuneX/Decoder.cs @@ -0,0 +1,191 @@ +//! \file Decoder.cs +//! \date 2026-02-22 +//! \brief HUNEX General Game Engine decompression functions. +// +// 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.Linq; + +namespace GameRes.Formats.HuneX { + internal class HuffmanNode { + public int Weight; + public int Index; + public HuffmanNode Parent; + public HuffmanNode Child0; + public HuffmanNode Child1; + } + + internal sealed class HuffmanTree { + List m_table; + bool m_invert; + + public HuffmanTree(int first_real_entry, Dictionary weights, bool invert = false) { + m_table = new List(first_real_entry); + + for (int i = 0; i < first_real_entry; i++) { + m_table.Add(new HuffmanNode { + Index = i, + Weight = weights.TryGetValue(i, out int w) ? w : 0 + }); + } + + m_invert = invert; + } + + public void Build(int max_entries) { + int total_weight = m_table.Sum(x => x.Weight); + for (int i = m_table.Count; i < max_entries; i++) { + HuffmanNode child0 = null, child1 = null; + for (int j = 0; j < i; j++) { + var node = m_table[j]; + if (node.Weight == 0 || node.Parent != null) + continue; + if (child0 == null || node.Weight < child0.Weight) { + child1 = child0; + child0 = node; + } + else if (child1 == null || node.Weight < child1.Weight) { + child1 = node; + } + } + var parent = new HuffmanNode(); + if (m_invert) { + SetNodeRelation(parent, child1, child0); + } + else { + SetNodeRelation(parent, child0, child1); + } + m_table.Add(parent); + if (parent.Weight >= total_weight) + break; + } + } + + public int DecodeSequence(MsbBitStream input) { + HuffmanNode node = m_table[m_table.Count - 1]; + + while (node.Child0 != null || node.Child1 != null) { + int bit = input.GetNextBit(); + node = bit > 0 ? node.Child1 : node.Child0; + } + + return node.Index; + } + + void SetNodeRelation(HuffmanNode parent, HuffmanNode child0, HuffmanNode child1) { + if (child0 != null) { + parent.Child0 = child0; + child0.Parent = parent; + parent.Weight += child0.Weight; + } + if (child1 != null) { + parent.Child1 = child1; + child1.Parent = parent; + parent.Weight += child1.Weight; + } + } + } + + internal class LenZuSettings { + public byte HuffmanTableBitCount; + public byte BackrefLowBitCount; + public byte BackrefBaseDistance; + } + + internal sealed class LenZuDecoder { + Stream m_input; + byte[] m_unpacked; + LenZuSettings m_settings; + + public LenZuDecoder(byte[] buffer) { + m_unpacked = new byte[BitConverter.ToUInt32(buffer, 0)]; + m_settings = new LenZuSettings { + HuffmanTableBitCount = Math.Max(buffer[0x11], buffer[0x12]), + BackrefLowBitCount = buffer[0x14], + BackrefBaseDistance = buffer[0x15] + }; + m_input = new MemoryStream(buffer.Skip(0x16).ToArray()); + } + + public byte[] Unpack() { + int offset = 0; + int first_real_entry = 1 << m_settings.HuffmanTableBitCount; + int index_bits = (m_settings.HuffmanTableBitCount + 7) / 8; + int index_bytes = (index_bits + 7) / 8; + int fill_entries = ReadIntVL(index_bytes); + if (fill_entries == 0) + fill_entries = first_real_entry; + var weights = new Dictionary(); + if (first_real_entry * 4 < (index_bits + 4) * fill_entries) { + fill_entries = first_real_entry; + for (int i = 0; i < fill_entries; i++) { + weights[i] = ReadIntVL(); + } + } + else { + for (int i = 0; i < fill_entries; i++) { + int idx = ReadIntVL(index_bytes); + weights[idx] = ReadIntVL(); + } + } + var tree = new HuffmanTree(first_real_entry, 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) { + int isBackRef = input.GetNextBit(); + if (isBackRef == -1) + break; + int length = tree.DecodeSequence(input); + if (isBackRef > 0) { + length += m_settings.BackrefBaseDistance; + int distanceHighBits = tree.DecodeSequence(input); + int distanceLowBits = m_settings.BackrefLowBitCount > 0 + ? input.GetBits(m_settings.BackrefLowBitCount) : 0; + int distance = (distanceLowBits + | (distanceHighBits << m_settings.BackrefLowBitCount)) + + m_settings.BackrefBaseDistance; + for (int i = 0; i < length; i++) { + m_unpacked[offset] = m_unpacked[offset - distance]; + offset++; + } + } + else { + for (int i = 0; i < length + 1; i++) { + m_unpacked[offset++] = (byte)input.GetBits(8); + } + } + } + return m_unpacked; + } + } + + int ReadIntVL(int length = sizeof(int)) { + var buffer = new byte[Math.Max(sizeof(int), length)]; + m_input.Read(buffer, 0, length); + return BitConverter.ToInt32(buffer, 0); + } + } +}