diff --git a/ArcFormats/ArcFormats.csproj b/ArcFormats/ArcFormats.csproj index 97e1b49a..8ce9e7c9 100644 --- a/ArcFormats/ArcFormats.csproj +++ b/ArcFormats/ArcFormats.csproj @@ -139,6 +139,8 @@ + + @@ -1091,6 +1093,7 @@ + diff --git a/ArcFormats/Enigma/ArcEVB.cs b/ArcFormats/Enigma/ArcEVB.cs index e117e293..f833329f 100644 --- a/ArcFormats/Enigma/ArcEVB.cs +++ b/ArcFormats/Enigma/ArcEVB.cs @@ -29,6 +29,7 @@ using System.IO; using System.Linq; using System.Text; using GameRes.Utility; +using GameRes.Compression; namespace GameRes.Formats.Enigma { public enum NodeTypes { @@ -45,14 +46,33 @@ namespace GameRes.Formats.Enigma { public override bool IsHierarchic { get { return true; } } public override bool CanWrite { get { return false; } } + public EvbPackOpener() { + Signatures = new uint[] { 0x425645, 0x905a4d, 0 }; + } + public override ArcFile TryOpen(ArcView file) { - uint index_size = file.View.ReadUInt32(0x40) + 68; - uint index_offset = 0x4F; + uint base_offset = 0; + if (file.View.AsciiEqual(0, "MZ")) { + var exe = new ExeFile(file); + var sig = new byte[] { 0x45, 0x56, 0x42, 0x00 }; + if (exe.ContainsSection(".enigma1")) { + var ofs = exe.FindString(exe.Sections[".enigma1"], sig); + if (ofs != -1) + base_offset = (uint)ofs; + } + if (base_offset == 0) + return null; + } + else if (!file.View.AsciiEqual(0, "EVB")) + return null; + + uint index_size = file.View.ReadUInt32(base_offset + 0x40) + base_offset + 68; + uint index_offset = base_offset + 0x4F; uint file_offset = index_size; var dir = new List(); var name_buffer = new StringBuilder(); - var counts = new List { file.View.ReadUInt32(0x4C) }; + var counts = new List { file.View.ReadUInt32(base_offset + 0x4C) }; var names = new List { "" }; while (index_offset < index_size - 4) { @@ -73,12 +93,12 @@ namespace GameRes.Formats.Enigma { index_offset++; counts[counts.Count - 1]--; if (type == NodeTypes.File) { - var entry = Create(Path.Combine(names.Concat(new[] { name }).ToArray())); + var entry = Create(Path.Combine(names.Concat(new[] { name }).ToArray())); uint unpacked_size = file.View.ReadUInt32(index_offset + 2); uint size = file.View.ReadUInt32(index_offset + 49); - if (unpacked_size != size) - return null; // packed entry not implemented + entry.IsPacked = unpacked_size != size; entry.Offset = file_offset; + entry.UnpackedSize = unpacked_size; entry.Size = size; file_offset += size; if (!entry.CheckPlacement(file.MaxOffset)) @@ -106,5 +126,30 @@ namespace GameRes.Formats.Enigma { return new ArcFile(file, this, dir); } + + public override Stream OpenEntry(ArcFile arc, Entry entry) { + var pent = entry as PackedEntry; + if (pent.IsPacked) { + uint header_size = arc.File.View.ReadUInt32(pent.Offset); + uint offset = header_size; + Stream input = null; + + for (uint i = 8; i < header_size; i += 12) { + uint chunk_size = arc.File.View.ReadUInt32(pent.Offset + i); + var chunk = new aPLibStream( + arc.File.CreateStream(pent.Offset + offset, chunk_size) + ); + if (input != null) + input = new ConcatStream(input, chunk); + else + input = chunk; + offset += chunk_size; + } + + return input; + } + else + return arc.File.CreateStream(pent.Offset, pent.Size); + } } } diff --git a/ArcFormats/aNCHOR/ArcFPD.cs b/ArcFormats/aNCHOR/ArcFPD.cs new file mode 100644 index 00000000..f9c1a3cc --- /dev/null +++ b/ArcFormats/aNCHOR/ArcFPD.cs @@ -0,0 +1,134 @@ +//! \file ArcFPD.cs +//! \date 2026-01-19 +//! \brief AGES Mk2 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; +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.IO; +using System.Text; +using GameRes.Compression; +using GameRes.Utility; + +namespace GameRes.Formats.Anchor { + internal class FpdArchive : ArcFile { + public readonly byte[] Key; + + public FpdArchive(ArcView arc, ArchiveFormat impl, ICollection dir, byte[] key) : base (arc, impl, dir) { + Key = key; + } + } + + [Export(typeof(ArchiveFormat))] + public class FpdOpener : ArchiveFormat { + public override string Tag { get { return "FPD"; } } + public override string Description { get { return "AGES Mk2 resource archive"; } } + public override uint Signature { get { return 0x00445046; } } // 'FPD\x00' + public override bool IsHierarchic { get { return true; } } + public override bool CanWrite { get { return false; } } + + public override ArcFile TryOpen(ArcView file) { + int version = Binary.BigEndian(file.View.ReadInt32(4)); + if (version != 2) + return null; + + int count = (int)Binary.BigEndian(file.View.ReadInt64(8)); + if (!IsSaneCount(count)) + return null; + + long data_start = Binary.BigEndian(file.View.ReadInt64(0x10)); + long offset = 0x38; + if (data_start < count * 32 + offset) + return null; + uint index_size = (uint)(data_start - offset); + + var key = QueryKey(); + var dir = new List(count); + + using (var input = file.CreateStream(offset, (uint)(data_start - offset))) + using (var decrypted = new ByteStringEncryptedStream(input, key)) + using (var reader = new BinaryReader(decrypted)) { + var name_offsets = new List(count); + for (int i = 0; i < count; i++) { + long name_offset = Binary.BigEndian(reader.ReadInt64()); + long data_offset = Binary.BigEndian(reader.ReadInt64()); + long size = Binary.BigEndian(reader.ReadInt64()); + long unpacked_size = Binary.BigEndian(reader.ReadInt64()); + + var entry = new PackedEntry { + Offset = data_start + data_offset, + Size = (uint)size, + UnpackedSize = (uint)unpacked_size, + IsPacked = unpacked_size != 0 + }; + if (!entry.CheckPlacement(file.MaxOffset)) + return null; + dir.Add(entry); + name_offsets.Add(name_offset); + } + var name_block = reader.ReadBytes((int)(data_start - offset - count * 32)); + using (var mem = new MemoryStream(name_block)) + using (var stream = new ZLibStream(mem, CompressionMode.Decompress)) + using (var output = new MemoryStream()) { + stream.CopyTo(output); + var names = output.ToArray(); + for (int i = 0; i < count; i++) { + dir[i].Name = Binary.GetCString(names, (int)name_offsets[i], Encoding.UTF8); + dir[i].Type = FormatCatalog.Instance.GetTypeFromName(dir[i].Name); + } + } + return new FpdArchive(file, this, dir, key); + } + } + + public override Stream OpenEntry(ArcFile arc, Entry entry) { + var farc = arc as FpdArchive; + var pent = entry as PackedEntry; + var input = farc.File.CreateStream(entry.Offset, entry.Size); + var decrypted = new ByteStringEncryptedStream(input, farc.Key); + if (pent.IsPacked) + return new ZLibStream(decrypted, CompressionMode.Decompress); + else + return decrypted; + // TODO: epk decryption + } + + byte[] QueryKey() { + return DefaultScheme.ArchiveKey; + } + + FpdScheme DefaultScheme = new FpdScheme(); + + public override ResourceScheme Scheme { + get { return DefaultScheme; } + set { DefaultScheme = (FpdScheme)value; } + } + } + + [Serializable] + public class FpdScheme : ResourceScheme { + public byte[] ArchiveKey; + public byte[] EpkKey; + } +} diff --git a/ArcFormats/aNCHOR/AudioFCD.cs b/ArcFormats/aNCHOR/AudioFCD.cs new file mode 100644 index 00000000..56f79fa1 --- /dev/null +++ b/ArcFormats/aNCHOR/AudioFCD.cs @@ -0,0 +1,55 @@ +//! \file AudioFCD.cs +//! \date 2026-01-19 +//! \brief AGES Mk2 audio 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.Linq; + +namespace GameRes.Formats.Anchor +{ + [Export(typeof(AudioFormat))] + public class FcdAudio : AudioFormat + { + public override string Tag { get { return "FCD"; } } + public override string Description { get { return "AGES Mk2 audio format"; } } + public override uint Signature { get { return 0x00444346; } } // 'FCD\x00' + + public FcdAudio () + { + Extensions = new string[] { "fcd" }; + } + + public override SoundInput TryOpen (IBinaryStream file) + { + file.Position = 4; + // guess: big endian, version=2, type=0 (ogg), offset=0xC + byte[] data = { 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x4F, 0x67, 0x67, 0x53 }; + if (!file.ReadBytes (0x0C).SequenceEqual (data)) + throw new NotSupportedException(); + return new OggInput (new StreamRegion (file.AsStream, 0x0C)); + } + } +} diff --git a/ArcFormats/aPLibStream.cs b/ArcFormats/aPLibStream.cs new file mode 100644 index 00000000..a454750f --- /dev/null +++ b/ArcFormats/aPLibStream.cs @@ -0,0 +1,188 @@ +//! \file aPLibStream.cs +//! \date 2026-01-09 + +/* + * aPLib compression library - the smaller the better :) + * + * C depacker + * + * Copyright (c) 1998-2014 Joergen Ibsen + * All Rights Reserved + * + * http://www.ibsensoftware.com/ + */ + +// C# port by scientificworld + +using System; +using System.Collections.Generic; +using System.IO; + +namespace GameRes.Compression { + public sealed class aPLibCoroutine : Decompressor { + Stream m_input; + uint m_tag; + uint m_bitcount; + + public override void Initialize (Stream input) { + m_input = input; + } + + protected override IEnumerator Unpack () { + uint offs, len, R0, LWM; + + var hist = new List(); + + m_bitcount = 0; + R0 = uint.MaxValue; // (uint) -1 + LWM = 0; + + m_buffer[m_pos] = ReadByte(); + hist.Add(m_buffer[m_pos++]); + if (--m_length == 0) + yield return m_pos; + + while (true) { + if (GetBit() != 0) { + if (GetBit() != 0) { + if (GetBit() != 0) { + offs = 0; + + for (int i = 0; i < 4; i++) { + offs = (offs << 1) + GetBit(); + } + + if (offs != 0) { + m_buffer[m_pos] = hist[(int)(hist.Count - offs)]; + } + else { + m_buffer[m_pos] = 0x00; + } + hist.Add(m_buffer[m_pos++]); + + LWM = 0; + if (--m_length == 0) + yield return m_pos; + } + else { + offs = (uint)ReadByte(); + + len = 2 + (offs & 0x0001); + + offs >>= 1; + + if (offs != 0) { + for (; len != 0; len--) { + m_buffer[m_pos] = hist[(int)(hist.Count - offs)]; + hist.Add(m_buffer[m_pos++]); + if (--m_length == 0) + yield return m_pos; + } + } + else { + yield break; + } + + R0 = offs; + LWM = 1; + } + } + else { + offs = GetGamma(); + + if ((LWM == 0) && (offs == 2)) { + offs = R0; + + len = GetGamma(); + + for (; len != 0; len--) { + m_buffer[m_pos] = hist[(int)(hist.Count - offs)]; + hist.Add(m_buffer[m_pos++]); + if (--m_length == 0) + yield return m_pos; + } + } + else { + if (LWM == 0) { + offs -= 3; + } + else { + offs -= 2; + } + + offs <<= 8; + offs += (uint)ReadByte(); + + len = GetGamma(); + + if (offs >= 32000) { + len++; + } + if (offs >= 1280) { + len++; + } + if (offs < 128) { + len += 2; + } + + for (; len != 0; len--) { + m_buffer[m_pos] = hist[(int)(hist.Count - offs)]; + hist.Add(m_buffer[m_pos++]); + if (--m_length == 0) + yield return m_pos; + } + + R0 = offs; + } + + LWM = 1; + } + } + else { + m_buffer[m_pos] = ReadByte(); + hist.Add(m_buffer[m_pos++]); + LWM = 0; + if (--m_length == 0) + yield return m_pos; + } + } + } + + uint GetBit () { + uint bit; + + if (m_bitcount-- == 0) { + m_tag = (uint)ReadByte(); + m_bitcount = 7; + } + bit = (m_tag >> 7) & 0x01; + m_tag <<= 1; + + return bit; + } + + uint GetGamma () { + uint result = 1; + + do { + result = (result << 1) + GetBit(); + } while (GetBit() != 0); + + return result; + } + + byte ReadByte () { + int b = m_input.ReadByte(); + if (b == -1) + throw new EndOfStreamException(); + return (byte)b; + } + } + + public class aPLibStream : PackedStream { + public aPLibStream (Stream input, CompressionMode mode = CompressionMode.Decompress, bool leave_open = false) : base (input, leave_open) { + if (mode != CompressionMode.Decompress) + throw new NotImplementedException ("aPLibStream compression not implemented"); + } + } +} diff --git a/GameRes/Utility.cs b/GameRes/Utility.cs index 315b9120..2310f934 100644 --- a/GameRes/Utility.cs +++ b/GameRes/Utility.cs @@ -113,6 +113,11 @@ namespace GameRes.Utility return GetCString (data, index, length_limit, Encodings.cp932); } + public static string GetCString (byte[] data, int index, Encoding enc) + { + return GetCString (data, index, data.Length - index, enc); + } + public static string GetCString (byte[] data, int index) { return GetCString (data, index, data.Length - index, Encodings.cp932);