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/Resources/Formats.dat b/ArcFormats/Resources/Formats.dat index 9b7bcf24..6a81cd5f 100644 Binary files a/ArcFormats/Resources/Formats.dat and b/ArcFormats/Resources/Formats.dat differ 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/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);