diff --git a/ArcFormats/Kogado/ArcARC.cs b/ArcFormats/Kogado/ArcARC.cs new file mode 100644 index 00000000..cceca363 --- /dev/null +++ b/ArcFormats/Kogado/ArcARC.cs @@ -0,0 +1,308 @@ +//! \file ArcARC.cs +//! \date 2023 Aug 26 +//! \brief Kogado resource archive. +// +// Copyright (C) 2023 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 GameRes.Compression; +using GameRes.Formats.DirectDraw; +using System; +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.IO; +using System.Text; +using System.Windows.Media; + +// [070302][G-mode] Keitai Shoujo + +namespace GameRes.Formats.Kogado +{ + [Export(typeof(ArchiveFormat))] + public class ArcOpener : ArchiveFormat + { + public override string Tag { get => "ARC/KOGADO"; } + public override string Description { get => "Kogado engine resource archive"; } + public override uint Signature { get => 0xA8BCADBE; } // 'ARCW' ^ 0xFFFFFFFF + public override bool IsHierarchic { get => true; } + public override bool CanWrite { get => false; } + + public override ArcFile TryOpen (ArcView file) + { + using (var reader = new ArcIndexReader (file)) + { + var dir = reader.ReadIndex(); + return new ArcFile(file, this, dir); + } + } + + public override Stream OpenEntry (ArcFile arc, Entry entry) + { + Stream input = arc.File.CreateStream (entry.Offset, entry.Size); + input = new XoredStream (input, 0xFF); + var ova = entry as OvaEntry; + if (ova != null) + return new PrefixStream (ova.Header, input); + var pent = entry as PackedEntry; + if (null == pent || !pent.IsPacked) + return input; + return new LimitStream (new LzssStream (input), pent.UnpackedSize); + } + + public override IImageDecoder OpenImage (ArcFile arc, Entry entry) + { + var dds = entry as DdsEntry; + if (null == dds) + return base.OpenImage (arc, entry); + var input = this.OpenEntry (arc, entry); + return new ImageDecoder (input, dds.Info); + } + } + + internal class OvaEntry : PackedEntry + { + public byte[] Header; + } + + internal class DdsEntry : PackedEntry + { + public DdsInfo Info; + } + + internal class DdsInfo : ImageMetaData + { + public DdsPF Flags; + } + + internal sealed class ArcIndexReader : IDisposable + { + IBinaryStream m_input; + uint m_base_offset; + long m_max_offset; + + public ArcIndexReader (ArcView arc) + { + m_base_offset = arc.View.ReadUInt32 (0xC); + m_max_offset = arc.MaxOffset; + m_input = arc.CreateStream(); + } + + byte[] m_filenames; + byte[] m_index; + List m_dir; + + public List ReadIndex () + { + m_input.Position = 0x10; + m_filenames = ReadChunk(); + m_index = ReadChunk(); + m_dir = new List(); + int section_count = m_index.ToInt32 (0); + int pos = 4; + for (int i = 0; i < section_count; ++i) + { + int count = m_index.ToInt32 (pos+8); + if (m_dir.Capacity < m_dir.Count + count) + m_dir.Capacity = m_dir.Count + count; + int section_size = m_index.ToInt32 (pos+0xC); + int name_pos = pos + 0x10; + int layout_pos = name_pos + 4 * count; + if (m_index.AsciiEqual (pos, "DDS\0")) + ReadDdsSection (name_pos, layout_pos, count); + else if (m_index.AsciiEqual (pos, "OVA\0")) + ReadOvaSection (name_pos, layout_pos, count); + else + ReadSection (name_pos, layout_pos, count); + pos += 0x10 + section_size; + } + return m_dir; + } + + void ReadSection (int name_pos, int layout_pos, int count) + { + for (int j = 0; j < count; ++j) + { + var name = ReadFileName (m_index.ToInt32 (name_pos)); + var entry = FormatCatalog.Instance.Create (name); + + entry.Offset = m_index.ToUInt32 (layout_pos) + m_base_offset; + entry.Size = m_index.ToUInt32 (layout_pos+4); + entry.UnpackedSize = m_index.ToUInt32 (layout_pos+0xC); + entry.IsPacked = true; + if (!entry.CheckPlacement (m_max_offset+0x14)) + throw new InvalidFormatException(); + + m_dir.Add (entry); + name_pos += 4; + layout_pos += 0x10; + } + } + + void ReadDdsSection (int name_pos, int layout_pos, int count) + { + int header_count = m_index.ToInt32 (layout_pos); + var headers = new DdsInfo[header_count]; + layout_pos += 4; + for (int i = 0; i < header_count; ++i) + { + headers[i] = new DdsInfo { + Width = m_index.ToUInt32 (layout_pos+4), + Height = m_index.ToUInt32 (layout_pos+8), + BPP = 32, + Flags = (DdsPF)m_index.ToUInt32 (layout_pos), + }; + layout_pos += 0xC; + } + for (int j = 0; j < count; ++j) + { + var name = ReadFileName (m_index.ToInt32 (name_pos)); + var entry = FormatCatalog.Instance.Create (name); + + entry.Offset = m_index.ToUInt32 (layout_pos) + m_base_offset; + entry.Size = m_index.ToUInt32 (layout_pos+4); + entry.UnpackedSize = m_index.ToUInt32 (layout_pos+0xC); + int header_id = m_index.ToInt32 (layout_pos+0x10); + entry.Info = headers[header_id]; + entry.IsPacked = true; + if (!entry.CheckPlacement (m_max_offset+0x14)) + throw new InvalidFormatException(); + + m_dir.Add (entry); + name_pos += 4; + layout_pos += 0x14; + } + } + + void ReadOvaSection (int name_pos, int layout_pos, int count) + { + int header_count = m_index.ToInt32 (layout_pos+4); + var headers = new byte[header_count][]; + layout_pos += 8; + for (int i = 0; i < header_count; ++i) + { + int header_len = m_index.ToInt32 (layout_pos+8); + int header_pos = layout_pos + 12; + headers[i] = new CowArray (m_index, header_pos, header_len).ToArray(); + layout_pos = header_pos + header_len; + } + for (int j = 0; j < count; ++j) + { + var name = ReadFileName (m_index.ToInt32 (name_pos)); + var entry = FormatCatalog.Instance.Create (name); + + entry.Offset = m_index.ToUInt32 (layout_pos) + m_base_offset; + entry.UnpackedSize = m_index.ToUInt32 (layout_pos+4); + int header_id = m_index.ToInt32 (layout_pos+8); + entry.Header = headers[header_id]; + entry.Size = entry.UnpackedSize - (uint)entry.Header.Length; + entry.IsPacked = true; + if (!entry.CheckPlacement (m_max_offset)) + throw new InvalidFormatException(); + + m_dir.Add (entry); + name_pos += 4; + layout_pos += 0xC; + } + } + + string ReadFileName (int pos) + { + int i; + for (i = pos; i+1 < m_filenames.Length; i += 2) + { + if (m_filenames[i] == 0 && m_filenames[i+1] == 0) + break; + } + return Encoding.Unicode.GetString (m_filenames, pos, i - pos); + } + + byte[] ReadChunk () + { + long start_pos = m_input.Position; + int size = m_input.ReadInt32(); + int type = m_input.ReadInt32(); + int unpacked_size = m_input.ReadInt32(); + if (size <= 0 || unpacked_size <= 0) + throw new InvalidFormatException(); + var data = new byte[unpacked_size]; + using (var decrypted = new XoredStream (m_input.AsStream, 0xFF, true)) + using (var lzss = new LzssStream (decrypted)) + { + lzss.Read (data, 0, data.Length); + } + m_input.Position = start_pos + size; + return data; + } + + #region IDisposable Members + bool _disposed = false; + + public void Dispose () + { + if (!_disposed) + { + m_input.Dispose(); + _disposed = true; + } + GC.SuppressFinalize (this); + } + #endregion + } + + internal sealed class ImageDecoder : IImageDecoder + { + Stream m_input; + ImageData m_image; + + public Stream Source => m_input; + public ImageFormat SourceFormat => null; + public PixelFormat Format { get; private set; } + public ImageMetaData Info { get; private set; } + public ImageData Image => m_image ?? (m_image = GetImageData()); + + public ImageDecoder (Stream input, DdsInfo info) + { + m_input = input; + Info = info; + Format = info.Flags.HasFlag (DdsPF.AlphaPixels) ? PixelFormats.Bgra32 : PixelFormats.Bgr32; + } + + private ImageData GetImageData () + { + var pixels = new byte[Info.iWidth * Info.iHeight * 4]; + m_input.Read (pixels, 0, pixels.Length); + return ImageData.Create (Info, Format, null, pixels); + } + + #region IDisposable members + bool m_disposed = false; + public void Dispose () + { + if (!m_disposed) + { + m_input.Dispose(); + m_disposed = true; + } + GC.SuppressFinalize (this); + } + #endregion + } +}