From 42085dd4689139aea059f24c8ed56ff80bc935b5 Mon Sep 17 00:00:00 2001 From: morkt Date: Thu, 7 Sep 2023 11:50:13 +0400 Subject: [PATCH 01/36] (PSD): moved to adobe folder. --- ArcFormats/{ => Adobe}/ImagePSD.cs | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename ArcFormats/{ => Adobe}/ImagePSD.cs (100%) diff --git a/ArcFormats/ImagePSD.cs b/ArcFormats/Adobe/ImagePSD.cs similarity index 100% rename from ArcFormats/ImagePSD.cs rename to ArcFormats/Adobe/ImagePSD.cs From ae24ef1cdc6b34c7312d398dbc87c0e81f24c06a Mon Sep 17 00:00:00 2001 From: morkt Date: Thu, 7 Sep 2023 11:51:19 +0400 Subject: [PATCH 02/36] (UnpackTpw): made into separate static method. --- ArcFormats/Ankh/ArcGRP.cs | 133 ++++++++++++++++++++------------------ 1 file changed, 71 insertions(+), 62 deletions(-) diff --git a/ArcFormats/Ankh/ArcGRP.cs b/ArcFormats/Ankh/ArcGRP.cs index b4bb0ed2..4dc09bed 100644 --- a/ArcFormats/Ankh/ArcGRP.cs +++ b/ArcFormats/Ankh/ArcGRP.cs @@ -235,76 +235,85 @@ namespace GameRes.Formats.Ankh Stream OpenTpw (ArcFile arc, PackedEntry entry) { - var output = new byte[entry.UnpackedSize]; using (var input = arc.File.CreateStream (entry.Offset, entry.Size)) { - input.Position = 8; - var offsets = new int[4]; - offsets[0] = input.ReadUInt16(); - offsets[1] = offsets[0] * 2; - offsets[2] = offsets[0] * 3; - offsets[3] = offsets[0] * 4; - int dst = 0; - while (dst < output.Length) + var output = new byte[entry.UnpackedSize]; + UnpackTpw (input, output); + return new BinMemoryStream (output, entry.Name); + } + } + + internal static void UnpackTpw (IBinaryStream input, byte[] output) + { + input.Position = 8; + var offsets = new int[4]; + offsets[0] = input.ReadUInt16(); + offsets[1] = offsets[0] * 2; + offsets[2] = offsets[0] * 3; + offsets[3] = offsets[0] * 4; + int dst = 0; + while (dst < output.Length) + { + byte ctl = input.ReadUInt8(); + if (0 == ctl) + break; + int remaining = output.Length - dst; + int count; + if (ctl < 0x40) { - byte ctl = input.ReadUInt8(); - if (0 == ctl) - break; - int count; - if (ctl < 0x40) - { - count = Math.Min (ctl, output.Length - dst); - input.Read (output, dst, count); - dst += count; - } - else if (ctl <= 0x6F) - { - if (0x6F == ctl) - count = input.ReadUInt16(); - else - count = ctl - 0x3D; - byte v = input.ReadUInt8(); - while (count --> 0) - output[dst++] = v; - } - else if (ctl <= 0x9F) - { - if (ctl == 0x9F) - count = input.ReadUInt16(); - else - count = ctl - 0x6E; - byte v1 = input.ReadUInt8(); - byte v2 = input.ReadUInt8(); - while (count --> 0) - { - output[dst++] = v1; - output[dst++] = v2; - } - } - else if (ctl <= 0xBF) - { - if (ctl == 0xBF) - count = input.ReadUInt16(); - else - count = ctl - 0x9E; - input.Read (output, dst, 3); - if (count > 0) - { - count *= 3; - Binary.CopyOverlapped (output, dst, dst+3, count-3); - dst += count; - } - } + count = Math.Min (ctl, remaining); + input.Read (output, dst, count); + dst += count; + } + else if (ctl <= 0x6F) + { + if (0x6F == ctl) + count = input.ReadUInt16(); else + count = (byte)(ctl - 0x3D); + count = Math.Min (count, remaining); + byte v = input.ReadUInt8(); + while (count --> 0) + output[dst++] = v; + } + else if (ctl <= 0x9F) + { + if (ctl == 0x9F) + count = input.ReadUInt16(); + else + count = (byte)(ctl - 0x6E); + dst += input.Read (output, dst, Math.Min (2, remaining)); + --count; + if (count > 0 && remaining > 2) { - count = (ctl & 0x3F) + 3; - int offset = input.ReadUInt8(); - offset = (offset & 0x3F) - offsets[offset >> 6]; - Binary.CopyOverlapped (output, dst+offset, dst, count); + count = Math.Min (count * 2, remaining - 2); + Binary.CopyOverlapped (output, dst-2, dst, count); dst += count; } } - return new BinMemoryStream (output, entry.Name); + else if (ctl <= 0xBF) + { + if (ctl == 0xBF) + count = input.ReadUInt16(); + else + count = (byte)(ctl - 0x9E); + dst += input.Read (output, dst, Math.Min (3, remaining)); + --count; + if (count > 0 && remaining > 3) + { + count = Math.Min (count * 3, remaining - 3); + Binary.CopyOverlapped (output, dst-3, dst, count); + dst += count; + } + } + else + { + count = Math.Min ((ctl & 0x3F) + 3, remaining); + int offset = input.ReadUInt8(); + offset = (offset & 0x3F) - offsets[offset >> 6]; + Binary.CopyOverlapped (output, dst+offset, dst, count); + dst += count; + } } } } From 4e8a4edd4a4650fa8688908748e5ab649508f760 Mon Sep 17 00:00:00 2001 From: morkt Date: Thu, 7 Sep 2023 11:51:48 +0400 Subject: [PATCH 03/36] (ISA): moved to Ism folder. --- ArcFormats/{ => Ism}/ArcISA.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) rename ArcFormats/{ => Ism}/ArcISA.cs (90%) diff --git a/ArcFormats/ArcISA.cs b/ArcFormats/Ism/ArcISA.cs similarity index 90% rename from ArcFormats/ArcISA.cs rename to ArcFormats/Ism/ArcISA.cs index 86f44f7b..40f1d764 100644 --- a/ArcFormats/ArcISA.cs +++ b/ArcFormats/Ism/ArcISA.cs @@ -37,6 +37,11 @@ namespace GameRes.Formats.ISM public override bool IsHierarchic { get { return false; } } public override bool CanWrite { get { return false; } } + public IsaOpener () + { + ContainedFormats = new[] { "ISG", "PNG/ISM", "OGG", }; + } + public override ArcFile TryOpen (ArcView file) { if (!file.View.AsciiEqual (4, "ARCHIVED")) @@ -87,6 +92,13 @@ namespace GameRes.Formats.ISM entry.Size = m_file.View.ReadUInt32 (index_offset+8); if (!entry.CheckPlacement (m_file.MaxOffset)) return null; + if (string.IsNullOrEmpty (entry.Type) && name_length < 0x20) // try to fix truncated extension + { + if (name.EndsWith (".OG")) + entry.Type = "audio"; + else if (name.EndsWith (".PN")) + entry.Type = "image"; + } m_dir.Add (entry); index_offset += record_length; } From 68dae3e40acbd30ec4399cdfbfd066411413a83e Mon Sep 17 00:00:00 2001 From: morkt Date: Thu, 7 Sep 2023 11:59:51 +0400 Subject: [PATCH 04/36] (ISG): moved to Ism folder. --- ArcFormats/{ => Ism}/ImageISG.cs | 195 ++++++++++++++++++++----------- 1 file changed, 127 insertions(+), 68 deletions(-) rename ArcFormats/{ => Ism}/ImageISG.cs (58%) diff --git a/ArcFormats/ImageISG.cs b/ArcFormats/Ism/ImageISG.cs similarity index 58% rename from ArcFormats/ImageISG.cs rename to ArcFormats/Ism/ImageISG.cs index 1fef6d27..420a9f16 100644 --- a/ArcFormats/ImageISG.cs +++ b/ArcFormats/Ism/ImageISG.cs @@ -41,6 +41,7 @@ namespace GameRes.Formats.ISM public int Colors; public uint Packed; public uint Unpacked; + public int RecursionDepth; } [Export(typeof(ImageFormat))] @@ -78,88 +79,46 @@ namespace GameRes.Formats.ISM public override ImageData Read (IBinaryStream stream, ImageMetaData info) { var meta = (IsgMetaData)info; - if (0x21 != meta.Type && 0x10 != meta.Type) + if (0x21 != meta.Type && 0x10 != meta.Type && 0x34 != meta.Type) throw new InvalidFormatException ("Unsupported ISM image type"); - stream.Position = 0x30; - using (var input = new Reader (stream, meta)) - { - if (0x21 == meta.Type) - input.Unpack21(); - else - input.Unpack10(); - var palette = new BitmapPalette (input.Palette); - return ImageData.CreateFlipped (info, PixelFormats.Indexed8, palette, input.Data, (int)info.Width); - } + var input = new Reader (stream, meta); + var pixels = input.Unpack (this); + var palette = new BitmapPalette (input.Palette); + return ImageData.CreateFlipped (info, PixelFormats.Indexed8, palette, input.Data, info.iWidth); } - internal class Reader : IDisposable + internal class Reader { IBinaryStream m_input; byte[] m_data; - Color[] m_palette; int m_input_size; + IsgMetaData m_info; - public Color[] Palette { get { return m_palette; } } + public const int RecursionLimit = 32; // have seen 18 deep + + public Color[] Palette { get; private set; } public byte[] Data { get { return m_data; } } public Reader (IBinaryStream file, IsgMetaData info) { - int palette_size = (int)info.Colors*4; - var palette_data = new byte[Math.Max (0x400, palette_size)]; - if (palette_size != file.Read (palette_data, 0, palette_size)) - throw new InvalidFormatException(); - m_palette = new Color[0x100]; - for (int i = 0; i < m_palette.Length; ++i) - { - m_palette[i] = Color.FromRgb (palette_data[i*4+2], palette_data[i*4+1], palette_data[i*4]); - } m_input = file; m_input_size = (int)info.Packed; - m_data = new byte[info.Width * info.Height]; + m_info = info; } - public void Unpack21 () + public byte[] Unpack (IsgFormat isg) { - int dst = 0; - var frame = new byte[2048]; - int frame_pos = 2039; - int remaining = m_input_size; - byte ctl = m_input.ReadUInt8(); - --remaining; - int bit = 0x80; - while (remaining > 0) - { - if (0 != (ctl & bit)) - { - byte hi = m_input.ReadUInt8(); - byte lo = m_input.ReadUInt8(); - remaining -= 2; - int offset = (hi & 7) << 8 | lo; - for (int count = (hi >> 3) + 3; count > 0; --count) - { - byte p = frame[offset]; - frame[frame_pos] = p; - m_data[dst++] = p; - offset = (offset + 1) & 0x7ff; - frame_pos = (frame_pos + 1) & 0x7ff; - } - } - else - { - byte p = m_input.ReadUInt8(); - --remaining; - m_data[dst++] = p; - frame[frame_pos] = p; - frame_pos = (frame_pos + 1) & 0x7ff; - } - if (0 == (bit >>= 1)) - { - ctl = m_input.ReadUInt8(); - --remaining; - bit = 0x80; - } - } + m_input.Position = 0x30; + if (0x34 == m_info.Type) + return Unpack34(isg); + Palette = ImageFormat.ReadColorMap (m_input.AsStream); + m_data = new byte[m_info.Width * m_info.Height]; + if (0x21 == m_info.Type) + DecompressLzss (m_input_size, m_data); + else + Unpack10(); + return m_data; } public void Unpack10 () @@ -192,12 +151,112 @@ namespace GameRes.Formats.ISM } } - #region IDisposable Members - public void Dispose () + public byte[] Unpack34 (IsgFormat isg) { - GC.SuppressFinalize (this); + if (m_info.RecursionDepth >= RecursionLimit) + throw new InvalidFormatException ("Recursion limit reached for ISG image."); + var base_name = m_input.ReadCString (0x10); + var base_image = ReadBaseImage (base_name, isg); + if (null == base_image) + throw new InvalidFormatException ("Unable to read baseline ISG image."); + int count = m_input.ReadInt32(); + int packed_size = m_input.ReadInt32(); + int ovl_ctl_size = m_info.iWidth * m_info.iHeight / 128; + var overlay_info = m_input.ReadBytes (ovl_ctl_size); + var overlay_data = new byte[count * 32 + 8]; + DecompressLzss (packed_size, overlay_data); + int bit_count = 0; + int ctl_src = 0; + int data_src = 0; + for (int y = 0; y < m_info.iHeight; y += 4) + for (int x = 0; x < m_info.iWidth; x += 4) + { + if (((1 << bit_count) & overlay_info[ctl_src]) != 0) + { + int dst = y * m_info.iWidth + x; + for (int r = 0; r < 4; ++r) + { + base_image[dst ] = overlay_data[data_src++]; + base_image[dst+1] = overlay_data[data_src++]; + base_image[dst+2] = overlay_data[data_src++]; + base_image[dst+3] = overlay_data[data_src++]; + dst += m_info.iWidth; + } + } + if (++bit_count == 8) + { + bit_count = 0; + ++ctl_src; + } + } + return m_data = base_image; + } + + internal void DecompressLzss (int remaining, byte[] output) + { + int dst = 0; + var frame = new byte[2048]; + int frame_pos = 2039; + byte ctl = m_input.ReadUInt8(); + --remaining; + int bit = 0x80; + while (remaining > 0) + { + if (0 != (ctl & bit)) + { + byte hi = m_input.ReadUInt8(); + byte lo = m_input.ReadUInt8(); + remaining -= 2; + int offset = (hi & 7) << 8 | lo; + for (int count = (hi >> 3) + 3; count > 0; --count) + { + byte p = frame[offset]; + frame[frame_pos] = p; + output[dst++] = p; + offset = (offset + 1) & 0x7ff; + frame_pos = (frame_pos + 1) & 0x7ff; + } + } + else + { + byte p = m_input.ReadUInt8(); + --remaining; + output[dst++] = p; + frame[frame_pos] = p; + frame_pos = (frame_pos + 1) & 0x7ff; + } + if (0 == (bit >>= 1)) + { + ctl = m_input.ReadUInt8(); + --remaining; + bit = 0x80; + } + } + } + + internal byte[] ReadBaseImage (string name, IsgFormat isg) + { + if (!VFS.FileExists (name)) + { + if (name.Length <= 12) + return null; + name = name.Substring (0, 12); + if (!VFS.FileExists (name)) + return null; + } + using (var base_file = VFS.OpenBinaryStream (name)) + { + var base_info = isg.ReadMetaData (base_file) as IsgMetaData; + if (null == base_info || base_info.Width != m_info.Width || base_info.Height != m_info.Height) + throw new InvalidFormatException ("Invalid baseline ISG image."); + base_info.RecursionDepth = m_info.RecursionDepth + 1; + base_info.FileName = name; + var reader = new Reader (base_file, base_info); + var pixels = reader.Unpack (isg); + this.Palette = reader.Palette; + return pixels; + } } - #endregion } } } From 98549b47b8e5e339cb60ea16f2baf9e7e035bad5 Mon Sep 17 00:00:00 2001 From: morkt Date: Thu, 7 Sep 2023 12:00:21 +0400 Subject: [PATCH 05/36] (PngIsmFormat): PNG image with inverted alpha-channel. --- ArcFormats/Ism/ImagePNG.cs | 71 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 ArcFormats/Ism/ImagePNG.cs diff --git a/ArcFormats/Ism/ImagePNG.cs b/ArcFormats/Ism/ImagePNG.cs new file mode 100644 index 00000000..cd2c3fcb --- /dev/null +++ b/ArcFormats/Ism/ImagePNG.cs @@ -0,0 +1,71 @@ +//! \file ImagePNG.cs +//! \date 2023 Sep 02 +//! \brief PNG image with inverted alpha-channel. +// +// 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 System.ComponentModel.Composition; +using System.IO; +using System.Windows.Media; +using System.Windows.Media.Imaging; + +namespace GameRes.Formats.Ism +{ + [Export(typeof(ImageFormat))] + [ExportMetadata("Priority", -1)] // deprioritize + public class PngIsmFormat : ImageFormat + { + public override string Tag { get => "PNG/ISM"; } + public override string Description { get => "ISM engine PNG image"; } + public override uint Signature { get => 0x474e5089; } + + public override ImageMetaData ReadMetaData (IBinaryStream file) + { + // format only applied when extracting from related archive + if (!VFS.IsVirtual || VFS.CurrentArchive.Tag != "ISA") + return null; + return Png.ReadMetaData (file); + } + + public override ImageData Read (IBinaryStream file, ImageMetaData info) + { + var decoder = new PngBitmapDecoder (file.AsStream, + BitmapCreateOptions.None, BitmapCacheOption.OnLoad); + BitmapSource bitmap = decoder.Frames[0]; + if (bitmap.Format != PixelFormats.Bgra32) + return new ImageData (bitmap, info); + int stride = bitmap.PixelWidth * 4; + var pixels = new byte[stride * bitmap.PixelHeight]; + bitmap.CopyPixels (pixels, stride, 0); + for (int i = 3; i < pixels.Length; i += 4) + { + pixels[i] ^= 0xFF; + } + return ImageData.Create (info, bitmap.Format, bitmap.Palette, pixels); + } + + public override void Write (Stream file, ImageData image) + { + throw new System.NotImplementedException ("PngIsmFormat.Write not implemented"); + } + } +} From 10ba6e6986c665664fe6bbff3c3aba30401f67df Mon Sep 17 00:00:00 2001 From: morkt Date: Thu, 7 Sep 2023 12:01:40 +0400 Subject: [PATCH 06/36] (ExeFile.ResourceAccessor): added resource enumeration methods. --- ArcFormats/ExeFile.cs | 106 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 95 insertions(+), 11 deletions(-) diff --git a/ArcFormats/ExeFile.cs b/ArcFormats/ExeFile.cs index 2e44ef4c..8c73f48f 100644 --- a/ArcFormats/ExeFile.cs +++ b/ArcFormats/ExeFile.cs @@ -26,6 +26,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Linq; using System.Runtime.InteropServices; using System.Text; using GameRes.Utility; @@ -302,33 +303,99 @@ namespace GameRes.Formats public ResourceAccessor (string filename) { + const uint LOAD_LIBRARY_AS_DATAFILE = 0x02; const uint LOAD_LIBRARY_AS_IMAGE_RESOURCE = 0x20; - m_exe = NativeMethods.LoadLibraryEx (filename, IntPtr.Zero, LOAD_LIBRARY_AS_IMAGE_RESOURCE); + m_exe = NativeMethods.LoadLibraryEx (filename, IntPtr.Zero, LOAD_LIBRARY_AS_DATAFILE|LOAD_LIBRARY_AS_IMAGE_RESOURCE); if (IntPtr.Zero == m_exe) throw new Win32Exception (Marshal.GetLastWin32Error()); } public byte[] GetResource (string name, string type) { - if (m_disposed) - throw new ObjectDisposedException ("Access to disposed ResourceAccessor object failed."); - var res = NativeMethods.FindResource (m_exe, name, type); + var res = FindResource (name, type); if (IntPtr.Zero == res) return null; - var glob = NativeMethods.LoadResource (m_exe, res); - if (IntPtr.Zero == glob) - return null; - uint size = NativeMethods.SizeofResource (m_exe, res); - var src = NativeMethods.LockResource (glob); + var src = LockResource (res); if (IntPtr.Zero == src) return null; - + uint size = NativeMethods.SizeofResource (m_exe, res); var dst = new byte[size]; Marshal.Copy (src, dst, 0, dst.Length); return dst; } + public int ReadResource (string name, string type, byte[] dest, int pos) + { + var res = FindResource (name, type); + if (IntPtr.Zero == res) + return 0; + var src = LockResource (res); + if (IntPtr.Zero == src) + return 0; + int length = (int)NativeMethods.SizeofResource (m_exe, res); + length = Math.Min (dest.Length - pos, length); + Marshal.Copy (src, dest, pos, length); + return length; + } + + public uint GetResourceSize (string name, string type) + { + var res = FindResource (name, type); + if (IntPtr.Zero == res) + return 0; + return NativeMethods.SizeofResource (m_exe, res); + } + + private IntPtr FindResource (string name, string type) + { + if (m_disposed) + throw new ObjectDisposedException ("Access to disposed ResourceAccessor object failed."); + return NativeMethods.FindResource (m_exe, name, type); + } + + private IntPtr LockResource (IntPtr res) + { + var glob = NativeMethods.LoadResource (m_exe, res); + if (IntPtr.Zero == glob) + return IntPtr.Zero; + return NativeMethods.LockResource (glob); + } + + public IEnumerable EnumTypes () + { + var types = new List(); + if (!NativeMethods.EnumResourceTypes (m_exe, (m, t, p) => AddResourceName (types, t), IntPtr.Zero)) + return Enumerable.Empty(); + return types; + } + + public IEnumerable EnumNames (string type) + { + var names = new List(); + if (!NativeMethods.EnumResourceNames (m_exe, type, (m, t, n, p) => AddResourceName (names, n), IntPtr.Zero)) + return Enumerable.Empty(); + return names; + } + + private static bool AddResourceName (List list, IntPtr name) + { + list.Add (ResourceNameToString (name)); + return true; + } + + private static string ResourceNameToString (IntPtr resName) + { + if ((resName.ToInt64() >> 16) == 0) + { + return "#" + resName.ToString(); + } + else + { + return Marshal.PtrToStringUni (resName); + } + } + #region IDisposable implementation bool m_disposed = false; public void Dispose () @@ -363,7 +430,7 @@ namespace GameRes.Formats [return: MarshalAs(UnmanagedType.Bool)] static internal extern bool FreeLibrary (IntPtr hModule); - [DllImport( "kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] static internal extern IntPtr FindResource (IntPtr hModule, string lpName, string lpType); [DllImport("Kernel32.dll", SetLastError = true)] @@ -374,5 +441,22 @@ namespace GameRes.Formats [DllImport("kernel32.dll")] static internal extern IntPtr LockResource (IntPtr hResData); + + internal delegate bool EnumResTypeProc (IntPtr hModule, IntPtr lpszType, IntPtr lParam); + internal delegate bool EnumResNameProc (IntPtr hModule, IntPtr lpszType, IntPtr lpszName, IntPtr lParam); + internal delegate bool EnumResLangProc (IntPtr hModule, IntPtr lpszType, IntPtr lpszName, ushort wIDLanguage, IntPtr lParam); + + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + static internal extern bool EnumResourceTypes (IntPtr hModule, [MarshalAs(UnmanagedType.FunctionPtr)] EnumResTypeProc lpEnumFunc, IntPtr lParam); + + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + static internal extern bool EnumResourceNames (IntPtr hModule, IntPtr lpszType, EnumResNameProc lpEnumFunc, IntPtr lParam); + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + static internal extern bool EnumResourceNames (IntPtr hModule, string lpszType, EnumResNameProc lpEnumFunc, IntPtr lParam); + + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + static internal extern bool EnumResourceLanguages (IntPtr hModule, IntPtr lpszType, string lpName, EnumResLangProc lpEnumFunc, IntPtr lParam); + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + static internal extern bool EnumResourceLanguages (IntPtr hModule, string lpszType, string lpName, EnumResLangProc lpEnumFunc, IntPtr lParam); } } From b6ebb7d45189cd98adf34315a7c53321de3ab24f Mon Sep 17 00:00:00 2001 From: morkt Date: Thu, 7 Sep 2023 12:02:19 +0400 Subject: [PATCH 07/36] (CSAF): support different archive keys. --- ArcFormats/FamilyAdvSystem/ArcCSAF.cs | 31 +++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/ArcFormats/FamilyAdvSystem/ArcCSAF.cs b/ArcFormats/FamilyAdvSystem/ArcCSAF.cs index fefd232a..1af19564 100644 --- a/ArcFormats/FamilyAdvSystem/ArcCSAF.cs +++ b/ArcFormats/FamilyAdvSystem/ArcCSAF.cs @@ -2,7 +2,7 @@ //! \date 2019 Jan 01 //! \brief Family Adv System resource archive. // -// Copyright (C) 2019 by morkt +// Copyright (C) 2019-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 @@ -34,6 +34,11 @@ using GameRes.Utility; namespace GameRes.Formats.FamilyAdvSystem { + [Serializable] + public class FamilyAdvScheme : ResourceScheme + { + public IDictionary KnownKeys; + } [Export(typeof(ArchiveFormat))] public class CsafOpener : ArchiveFormat @@ -70,8 +75,9 @@ namespace GameRes.Formats.FamilyAdvSystem { if (is_encrypted) { + var key = QueryEncryptionKey (file); file.View.Read (0x20, index, 0, index_size); - enc = new CsafEncryption (DefaultKey, DefaultIV); + enc = new CsafEncryption (key, DefaultIV); using (var decryptor = enc.CreateDecryptor (0)) using (var enc_names = file.CreateStream (0x20 + index_size, names_size)) using (var dec_names = new InputCryptoStream (enc_names, decryptor)) @@ -136,6 +142,27 @@ namespace GameRes.Formats.FamilyAdvSystem var input = new CsafStream (carc); return new StreamRegion (input, entry.Offset, entry.Size); } + + internal string QueryEncryptionKey (ArcView file) + { + var title = FormatCatalog.Instance.LookupGame (file.Name); + if (string.IsNullOrEmpty (title)) + return DefaultKey; + string key; + if (!KnownKeys.TryGetValue (title, out key)) + return DefaultKey; + return key; + } + + FamilyAdvScheme DefaultScheme = new FamilyAdvScheme { KnownKeys = new Dictionary() }; + + internal IDictionary KnownKeys { get { return DefaultScheme.KnownKeys; } } + + public override ResourceScheme Scheme + { + get { return DefaultScheme; } + set { DefaultScheme = (FamilyAdvScheme)value; } + } } internal class CsafEncryption : IDisposable From b7c4c91fa4a50adfc1d05f2a8fe68fe8fd3ec751 Mon Sep 17 00:00:00 2001 From: morkt Date: Thu, 7 Sep 2023 12:03:23 +0400 Subject: [PATCH 08/36] (GScripter): added archive format. --- ArcFormats/GScripter/ArcDATA.cs | 80 +++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 ArcFormats/GScripter/ArcDATA.cs diff --git a/ArcFormats/GScripter/ArcDATA.cs b/ArcFormats/GScripter/ArcDATA.cs new file mode 100644 index 00000000..cf5065c6 --- /dev/null +++ b/ArcFormats/GScripter/ArcDATA.cs @@ -0,0 +1,80 @@ +//! \file ArcDATA.cs +//! \date 2023 Sep 06 +//! \brief GScripter 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 System; +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.IO; + +namespace GameRes.Formats.GScripter +{ + [Export(typeof(ArchiveFormat))] + public class DataOpener : ArchiveFormat + { + public override string Tag { get => "DAT/GScripter"; } + public override string Description { get => "GScripter engine resource archive"; } + public override uint Signature { get => 0; } + public override bool IsHierarchic { get => false; } + public override bool CanWrite { get => false; } + + public DataOpener () + { + Extensions = new[] { "" }; + } + + public override ArcFile TryOpen (ArcView file) + { + var info_name = file.Name + ".info"; + if (!VFS.FileExists (info_name)) + return null; + using (var index = VFS.OpenView (info_name)) + { + int count = (int)index.MaxOffset / 0x28; + if (!IsSaneCount (count) || count * 0x28 != index.MaxOffset) + return null; + var arc_name = Path.GetFileName (file.Name); + bool is_cg = arc_name.StartsWith ("CG"); + bool is_sound = !is_cg && arc_name.StartsWith ("SOUND"); + uint index_pos = 0; + var dir = new List (count); + for (int i = 0; i < count; ++i) + { + var name = index.View.ReadString (index_pos, 0x20); + var entry = new Entry { + Name = name, + Type = is_cg ? "image" : is_sound ? "audio" : "", + Offset = index.View.ReadUInt32 (index_pos+0x20), + Size = index.View.ReadUInt32 (index_pos+0x24), + }; + if (!entry.CheckPlacement (file.MaxOffset)) + return null; + dir.Add (entry); + index_pos += 0x28; + } + return new ArcFile (file, this, dir); + } + } + } +} From 323dcd7860660a656b6193eae14d07b9571ecbec Mon Sep 17 00:00:00 2001 From: morkt Date: Thu, 7 Sep 2023 12:04:37 +0400 Subject: [PATCH 09/36] (PICT): implemented macintosh image format. --- ArcFormats/Macintosh/ImagePICT.cs | 436 ++++++++++++++++++++++++++++++ 1 file changed, 436 insertions(+) create mode 100644 ArcFormats/Macintosh/ImagePICT.cs diff --git a/ArcFormats/Macintosh/ImagePICT.cs b/ArcFormats/Macintosh/ImagePICT.cs new file mode 100644 index 00000000..d332afa1 --- /dev/null +++ b/ArcFormats/Macintosh/ImagePICT.cs @@ -0,0 +1,436 @@ +//! \file ImagePICT.cs +//! \date 2023 Aug 24 +//! \brief Macintosh picture format. +// +// 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.Utility; +using System; +using System.ComponentModel.Composition; +using System.IO; +using System.Windows.Media; +using System.Windows.Media.Imaging; + +namespace GameRes.Formats.Apple +{ + internal class PictMetaData : ImageMetaData + { + public uint DataOffset; + } + + [Export(typeof(ImageFormat))] + public class PictFormat : ImageFormat + { + public override string Tag { get => "PICT/MAC"; } + public override string Description { get => "Apple Macintosh image format"; } + public override uint Signature { get => 0; } + + public PictFormat () + { + Signatures = new[] { 0u, 0x54434950u }; + Extensions = new[] { "pct", "pict", "pic" }; + } + + public override ImageMetaData ReadMetaData (IBinaryStream file) + { + int header_pos = 0x200; + if (file.Signature == 0x54434950) // 'PICT' + header_pos = 4; + if (file.Length < header_pos + 0x10) + return null; + file.Position = header_pos + 2; + short top = file.ReadI16BE(); + short left = file.ReadI16BE(); + short bottom = file.ReadI16BE(); + short right = file.ReadI16BE(); + if (file.ReadU16BE() != 0x11) + return null; + int version = file.ReadU16BE(); + if (version != 0x2FF) + return null; + return new PictMetaData { + Width = (uint)(right - left), + Height = (uint)(bottom - top), + OffsetX = left, + OffsetY = top, + BPP = 32, + DataOffset = (uint)file.Position, + }; + } + + public override ImageData Read (IBinaryStream file, ImageMetaData info) + { + var decoder = new PictReader (file, (PictMetaData)info); + var pixels = decoder.Unpack(); + return ImageData.Create (info, decoder.Format, decoder.Palette, pixels); + } + + public override void Write (Stream file, ImageData image) + { + throw new System.NotImplementedException ("PictFormat.Write not implemented"); + } + } + + internal static class BinaryStreamExtension + { + static public short ReadI16BE (this IBinaryStream file) + { + return Binary.BigEndian (file.ReadInt16()); + } + + static public ushort ReadU16BE (this IBinaryStream file) + { + return Binary.BigEndian (file.ReadUInt16()); + } + + static public int ReadI32BE (this IBinaryStream file) + { + return Binary.BigEndian (file.ReadInt32()); + } + + static public uint ReadU32BE (this IBinaryStream file) + { + return Binary.BigEndian (file.ReadUInt32()); + } + } + + internal class Pixmap + { + public short Version; + public short PackType; + public int PackSize; + public int HorizRes; + public int VertRes; + public short PixelType; + public short BPP; + public short CompCount; + public short CompSize; + public int PlaneBytes; + public int Table; + + public void Deserialize (IBinaryStream input) + { + Version = input.ReadI16BE(); + PackType = input.ReadI16BE(); + PackSize = input.ReadI32BE(); + HorizRes = input.ReadI32BE() >> 16; // read 2 bytes and skip next 2 + VertRes = input.ReadI32BE() >> 16; + PixelType = input.ReadI16BE(); + BPP = input.ReadI16BE(); + CompCount = input.ReadI16BE(); + CompSize = input.ReadI16BE(); + PlaneBytes = input.ReadI32BE(); + Table = input.ReadI32BE(); + input.Seek (4, SeekOrigin.Current); + if (BPP <= 0 || BPP > 32 || CompCount <= 0 || CompCount > 4 || CompSize <= 0) + throw new InvalidFormatException(); + } + } + + internal class PictReader + { + IBinaryStream m_input; + PictMetaData m_info; + + public PixelFormat Format { get; private set; } + public BitmapPalette Palette { get; private set; } + + public PictReader (IBinaryStream input, PictMetaData info) + { + m_input = input; + m_info = info; + } + + bool HasAlpha = false; + byte[] m_buffer; + + public byte[] Unpack () + { + Color[] colormap = null; + Pixmap pixmap = null; + m_input.Position = m_info.DataOffset; + + while (m_input.PeekByte() != -1) + { + if ((m_input.Position & 1) != 0) + Skip (1); + int code = m_input.ReadU16BE(); + if (0x00FF == code || 0xFFFF == code) // EOF + break; + switch (code) + { + case 0x0000: // NOP + continue; + + case 0x0001: // Clip + { + int length = m_input.ReadU16BE(); + if (length < 2) + throw new InvalidFormatException(); + Skip (length-2); + break; + } + + case 0x0090: + case 0x0091: + case 0x0098: + case 0x0099: + case 0x009A: + case 0x009B: // BitsRect + { + int stride = 0; + if (code != 0x9A && code != 0x9B) + stride = m_input.ReadU16BE(); + else + Skip (6); + // FIXME we just read the first bitmap and override an existing frame + // TODO place bitmap into frame according to its RECT + m_info.OffsetY = m_input.ReadI16BE(); + m_info.OffsetX = m_input.ReadI16BE(); + m_info.Height = (uint)(m_input.ReadI16BE() - m_info.OffsetY); + m_info.Width = (uint)(m_input.ReadI16BE() - m_info.OffsetX); + if (0x9A == code || 0x9B == code || (stride & 0x8000) != 0) + { + pixmap = new Pixmap(); + pixmap.Deserialize (m_input); + HasAlpha = pixmap.CompCount == 4; + } + if (code != 0x9A && code != 0x9B) + { + int colors = 2; + int flags = 0; + if ((stride & 0x8000) != 0) + { + Skip (4); + flags = m_input.ReadU16BE(); + colors = m_input.ReadU16BE() + 1; + } + if (null == colormap) + colormap = new Color[colors]; + if ((stride & 0x8000) != 0) + { + for (int i = 0; i < colors; i++) + { + int c = m_input.ReadU16BE() % colors; + if ((flags & 0x8000) != 0) + c = i; + int r = m_input.ReadU16BE() / 0x101; + int g = m_input.ReadU16BE() / 0x101; + int b = m_input.ReadU16BE() / 0x101; + colormap[c] = Color.FromRgb ((byte)r, (byte)g, (byte)b); + } + } + else + { + var White = Color.FromRgb (0xFF, 0xFF, 0xFF); + for (int i = 0; i < colors; i++) + { + colormap[i] = Color.Subtract (White, colormap[i]); + } + } + } + Skip (8+8+2); + // -> Skip (8); // source RECT + // Skip (8); // destination RECT + // Skip (2); // transfer mode + if (code == 0x91 || code == 0x99 || code == 0x9b) + { + int length = m_input.ReadU16BE(); + if (length > 2) + Skip (length - 2); + } + if (code != 0x9A && code != 0x9B && (stride & 0x8000) == 0) + DecodeRleBitmap (stride, 1); + else + DecodeRleBitmap (stride, pixmap.BPP); + break; + } + + case 0x00A1: // LongComment + { + m_input.ReadU16BE(); // comment type + int length = m_input.ReadU16BE(); + Skip (length); + break; + } + case 0x0C00: // Header + Skip (0x18); + break; + + default: + throw new NotSupportedException (string.Format ("Unknown code 0x{0:X4} in PICT stream.", code)); + } + } + if (colormap != null) + Palette = new BitmapPalette (colormap); + if (null == m_buffer) + throw new InvalidFormatException(); + SetFormat (pixmap); + return RepackPixels (pixmap); + } + + byte[] RepackPixels (Pixmap pixmap) + { + int bpp = m_info.BPP; + if (bpp <= 16) + return m_buffer; + int bytes_per_pixel = bpp / 8; + int stride = m_info.iWidth * bytes_per_pixel; + var pixels = new byte[stride * m_info.iHeight]; + int src = 0; + for (int y = 0; y < m_info.iHeight; ++y) + { + int dst = y * stride; + for (int x = 0; x < m_info.iWidth; ++x) + { + if (HasAlpha) + { + pixels[dst+3] = m_buffer[src]; + pixels[dst+2] = m_buffer[src+m_info.iWidth]; + pixels[dst+1] = m_buffer[src+m_info.iWidth*2]; + pixels[dst] = m_buffer[src+m_info.iWidth*3]; + } + else + { + pixels[dst+2] = m_buffer[src]; + pixels[dst+1] = m_buffer[src+m_info.iWidth]; + pixels[dst] = m_buffer[src+m_info.iWidth*2]; + } + ++src; + dst += bytes_per_pixel; + } + src += (pixmap.CompCount - 1) * m_info.iWidth; + } + return pixels; + } + + void SetFormat (Pixmap pixmap) + { + int bpp = null == pixmap ? 8 : pixmap.BPP; + if (32 == bpp) + { + if (4 == pixmap.CompCount) + Format = PixelFormats.Bgra32; + else + Format = PixelFormats.Bgr32; + } + else if (24 == bpp) + Format = PixelFormats.Bgr24; + else if (16 == bpp) + Format = PixelFormats.Bgr555; + else if (8 == bpp) + { + if (Palette != null) + Format = PixelFormats.Indexed8; + else + Format = PixelFormats.Gray8; + } + else + throw new NotSupportedException (string.Format ("Not supported PICT bitdepth -- {0}bpp", bpp)); + m_info.BPP = bpp; + } + + void Skip (int amount) + { + m_input.Seek (amount, SeekOrigin.Current); + } + + byte[] m_unpack_buffer = new byte[0x800]; + byte[] m_scanline; + + void DecodeRleBitmap (int stride, int bpp) + { + if (bpp < 8) + throw new NotSupportedException(); + if (bpp <= 8) + stride &= 0x7fff; + int width = m_info.iWidth; + int bytes_per_pixel = 1; + if (16 == bpp) + { + bytes_per_pixel = 2; + width *= 2; + } + else if (32 == bpp) + width *= HasAlpha ? 4 : 3; + if (stride == 0) + stride = width; + int stride_32bpp = m_info.iWidth * 4; + + int total_bytes = stride_32bpp * m_info.iHeight; + if (null == m_buffer || m_buffer.Length < total_bytes) + m_buffer = new byte[total_bytes]; + int scanline_length = stride_32bpp * 2; + if (null == m_scanline || m_scanline.Length < scanline_length) + { + m_scanline = new byte[scanline_length]; + } + if (stride < 8) + { + int dst = 0; + int row_size = width * (bpp / 8); + for (int y = 0; y < m_info.iHeight; ++y) + { + m_input.Read (m_buffer, dst, stride); + dst += row_size; + } + return; + } + for (int y = 0; y < m_info.iHeight; ++y) + { + int dst = y * width; + if (stride > 200) + scanline_length = m_input.ReadU16BE(); + else + scanline_length = m_input.ReadUInt8(); + if (scanline_length >= m_scanline.Length || scanline_length == 0) + throw new InvalidFormatException(); + m_input.Read (m_scanline, 0, scanline_length); + for (int j = 0; j < scanline_length; ) + { + if ((m_scanline[j] & 0x80) == 0) + { + int pixel_count = m_scanline[j] + 1; + int count = pixel_count * bytes_per_pixel; + int src = j + 1; + if ((dst + count) <= total_bytes) + Buffer.BlockCopy (m_scanline, src, m_buffer, dst, count); + dst += count; + j += count + 1; + } + else + { + int count = (m_scanline[j] ^ 0xFF) + 2; + int src = j + 1; + while (count --> 0) + { + if ((dst + bytes_per_pixel) <= total_bytes) + Buffer.BlockCopy (m_scanline, src, m_buffer, dst, bytes_per_pixel); + dst += bytes_per_pixel; + } + j += bytes_per_pixel + 1; + } + } + } + } + } +} From bc7c143c515e15cdfea0c5b6a259c7fffdb27e06 Mon Sep 17 00:00:00 2001 From: morkt Date: Thu, 7 Sep 2023 12:05:25 +0400 Subject: [PATCH 10/36] (DXR): support 'ediM' audio casts and bitmaps with alpha-channel. --- ArcFormats/Macromedia/ArcDXR.cs | 64 ++++++++++++++++++++++++++++----- 1 file changed, 56 insertions(+), 8 deletions(-) diff --git a/ArcFormats/Macromedia/ArcDXR.cs b/ArcFormats/Macromedia/ArcDXR.cs index ae043c3d..38e7af2d 100644 --- a/ArcFormats/Macromedia/ArcDXR.cs +++ b/ArcFormats/Macromedia/ArcDXR.cs @@ -51,7 +51,7 @@ namespace GameRes.Formats.Macromedia } internal static readonly HashSet RawChunks = new HashSet { - "RTE0", "RTE1", "FXmp", "VWFI", "VWSC", "Lscr", "STXT", + "RTE0", "RTE1", "FXmp", "VWFI", "VWSC", "Lscr", "STXT", "XMED", "snd " }; internal bool ConvertText = true; @@ -73,9 +73,11 @@ namespace GameRes.Formats.Macromedia ImportMedia (dir_file, dir); foreach (MemoryMapEntry entry in dir_file.MMap.Dir) { - if (RawChunks.Contains (entry.FourCC)) + if (entry.Size != 0 && RawChunks.Contains (entry.FourCC)) { entry.Name = string.Format ("{0:D6}.{1}", entry.Id, entry.FourCC.Trim()); + if ("snd " == entry.FourCC) + entry.Type = "audio"; dir.Add (entry); } } @@ -98,6 +100,8 @@ namespace GameRes.Formats.Macromedia internal Stream OpenSound (ArcFile arc, SoundEntry entry) { + if (null == entry.Header) + return base.OpenEntry (arc, entry); var header = arc.File.View.ReadBytes (entry.Header.Offset, entry.Header.Size); var format = entry.DeserializeHeader (header); var riff = new MemoryStream (0x2C); @@ -147,11 +151,30 @@ namespace GameRes.Formats.Macromedia Entry ImportSound (CastMember sound, DirectorFile dir_file) { - var sndHrec = dir_file.KeyTable.FindByCast (sound.Id, "sndH"); - var sndSrec = dir_file.KeyTable.FindByCast (sound.Id, "sndS"); + var name = sound.Info.Name; + KeyTableEntry sndHrec = null, sndSrec = null; + foreach (var elem in dir_file.KeyTable.Table.Where (e => e.CastId == sound.Id)) + { + if ("ediM" == elem.FourCC) + { + var ediM = dir_file.MMap[elem.Id]; + if (string.IsNullOrEmpty (name)) + name = ediM.Id.ToString ("D6"); + return new Entry + { + Name = name + ".mp3", + Type = "audio", + Offset = ediM.Offset, + Size = ediM.Size, + }; + } + if (null == sndHrec && "sndH" == elem.FourCC) + sndHrec = elem; + else if (null == sndSrec && "sndS" == elem.FourCC) + sndSrec = elem; + } if (sndHrec == null || sndSrec == null) return null; - var name = sound.Info.Name; var sndH = dir_file.MMap[sndHrec.Id]; var sndS = dir_file.MMap[sndSrec.Id]; if (string.IsNullOrEmpty (name)) @@ -168,7 +191,15 @@ namespace GameRes.Formats.Macromedia Entry ImportBitmap (CastMember bitmap, DirectorFile dir_file, Cast cast) { - var bitd = dir_file.KeyTable.FindByCast (bitmap.Id, "BITD"); +// var bitd = dir_file.KeyTable.FindByCast (bitmap.Id, "BITD"); + KeyTableEntry bitd = null, alfa = null; + foreach (var elem in dir_file.KeyTable.Table.Where (e => e.CastId == bitmap.Id)) + { + if (null == bitd && "BITD" == elem.FourCC) + bitd = elem; + else if (null == alfa && "ALFA" == elem.FourCC) + alfa = elem; + } if (bitd == null) return null; var entry = new BitmapEntry(); @@ -188,6 +219,8 @@ namespace GameRes.Formats.Macromedia if (clut != null) entry.PaletteRef = dir_file.MMap[clut.Id]; } + if (alfa != null) + entry.AlphaRef = dir_file.MMap[alfa.Id]; return entry; } @@ -214,13 +247,27 @@ namespace GameRes.Formats.Macromedia case -101: palette = Palettes.SystemWindows; break; } } - var input = arc.File.CreateStream (entry.Offset, entry.Size); var info = new ImageMetaData { Width = (uint)(bent.Right - bent.Left), Height = (uint)(bent.Bottom - bent.Top), BPP = bent.BitDepth }; - return new BitdDecoder (input.AsStream, info, palette); + byte[] alpha_channel = null; + if (bent.AlphaRef != null) + { + using (var alpha = arc.File.CreateStream (bent.AlphaRef.Offset, bent.AlphaRef.Size)) + { + var alpha_info = new ImageMetaData { + Width = info.Width, + Height = info.Height, + BPP = 8, + }; + var decoder = new BitdDecoder (alpha, alpha_info, null); + alpha_channel = decoder.Unpack8bpp(); + } + } + var input = arc.File.CreateStream (entry.Offset, entry.Size); + return new BitdDecoder (input.AsStream, info, palette) { AlphaChannel = alpha_channel }; } BitmapPalette ReadPalette (byte[] data) @@ -246,6 +293,7 @@ namespace GameRes.Formats.Macromedia public int BitDepth; public int Palette; public Entry PaletteRef; + public Entry AlphaRef; public void DeserializeHeader (byte[] data) { From d396fd2394e8ec6481f318e1d9a7632852b470d5 Mon Sep 17 00:00:00 2001 From: morkt Date: Thu, 7 Sep 2023 12:07:12 +0400 Subject: [PATCH 11/36] (SWF): fixed rect dimensions order. --- ArcFormats/Macromedia/ArcSWF.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ArcFormats/Macromedia/ArcSWF.cs b/ArcFormats/Macromedia/ArcSWF.cs index 3e9dc59d..abba37d1 100644 --- a/ArcFormats/Macromedia/ArcSWF.cs +++ b/ArcFormats/Macromedia/ArcSWF.cs @@ -330,8 +330,8 @@ namespace GameRes.Formats.Macromedia { int rsize = m_bits.GetBits (5); m_dim.X = GetSignedBits (rsize); + m_dim.Width = GetSignedBits (rsize) - m_dim.X; m_dim.Y = GetSignedBits (rsize); - m_dim.Width = GetSignedBits (rsize) - m_dim.X; m_dim.Height = GetSignedBits (rsize) - m_dim.Y; } From 4aeec4f6a4add8d5e9d869f13abf8f940b51e1dd Mon Sep 17 00:00:00 2001 From: morkt Date: Thu, 7 Sep 2023 12:08:16 +0400 Subject: [PATCH 12/36] (SND): audio from Macromedia Director casts. --- ArcFormats/Macromedia/AudioSND.cs | 112 ++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 ArcFormats/Macromedia/AudioSND.cs diff --git a/ArcFormats/Macromedia/AudioSND.cs b/ArcFormats/Macromedia/AudioSND.cs new file mode 100644 index 00000000..f8ba395f --- /dev/null +++ b/ArcFormats/Macromedia/AudioSND.cs @@ -0,0 +1,112 @@ +//! \file AudioSND.cs +//! \date 2023 Aug 30 +//! \brief Macromedia Director WAVE audio resource. +// +// 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 System; +using System.ComponentModel.Composition; + +namespace GameRes.Formats.Macromedia +{ + [Export(typeof(AudioFormat))] + public class SndAudio : AudioFormat + { + public override string Tag { get => "SND"; } + public override string Description { get => "Macromedia Director audio resource"; } + public override uint Signature { get => 0; } + public override bool CanWrite { get => false; } + + public override SoundInput TryOpen (IBinaryStream file) + { + if (!file.Name.HasExtension (".snd")) + return null; + int type = file.ReadUInt16(); + if (type != 0x0200) + return null; + var reader = new Reader (file.AsStream, ByteOrder.BigEndian); + reader.Skip (2); + int count = reader.ReadU16(); + if (0 == count) + return null; + int command = reader.ReadU16(); + if (command != 0x8051) + return null; + reader.ReadI16(); + int pos = reader.ReadI32(); + if (pos != reader.Position) + return null; + ushort channels = 1; + ushort bps = 8; + reader.Skip (4); + int param = reader.ReadI32(); + ushort sample_rate = reader.ReadU16(); + reader.Skip (10); + byte encoding = reader.ReadU8(); + byte freq = reader.ReadU8(); + if (freq != 0x3C) + return null; + int frames_count = 0; + if (0 == encoding) + { + frames_count = param / channels; + } + else if (0xFF == encoding) + { + channels = (ushort)param; + frames_count = reader.ReadI32(); + reader.Skip (22); + bps = reader.ReadU16(); + reader.Skip (14); + } + else + throw new NotSupportedException (string.Format ("Not supported 'snd' encoding {0:X2}", encoding)); + if (bps != 16 && bps != 8) + return null; + + var format = new WaveFormat { + FormatTag = 1, + Channels = channels, + SamplesPerSecond = sample_rate, + BlockAlign = (ushort)(bps / 8), + BitsPerSample = bps, + }; + format.SetBPS(); + if (8 == bps) + { + var data = new StreamRegion (file.AsStream, file.Position); + return new RawPcmInput (data, format); + } + int sample_count = frames_count * channels; + var samples = file.ReadBytes (sample_count); + for (int i = 1; i < samples.Length; i += 2) + { + byte s = samples[i-1]; + samples[i-1] = samples[i]; + samples[i] = s; + } + var raw = new BinMemoryStream (samples); + file.Dispose(); + return new RawPcmInput (raw, format); + } + } +} From 7f347c5e465200929b411c2c0c52bc94293d52eb Mon Sep 17 00:00:00 2001 From: morkt Date: Thu, 7 Sep 2023 12:08:49 +0400 Subject: [PATCH 13/36] (BITD): support alpha channel. --- ArcFormats/Macromedia/ImageBITD.cs | 47 ++++++++++++++++++++++++++---- 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/ArcFormats/Macromedia/ImageBITD.cs b/ArcFormats/Macromedia/ImageBITD.cs index 2212f3be..e75828b9 100644 --- a/ArcFormats/Macromedia/ImageBITD.cs +++ b/ArcFormats/Macromedia/ImageBITD.cs @@ -215,11 +215,12 @@ namespace GameRes.Formats.Macromedia ImageData m_image; BitmapPalette m_palette; - public Stream Source { get => m_input; } - public ImageFormat SourceFormat { get => null; } - public ImageMetaData Info { get => m_info; } - public ImageData Image { get => m_image ?? (m_image = GetImageData()); } + public Stream Source => m_input; + public ImageFormat SourceFormat => null; + public ImageMetaData Info => m_info; + public ImageData Image => m_image ?? (m_image = GetImageData()); public PixelFormat Format { get; private set; } + public byte[] AlphaChannel { get; set; } public BitdDecoder (Stream input, ImageMetaData info, BitmapPalette palette) { @@ -230,7 +231,8 @@ namespace GameRes.Formats.Macromedia m_stride = (m_width * m_info.BPP + 7) / 8; m_stride = (m_stride + 1) & ~1; m_output = new byte[m_stride * m_height]; - Format = info.BPP == 4 ? PixelFormats.Indexed4 + Format = info.BPP == 2 ? PixelFormats.Indexed2 + : info.BPP == 4 ? PixelFormats.Indexed4 : info.BPP == 8 ? PixelFormats.Indexed8 : info.BPP == 16 ? PixelFormats.Bgr555 : PixelFormats.Bgr32; @@ -239,14 +241,46 @@ namespace GameRes.Formats.Macromedia protected ImageData GetImageData () { + m_input.Position = 0; if (Info.BPP <= 8) Unpack8bpp(); else UnpackChannels (Info.BPP / 8); + if (AlphaChannel != null) + { + if (Info.BPP != 32) + { + BitmapSource bitmap = BitmapSource.Create (Info.iWidth, Info.iHeight, ImageData.DefaultDpiX, ImageData.DefaultDpiY, Format, m_palette, m_output, m_stride); + bitmap = new FormatConvertedBitmap (bitmap, PixelFormats.Bgr32, null, 0); + m_stride = bitmap.PixelWidth * 4; + m_output = new byte[bitmap.PixelHeight * m_stride]; + bitmap.CopyPixels (m_output, m_stride, 0); + } + ApplyAlphaChannel (AlphaChannel); + Format = PixelFormats.Bgra32; + } return ImageData.Create (m_info, Format, m_palette, m_output, m_stride); } - void Unpack8bpp () + void ApplyAlphaChannel (byte[] alpha) + { + int alpha_stride = (m_width + 1) & ~1; + int src = 0; + int pdst = 3; + for (int y = 0; y < m_height; ++y) + { + int dst = pdst; + for (int x = 0; x < m_width; ++x) + { + m_output[dst] = alpha[src+x]; + dst += 4; + } + src += alpha_stride; + pdst += m_stride; + } + } + + public byte[] Unpack8bpp () { for (int line = 0; line < m_output.Length; line += m_stride) { @@ -277,6 +311,7 @@ namespace GameRes.Formats.Macromedia } } } + return m_output; } public void UnpackChannels (int channels) From c5a8b04937133ce6c257baa0f27d9719ff86e9e5 Mon Sep 17 00:00:00 2001 From: morkt Date: Thu, 7 Sep 2023 12:09:23 +0400 Subject: [PATCH 14/36] (IBM): Ice Soft compressed bitmap. --- ArcFormats/Ice/ImageIBM.cs | 88 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 ArcFormats/Ice/ImageIBM.cs diff --git a/ArcFormats/Ice/ImageIBM.cs b/ArcFormats/Ice/ImageIBM.cs new file mode 100644 index 00000000..efd7e976 --- /dev/null +++ b/ArcFormats/Ice/ImageIBM.cs @@ -0,0 +1,88 @@ +//! \file ImageIBM.cs +//! \date 2023 Aug 25 +//! \brief Ice Soft compressed bitmap format. +// +// 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 System.ComponentModel.Composition; +using System.IO; +using System.Windows.Media.Imaging; +using GameRes.Formats.Ankh; + +// [000324][Juice] Orgel ~Kesenai Melody~ + +namespace GameRes.Formats.Ice +{ + internal class IbmMetaData : ImageMetaData + { + public int UnpackedSize; + } + + [Export(typeof(ImageFormat))] + public class IbmFormat : ImageFormat + { + public override string Tag { get => "IBM/ICE"; } + public override string Description { get => "Ice Soft compressed bitmap"; } + public override uint Signature { get => 0x01575054; } // 'TPW' + + public override ImageMetaData ReadMetaData (IBinaryStream file) + { + file.Position = 4; + int unpacked_size = file.ReadInt32(); + if (unpacked_size <= 0) + return null; + var bmp_header = new byte[56]; + GrpOpener.UnpackTpw (file, bmp_header); + using (var bmp = new BinMemoryStream (bmp_header, file.Name)) + { + var bmp_info = Bmp.ReadMetaData (bmp); + if (null == bmp_info) + return null; + return new IbmMetaData { + Width = bmp_info.Width, + Height = bmp_info.Height, + BPP = bmp_info.BPP, + UnpackedSize = unpacked_size, + }; + } + } + + public override ImageData Read (IBinaryStream file, ImageMetaData info) + { + var meta = (IbmMetaData)info; + var output = new byte[meta.UnpackedSize]; + GrpOpener.UnpackTpw (file, output); + using (var bmp = new BinMemoryStream (output, file.Name)) + { + var decoder = new BmpBitmapDecoder (bmp, BitmapCreateOptions.None, BitmapCacheOption.OnLoad); + var frame = decoder.Frames[0]; + frame.Freeze(); + return new ImageData (frame, info); + } + } + + public override void Write (Stream file, ImageData image) + { + throw new System.NotImplementedException ("IbmFormat.Write not implemented"); + } + } +} From 9c36a800a39e03e473d55c713528afd426c25505 Mon Sep 17 00:00:00 2001 From: morkt Date: Thu, 7 Sep 2023 12:10:20 +0400 Subject: [PATCH 15/36] (ISD): Ice Soft compressed script. --- ArcFormats/Ice/ScriptISD.cs | 58 +++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 ArcFormats/Ice/ScriptISD.cs diff --git a/ArcFormats/Ice/ScriptISD.cs b/ArcFormats/Ice/ScriptISD.cs new file mode 100644 index 00000000..49eaf1f0 --- /dev/null +++ b/ArcFormats/Ice/ScriptISD.cs @@ -0,0 +1,58 @@ +//! \file ScriptISD.cs +//! \date 2023 Aug 25 +//! \brief Ice Soft compressed binary script. +// +// 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.Formats.Ankh; +using System.ComponentModel.Composition; +using System.IO; + +namespace GameRes.Formats.Ice +{ + [Export(typeof(ScriptFormat))] + public class IsdScript : GenericScriptFormat + { + public override string Tag { get => "ISD"; } + public override string Description { get => "Ice Soft binary script"; } + public override uint Signature { get => 0x01575054; } // 'TPW' + + public override bool IsScript (IBinaryStream file) + { + return file.Signature == Signature; + } + + public override Stream ConvertFrom (IBinaryStream file) + { + file.Position = 4; + int unpacked_size = file.ReadInt32(); + var data = new byte[unpacked_size]; + GrpOpener.UnpackTpw (file, data); + return new BinMemoryStream (data, file.Name); + } + + public override Stream ConvertBack (IBinaryStream file) + { + throw new System.NotImplementedException(); + } + } +} From 11610a7f9c863ddfc397a6d7d4582740d361dab8 Mon Sep 17 00:00:00 2001 From: morkt Date: Thu, 7 Sep 2023 12:12:19 +0400 Subject: [PATCH 16/36] (MB): recognize 'CL' signatures as well. --- ArcFormats/ImageMB.cs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/ArcFormats/ImageMB.cs b/ArcFormats/ImageMB.cs index b4539992..39925bdc 100644 --- a/ArcFormats/ImageMB.cs +++ b/ArcFormats/ImageMB.cs @@ -36,12 +36,26 @@ namespace GameRes.Formats public override uint Signature { get { return 0; } } public override bool CanWrite { get { return true; } } + public MbImageFormat () + { + Extensions = new[] { "bmp", "gra" }; + } + public override ImageMetaData ReadMetaData (IBinaryStream stream) { int c1 = stream.ReadByte(); int c2 = stream.ReadByte(); - if ('M' != c1 || ('B' != c2 && 'C' != c2)) - return null; + switch (c1) + { + case 'M': + if ('B' != c2 && 'C' != c2) + return null; + break; + case 'C': + if ('L' != c2) + return null; + break; + } using (var bmp = OpenAsBitmap (stream)) return Bmp.ReadMetaData (bmp); } From b8095c33bab0dfb28b05c4a7380f1ca91fb56e23 Mon Sep 17 00:00:00 2001 From: morkt Date: Thu, 7 Sep 2023 12:13:32 +0400 Subject: [PATCH 17/36] (EPF): support images with less than 8 bpp. --- ArcFormats/Interheart/ImageCandy.cs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/ArcFormats/Interheart/ImageCandy.cs b/ArcFormats/Interheart/ImageCandy.cs index 6cab7fbe..b4e10d60 100644 --- a/ArcFormats/Interheart/ImageCandy.cs +++ b/ArcFormats/Interheart/ImageCandy.cs @@ -48,7 +48,7 @@ namespace GameRes.Formats.Interheart public CandyFormat () { - Signatures = new uint[] { 0x0E00, 0 }; + Signatures = new uint[] { 0x0E00, 0x80020A00, 0 }; } public override ImageMetaData ReadMetaData (IBinaryStream file) @@ -82,7 +82,7 @@ namespace GameRes.Formats.Interheart } else return null; - if (info.Version != 1 && info.Version != 2 || info.BPP < 8 || info.BPP > 32) + if (info.Version != 1 && info.Version != 2 || info.BPP < 1 || info.BPP > 32) return null; return info; } @@ -91,7 +91,7 @@ namespace GameRes.Formats.Interheart { var reader = new CandyDecoder (file, (CandyMetaData)info); var pixels = reader.Unpack(); - return ImageData.Create (info, reader.Format, reader.Palette, pixels); + return ImageData.Create (info, reader.Format, reader.Palette, pixels, reader.Stride); } public override void Write (Stream file, ImageData image) @@ -113,6 +113,7 @@ namespace GameRes.Formats.Interheart public PixelFormat Format { get; private set; } public BitmapPalette Palette { get; private set; } + public int Stride { get { return m_stride; } } public CandyDecoder (IBinaryStream input, CandyMetaData info) { @@ -133,6 +134,12 @@ namespace GameRes.Formats.Interheart m_stride = m_width * 4; Format = PixelFormats.Bgra32; } + else if (1 == info.BPP) + { + m_pixel_size = 1; + m_stride = (m_width + 7) / 8; + Format = PixelFormats.Indexed1; + } else { m_pixel_size = 1; @@ -148,6 +155,8 @@ namespace GameRes.Formats.Interheart if (m_colors > 0) Palette = ReadPalette(); LzUnpack(); + if (1 == m_info.BPP) + return m_output; return RestorePixels(); } From 40a523e59078b55e29185667aa0413b5dbd1f110 Mon Sep 17 00:00:00 2001 From: morkt Date: Thu, 7 Sep 2023 12:13:58 +0400 Subject: [PATCH 18/36] (PLT): support PL10 animations. --- ArcFormats/Kaguya/ArcPLT.cs | 100 ++++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/ArcFormats/Kaguya/ArcPLT.cs b/ArcFormats/Kaguya/ArcPLT.cs index bc2bcdc5..be74a4ee 100644 --- a/ArcFormats/Kaguya/ArcPLT.cs +++ b/ArcFormats/Kaguya/ArcPLT.cs @@ -23,8 +23,11 @@ // IN THE SOFTWARE. // +using System; using System.Collections.Generic; using System.ComponentModel.Composition; +using System.IO; +using System.Linq; using System.Windows.Media; namespace GameRes.Formats.Kaguya @@ -98,4 +101,101 @@ namespace GameRes.Formats.Kaguya return ImageData.CreateFlipped (Info, format, null, pixels, stride); } } + + internal class Pl10Entry : An21Entry + { + public ImageMetaData Info; + } + + [Export(typeof(ArchiveFormat))] + public class Pl10Opener : An21Opener + { + public override string Tag { get => "PL10"; } + public override uint Signature { get => 0x30314C50; } // 'PL10' + + public Pl10Opener () + { + Extensions = new string[] { "plt" }; + } + + public override ArcFile TryOpen (ArcView file) + { + using (var input = file.CreateStream()) + { + var base_info = GetBaseInfo (input); + var dir = GetFramesList (input); + if (null == dir) + return null; + string base_name = Path.GetFileNameWithoutExtension (file.Name); + foreach (Pl10Entry entry in dir) + { + entry.Name = string.Format ("{0}#{1:D2}", base_name, entry.FrameIndex); + entry.Type = "image"; + } + var first = (Pl10Entry)dir[0]; + base_info.BPP = first.Info.BPP; + return new An21Archive (file, this, dir, base_info); + } + } + + internal ImageMetaData GetBaseInfo (IBinaryStream input) + { + input.Position = 6; + return new ImageMetaData + { + OffsetX = input.ReadInt32(), + OffsetY = input.ReadInt32(), + Width = input.ReadUInt32(), + Height = input.ReadUInt32(), + }; + } + + internal List GetFramesList (IBinaryStream file) + { + file.Position = 4; + int count = file.ReadInt16(); + if (!IsSaneCount (count)) + return null; + var dir = new List (count); + long current_offset = 0x16; + file.Position = current_offset; + var frame_info = new ImageMetaData { + OffsetX = file.ReadInt32(), + OffsetY = file.ReadInt32(), + Width = file.ReadUInt32(), + Height = file.ReadUInt32(), + BPP = file.ReadInt32() * 8, + }; + uint depth = (uint)frame_info.BPP / 8; + uint image_size = depth * frame_info.Width * frame_info.Height; + var entry = new Pl10Entry + { + Offset = current_offset + 0x14, + Size = image_size, + FrameIndex = 0, + RleStep = 0, + Info = frame_info, + }; + dir.Add (entry); + for (int i = 1; i < count; ++i) + { + current_offset = entry.Offset + entry.Size; + file.Position = current_offset; + byte rle_step = file.ReadUInt8(); + uint packed_size = file.ReadUInt32(); + entry = new Pl10Entry + { + Offset = current_offset + 5, + Size = packed_size, + UnpackedSize = image_size, + IsPacked = true, + FrameIndex = i, + RleStep = rle_step, + Info = frame_info, + }; + dir.Add (entry); + } + return dir; + } + } } From 8f49baaee533be480833ed7b326d5432123dcefc Mon Sep 17 00:00:00 2001 From: morkt Date: Thu, 7 Sep 2023 12:15:29 +0400 Subject: [PATCH 19/36] (Kogado): 'ARCW' archive format. --- ArcFormats/Kogado/ArcARC.cs | 308 ++++++++++++++++++++++++++++++++++++ 1 file changed, 308 insertions(+) create mode 100644 ArcFormats/Kogado/ArcARC.cs 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 + } +} From c5cbcc228fa48e39b857bc6e99627f6f0c251d5a Mon Sep 17 00:00:00 2001 From: morkt Date: Thu, 7 Sep 2023 12:15:54 +0400 Subject: [PATCH 20/36] (Mugi): Mugi's resource archive. --- ArcFormats/Mugi/ArcBIN.cs | 94 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 ArcFormats/Mugi/ArcBIN.cs diff --git a/ArcFormats/Mugi/ArcBIN.cs b/ArcFormats/Mugi/ArcBIN.cs new file mode 100644 index 00000000..0b789208 --- /dev/null +++ b/ArcFormats/Mugi/ArcBIN.cs @@ -0,0 +1,94 @@ +//! \file ArcBIN.cs +//! \date 2023 Sep 03 +//! \brief Mugi's 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 System.Collections.Generic; +using System.ComponentModel.Composition; +using System.IO; + +// [070323][schoolzone] Cosplay! Kyonyuu Mahjong + +namespace GameRes.Formats.Mugi +{ + [Export(typeof(ArchiveFormat))] + public class BinOpener : ArchiveFormat + { + public override string Tag { get => "BIN/MUGI"; } + public override string Description { get => "Mugi's resource archive"; } + public override uint Signature { get => 0; } + public override bool IsHierarchic { get => false; } + public override bool CanWrite { get => false; } + + public override ArcFile TryOpen (ArcView file) + { + const uint index_size = 0x8000 + 0x4000; + if (file.MaxOffset <= index_size) + return null; + file.View.Reserve (0, index_size); + uint index_pos = 0x8000; + uint offset = file.View.ReadUInt32 (index_pos); + if (offset != index_size) + return null; + uint[] offsets = new uint[0x800]; + int count = 0; + while (offset != file.MaxOffset) + { + if (count == offsets.Length) + return null; + offsets[count++] = offset; + index_pos += 4; + offset = file.View.ReadUInt32 (index_pos); + if (offset < offsets[count-1] || offset > file.MaxOffset) + return null; + } + offsets[count--] = offset; + uint name_pos = 0; + uint size_pos = 0xA000; + var dir = new List (count); + for (int i = 0; i < count; ++i) + { + var name = file.View.ReadString (name_pos, 0x10); + var entry = Create (name); + entry.Offset = offsets[i]; + entry.Size = (uint)(offsets[i+1] - offsets[i]); + entry.UnpackedSize = file.View.ReadUInt32 (size_pos); + entry.IsPacked = entry.Size != entry.UnpackedSize; + dir.Add (entry); + name_pos += 0x10; + size_pos += 4; + } + return new ArcFile (file, this, dir); + } + + public override Stream OpenEntry (ArcFile arc, Entry entry) + { + var pent = (PackedEntry)entry; + Stream input = arc.File.CreateStream (entry.Offset, entry.Size); + if (pent.IsPacked) + input = new LzssStream (input); + return input; + } + } +} From 7c0283089e08ab2dcc1b0b4420a96a3b11091fb8 Mon Sep 17 00:00:00 2001 From: morkt Date: Thu, 7 Sep 2023 12:17:23 +0400 Subject: [PATCH 21/36] (QLIE): better support for 'FilePackVer1.0' archives. --- ArcFormats/Qlie/ArcQLIE.cs | 32 +++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/ArcFormats/Qlie/ArcQLIE.cs b/ArcFormats/Qlie/ArcQLIE.cs index ae263489..42fc771b 100644 --- a/ArcFormats/Qlie/ArcQLIE.cs +++ b/ArcFormats/Qlie/ArcQLIE.cs @@ -126,16 +126,32 @@ namespace GameRes.Formats.Qlie } var enc = QlieEncryption.Create (file, index.PackVersion, arc_key); List dir = null; - try + if (index.PackVersion.Major > 1) { dir = index.Read (enc, key_file, use_pack_keyfile); } - catch + else { - if (index.PackVersion.Major == 1) + // PackVer1.0 is a total mess -- it could either use + // • V1 index layout and V1 encryption + // • V1 index layout and V2 encryption + // • V2 index layout and V2 encryption + // all with the same 'FilePackVer1.0' signature + var possibleEncs = new IEncryption[] { + enc, new EncryptionV2 (IndexLayout.WithoutHash), new EncryptionV2() + }; + foreach (var v1enc in possibleEncs) { - enc = new EncryptionV2(); - dir = index.Read (enc, key_file, use_pack_keyfile); + try + { + dir = index.Read (v1enc, key_file, use_pack_keyfile); + if (dir != null) + { + enc = v1enc; + break; + } + } + catch { } } } if (null == dir) @@ -400,6 +416,8 @@ namespace GameRes.Formats.Qlie for (int i = 0; i < m_count; ++i) { int name_length = m_index.ReadUInt16(); + if (name_length > 0x100) // invalid encryption version + return null; if (enc.IsUnicode) name_length *= 2; if (name_length > m_name_buffer.Length) @@ -418,8 +436,8 @@ namespace GameRes.Formats.Qlie entry.UnpackedSize = m_index.ReadUInt32(); // [+0C] entry.IsPacked = 0 != m_index.ReadInt32(); // [+10] entry.EncryptionMethod = m_index.ReadInt32(); // [+14] - if (m_pack_version.Major > 1) - entry.Hash = m_index.ReadUInt32(); // [+18] + if (enc.IndexLayout == IndexLayout.WithHash) + entry.Hash = m_index.ReadUInt32(); // [+18] entry.KeyFile = key_file; if (read_pack_keyfile && entry.Name.Contains ("pack_keyfile")) { From bdffc239bc0419f84e616979b6e31e0b379eb6e8 Mon Sep 17 00:00:00 2001 From: morkt Date: Thu, 7 Sep 2023 12:18:15 +0400 Subject: [PATCH 22/36] (QLIE): better support for 'FilePackVer1.0' archives. --- ArcFormats/Qlie/Encryption.cs | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/ArcFormats/Qlie/Encryption.cs b/ArcFormats/Qlie/Encryption.cs index ff7ee8d3..dbdfcc8b 100644 --- a/ArcFormats/Qlie/Encryption.cs +++ b/ArcFormats/Qlie/Encryption.cs @@ -33,6 +33,8 @@ namespace GameRes.Formats.Qlie { bool IsUnicode { get; } + IndexLayout IndexLayout { get; } + uint CalculateHash (byte[] data, int length); string DecryptName (byte[] name, int name_length); @@ -40,9 +42,17 @@ namespace GameRes.Formats.Qlie void DecryptEntry (byte[] data, int offset, int length, QlieEntry entry); } + internal enum IndexLayout + { + WithoutHash, + WithHash + } + internal abstract class QlieEncryption : IEncryption { - public virtual bool IsUnicode { get { return false; } } + public virtual bool IsUnicode => false; + + public virtual IndexLayout IndexLayout => IndexLayout.WithHash; /// /// Hash generated from the key data contained within archive index. @@ -77,6 +87,8 @@ namespace GameRes.Formats.Qlie internal class EncryptionV1 : QlieEncryption { + public override IndexLayout IndexLayout => IndexLayout.WithoutHash; + public EncryptionV1 () { NameKey = 0xC4; @@ -126,10 +138,15 @@ namespace GameRes.Formats.Qlie internal class EncryptionV2 : QlieEncryption { - public EncryptionV2 () + public override IndexLayout IndexLayout => m_layout; + + private IndexLayout m_layout; + + public EncryptionV2 (IndexLayout layout = IndexLayout.WithHash) { NameKey = 0xC4; ArcKey = 0; + m_layout = layout; } public override uint CalculateHash (byte[] data, int length) From ceaa0efaf5ca990cc0316c2843273fc8ac95b619 Mon Sep 17 00:00:00 2001 From: morkt Date: Thu, 7 Sep 2023 12:19:21 +0400 Subject: [PATCH 23/36] (ScrPlayer): another convoluted image format. --- .../{ColorBitsTable1 => IColorBitsTable1} | Bin .../{ColorBitsTable2 => IColorBitsTable2} | Bin .../{ControlTable1 => IControlTable1} | Bin .../{ControlTable2 => IControlTable2} | Bin .../{ControlTable32 => IControlTable32} | Bin ArcFormats/ScrPlayer/ImageI.cs | 12 +- ArcFormats/ScrPlayer/ImageIMG.cs | 490 ++++++++++++++++++ ArcFormats/ScrPlayer/ImgControlTable1 | Bin 0 -> 16384 bytes ArcFormats/ScrPlayer/ImgControlTable2 | Bin 0 -> 16384 bytes ArcFormats/ScrPlayer/ImgControlTable32 | Bin 0 -> 16384 bytes ArcFormats/ScrPlayer/ImgDeltaTable2 | Bin 0 -> 16384 bytes 11 files changed, 496 insertions(+), 6 deletions(-) rename ArcFormats/ScrPlayer/{ColorBitsTable1 => IColorBitsTable1} (100%) rename ArcFormats/ScrPlayer/{ColorBitsTable2 => IColorBitsTable2} (100%) rename ArcFormats/ScrPlayer/{ControlTable1 => IControlTable1} (100%) rename ArcFormats/ScrPlayer/{ControlTable2 => IControlTable2} (100%) rename ArcFormats/ScrPlayer/{ControlTable32 => IControlTable32} (100%) create mode 100644 ArcFormats/ScrPlayer/ImageIMG.cs create mode 100644 ArcFormats/ScrPlayer/ImgControlTable1 create mode 100644 ArcFormats/ScrPlayer/ImgControlTable2 create mode 100644 ArcFormats/ScrPlayer/ImgControlTable32 create mode 100644 ArcFormats/ScrPlayer/ImgDeltaTable2 diff --git a/ArcFormats/ScrPlayer/ColorBitsTable1 b/ArcFormats/ScrPlayer/IColorBitsTable1 similarity index 100% rename from ArcFormats/ScrPlayer/ColorBitsTable1 rename to ArcFormats/ScrPlayer/IColorBitsTable1 diff --git a/ArcFormats/ScrPlayer/ColorBitsTable2 b/ArcFormats/ScrPlayer/IColorBitsTable2 similarity index 100% rename from ArcFormats/ScrPlayer/ColorBitsTable2 rename to ArcFormats/ScrPlayer/IColorBitsTable2 diff --git a/ArcFormats/ScrPlayer/ControlTable1 b/ArcFormats/ScrPlayer/IControlTable1 similarity index 100% rename from ArcFormats/ScrPlayer/ControlTable1 rename to ArcFormats/ScrPlayer/IControlTable1 diff --git a/ArcFormats/ScrPlayer/ControlTable2 b/ArcFormats/ScrPlayer/IControlTable2 similarity index 100% rename from ArcFormats/ScrPlayer/ControlTable2 rename to ArcFormats/ScrPlayer/IControlTable2 diff --git a/ArcFormats/ScrPlayer/ControlTable32 b/ArcFormats/ScrPlayer/IControlTable32 similarity index 100% rename from ArcFormats/ScrPlayer/ControlTable32 rename to ArcFormats/ScrPlayer/IControlTable32 diff --git a/ArcFormats/ScrPlayer/ImageI.cs b/ArcFormats/ScrPlayer/ImageI.cs index 16979c98..2de675a9 100644 --- a/ArcFormats/ScrPlayer/ImageI.cs +++ b/ArcFormats/ScrPlayer/ImageI.cs @@ -278,11 +278,11 @@ namespace GameRes.Formats.ScrPlayer #endregion #region Bitmap tables - static readonly Lazy s_control_table1 = new Lazy (() => LoadResource ("ControlTable1")); - static readonly Lazy s_control_table2 = new Lazy (() => LoadResource ("ControlTable2")); - static readonly Lazy s_control_table32 = new Lazy (() => LoadResource ("ControlTable32")); - static readonly Lazy s_color_bits1 = new Lazy (() => LoadResource ("ColorBitsTable1")); - static readonly Lazy s_color_bits2 = new Lazy (() => LoadResource ("ColorBitsTable2")); + static readonly Lazy s_control_table1 = new Lazy (() => LoadResource ("IControlTable1")); + static readonly Lazy s_control_table2 = new Lazy (() => LoadResource ("IControlTable2")); + static readonly Lazy s_control_table32 = new Lazy (() => LoadResource ("IControlTable32")); + static readonly Lazy s_color_bits1 = new Lazy (() => LoadResource ("IColorBitsTable1")); + static readonly Lazy s_color_bits2 = new Lazy (() => LoadResource ("IColorBitsTable2")); static byte[] ControlTable1 { get { return s_control_table1.Value; } } static byte[] ControlTable2 { get { return s_control_table2.Value; } } @@ -390,7 +390,7 @@ namespace GameRes.Formats.ScrPlayer 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, } }; - static readonly int[] OffsetTable = { + internal static readonly int[] OffsetTable = { -1, 0, 0, 1, 1, 1, -1, 1, 2, 1, -2, 1, -2, 0, 0, 2, 1, 2, -1, 2, -3, 0, }; #endregion diff --git a/ArcFormats/ScrPlayer/ImageIMG.cs b/ArcFormats/ScrPlayer/ImageIMG.cs new file mode 100644 index 00000000..b1a444ec --- /dev/null +++ b/ArcFormats/ScrPlayer/ImageIMG.cs @@ -0,0 +1,490 @@ +//! \file ImageIMG.cs +//! \date 2023 Aug 30 +//! \brief ScrPlayer image format. +// +// 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.Utility; +using System; +using System.ComponentModel.Composition; +using System.IO; +using System.Windows.Media; + +// [990709][Love Gun] Koi no Sweet Tart wa Ikaga + +namespace GameRes.Formats.ScrPlayer +{ + [Export(typeof(ImageFormat))] + public class ImgFormat : ImageFormat + { + public override string Tag { get => "IMG"; } + public override string Description { get => "ScrPlayer image format"; } + public override uint Signature { get => 0x20474D49; } // 'IMG ' + + public override ImageMetaData ReadMetaData (IBinaryStream file) + { + var header = file.ReadHeader (0x18); + int bpp = header.ToUInt16 (0x10); + if (bpp != 24 && bpp != 32) + return null; + return new ImageMetaData { + Width = header.ToUInt16 (0xC), + Height = header.ToUInt16 (0xE), + BPP = bpp, + }; + } + + public override ImageData Read (IBinaryStream file, ImageMetaData info) + { + using (var reader = new ImgReader (file, info)) + return reader.Unpack(); + } + + public override void Write (Stream file, ImageData image) + { + throw new System.NotImplementedException ("ImgFormat.Write not implemented"); + } + } + + internal sealed class ImgReader : IDisposable + { + ImgBitStream m_input; + ImageMetaData m_info; + int m_stride; + byte[] m_output; + + public PixelFormat Format { get; private set; } + + public ImgReader (IBinaryStream input, ImageMetaData info) + { + m_input = new ImgBitStream (input.AsStream, true); + m_info = info; + m_stride = m_info.iWidth * 4; + m_output = new byte[m_stride * (m_info.iHeight + 1)]; + Format = m_info.BPP == 32 ? PixelFormats.Bgra32 : PixelFormats.Bgr32; + } + + public ImageData Unpack () + { + m_input.Input.Position = 0x18; + if (32 == m_info.BPP) + Unpack32bpp(); + else + Unpack24bpp(); + return ImageData.Create (m_info, Format, null, m_output); + } + + byte[] m_row_buffer = new byte[0x2580]; + int[] m_rows_ptr = new int[3] { 0, 0xC80, 0xC80 * 2 }; + + void Unpack24bpp () + { + var offset_table = Img2Reader.OffsetTable.Clone() as int[]; + int dst = 0; + for (int y = 0; y < m_info.iHeight; ++y) + { + int row_pos = m_rows_ptr[2]; + m_rows_ptr[2] = m_rows_ptr[1]; + m_rows_ptr[1] = m_rows_ptr[0]; + m_rows_ptr[0] = row_pos; + for (int x = 0; x < m_info.iWidth; ) + { + int ctl = m_input.GetBits (ControlTable1, 13); + if (0xEF == ctl) + ctl += m_input.GetBits (ControlTable2, 11); + + int pos = m_input.GetBits (PosTable24, 5) * 2; + int x_offset = offset_table[pos]; + int y_offset = offset_table[pos + 1]; + if (pos > 0) + { + offset_table[pos] = offset_table[pos-2]; + offset_table[pos+1] = offset_table[pos-1]; + offset_table[pos-2] = x_offset; + offset_table[pos-1] = y_offset; + } + int src = m_rows_ptr[y_offset] + (x + x_offset) * 4; + if (ctl >= 0xD8) + { + int count = ctl - 0xD6; + Binary.CopyOverlapped (m_row_buffer, src, row_pos, count * 4); + row_pos += count * 4; + x += count; + } + else + { + switch (ColorCode[ctl]) + { + case 0: + m_row_buffer[row_pos + 2] = (byte)(m_row_buffer[src + 2] - MapRed[ctl]); + m_row_buffer[row_pos + 1] = (byte)(m_row_buffer[src + 1] - MapGreen[ctl]); + m_row_buffer[row_pos ] = (byte)(m_row_buffer[src ] - MapBlue[ctl]); + break; + case 1: + m_row_buffer[row_pos + 2] = (byte)(m_row_buffer[src + 2] - GetDelta()); + m_row_buffer[row_pos + 1] = (byte)(m_row_buffer[src + 1] - MapGreen[ctl]); + m_row_buffer[row_pos ] = (byte)(m_row_buffer[src ] - MapBlue[ctl]); + break; + case 2: + m_row_buffer[row_pos + 2] = (byte)(m_row_buffer[src + 2] - MapRed[ctl]); + m_row_buffer[row_pos + 1] = (byte)(m_row_buffer[src + 1] - GetDelta()); + m_row_buffer[row_pos ] = (byte)(m_row_buffer[src ] - MapBlue[ctl]); + break; + case 3: + m_row_buffer[row_pos + 2] = (byte)(m_row_buffer[src + 2] - GetDelta()); + m_row_buffer[row_pos + 1] = (byte)(m_row_buffer[src + 1] - GetDelta()); + m_row_buffer[row_pos ] = (byte)(m_row_buffer[src ] - MapBlue[ctl]); + break; + case 4: + m_row_buffer[row_pos + 2] = (byte)(m_row_buffer[src + 2] - MapRed[ctl]); + m_row_buffer[row_pos + 1] = (byte)(m_row_buffer[src + 1] - MapGreen[ctl]); + m_row_buffer[row_pos ] = (byte)(m_row_buffer[src ] - GetDelta()); + break; + case 5: + m_row_buffer[row_pos + 2] = (byte)(m_row_buffer[src + 2] - GetDelta()); + m_row_buffer[row_pos + 1] = (byte)(m_row_buffer[src + 1] - MapGreen[ctl]); + m_row_buffer[row_pos ] = (byte)(m_row_buffer[src ] - GetDelta()); + break; + case 6: + m_row_buffer[row_pos + 2] = (byte)(m_row_buffer[src + 2] - MapRed[ctl]); + m_row_buffer[row_pos + 1] = (byte)(m_row_buffer[src + 1] - GetDelta()); + m_row_buffer[row_pos ] = (byte)(m_row_buffer[src ] - GetDelta()); + break; + case 7: + m_row_buffer[row_pos + 2] = (byte)(m_row_buffer[src + 2] - GetDelta()); + m_row_buffer[row_pos + 1] = (byte)(m_row_buffer[src + 1] - GetDelta()); + m_row_buffer[row_pos ] = (byte)(m_row_buffer[src ] - GetDelta()); + break; + } + row_pos += 4; + ++x; + } + } + Buffer.BlockCopy (m_row_buffer, m_rows_ptr[0], m_output, dst, m_stride); + dst += m_stride; + } + } + + void Unpack32bpp () + { + for (int i = 3; i < m_row_buffer.Length; i += 4) + m_row_buffer[i] = 0xFF; + var offset_table = Img2Reader.OffsetTable.Clone() as int[]; + int dst = 0; + for (int y = 0; y < m_info.iHeight; ++y) + { + int row_pos = m_rows_ptr[2]; + m_rows_ptr[2] = m_rows_ptr[1]; + m_rows_ptr[1] = m_rows_ptr[0]; + m_rows_ptr[0] = row_pos; + for (int x = 0; x < m_info.iWidth; ) + { + int ctl = m_input.GetBits (ControlTable1, 13); + if (0xEF == ctl) + ctl += m_input.GetBits (ControlTable2, 11); + + int t = m_input.GetBits (ControlTable32, 13) * 2; + int pos = PosTable32[t] * 2; + int alpha = PosTable32[t+1]; + int x_offset = offset_table[pos]; + int y_offset = offset_table[pos + 1]; + if (pos > 0) + { + offset_table[pos] = offset_table[pos-2]; + offset_table[pos+1] = offset_table[pos-1]; + offset_table[pos-2] = x_offset; + offset_table[pos-1] = y_offset; + } + int src = m_rows_ptr[y_offset] + (x + x_offset) * 4; + if (ctl >= 0xD8) + { + int count = ctl - 0xD6; + Binary.CopyOverlapped (m_row_buffer, src, row_pos, count * 4); + row_pos += count * 4; + x += count; + } + else + { + switch (ColorCode[ctl]) + { + case 0: + m_row_buffer[row_pos + 2] = (byte)(m_row_buffer[src + 2] - MapRed[ctl]); + m_row_buffer[row_pos + 1] = (byte)(m_row_buffer[src + 1] - MapGreen[ctl]); + m_row_buffer[row_pos ] = (byte)(m_row_buffer[src ] - MapBlue[ctl]); + break; + case 1: + m_row_buffer[row_pos + 2] = (byte)(m_row_buffer[src + 2] - GetDelta()); + m_row_buffer[row_pos + 1] = (byte)(m_row_buffer[src + 1] - MapGreen[ctl]); + m_row_buffer[row_pos ] = (byte)(m_row_buffer[src ] - MapBlue[ctl]); + break; + case 2: + m_row_buffer[row_pos + 2] = (byte)(m_row_buffer[src + 2] - MapRed[ctl]); + m_row_buffer[row_pos + 1] = (byte)(m_row_buffer[src + 1] - GetDelta()); + m_row_buffer[row_pos ] = (byte)(m_row_buffer[src ] - MapBlue[ctl]); + break; + case 3: + m_row_buffer[row_pos + 2] = (byte)(m_row_buffer[src + 2] - GetDelta()); + m_row_buffer[row_pos + 1] = (byte)(m_row_buffer[src + 1] - GetDelta()); + m_row_buffer[row_pos ] = (byte)(m_row_buffer[src ] - MapBlue[ctl]); + break; + case 4: + m_row_buffer[row_pos + 2] = (byte)(m_row_buffer[src + 2] - MapRed[ctl]); + m_row_buffer[row_pos + 1] = (byte)(m_row_buffer[src + 1] - MapGreen[ctl]); + m_row_buffer[row_pos ] = (byte)(m_row_buffer[src ] - GetDelta()); + break; + case 5: + m_row_buffer[row_pos + 2] = (byte)(m_row_buffer[src + 2] - GetDelta()); + m_row_buffer[row_pos + 1] = (byte)(m_row_buffer[src + 1] - MapGreen[ctl]); + m_row_buffer[row_pos ] = (byte)(m_row_buffer[src ] - GetDelta()); + break; + case 6: + m_row_buffer[row_pos + 2] = (byte)(m_row_buffer[src + 2] - MapRed[ctl]); + m_row_buffer[row_pos + 1] = (byte)(m_row_buffer[src + 1] - GetDelta()); + m_row_buffer[row_pos ] = (byte)(m_row_buffer[src ] - GetDelta()); + break; + case 7: + m_row_buffer[row_pos + 2] = (byte)(m_row_buffer[src + 2] - GetDelta()); + m_row_buffer[row_pos + 1] = (byte)(m_row_buffer[src + 1] - GetDelta()); + m_row_buffer[row_pos ] = (byte)(m_row_buffer[src ] - GetDelta()); + break; + } + if (-3 == alpha) + { + alpha = GetDelta(); + } + m_row_buffer[row_pos + 3] = (byte)(m_row_buffer[src + 3] - alpha); + row_pos += 4; + ++x; + } + } + Buffer.BlockCopy (m_row_buffer, m_rows_ptr[0], m_output, dst, m_stride); + dst += m_stride; + } + } + + int GetDelta () + { + int d = m_input.GetBits (DeltaTable1, 8); + if (0x2B == d) + d += m_input.GetBits (DeltaTable2, 13); + return DeltaTable3[d]; + } + + static byte[] LoadResource (string name) => EmbeddedResource.Load (name, typeof(ImgReader)); + + #region IDisposable Members + bool _disposed = false; + public void Dispose () + { + if (!_disposed) + { + m_input.Dispose(); + _disposed = true; + } + } + #endregion + + #region Bitmap Tables + static readonly Lazy s_control_table1 = new Lazy (() => LoadResource ("ImgControlTable1")); + static readonly Lazy s_control_table2 = new Lazy (() => LoadResource ("ImgControlTable2")); + static readonly Lazy s_control_table32 = new Lazy (() => LoadResource ("ImgControlTable32")); + static readonly Lazy s_delta_table2 = new Lazy (() => LoadResource ("ImgDeltaTable2")); + + static byte[] ControlTable1 { get { return s_control_table1.Value; } } + static byte[] ControlTable2 { get { return s_control_table2.Value; } } + static byte[] ControlTable32 { get { return s_control_table32.Value; } } + static byte[] DeltaTable2 { get { return s_delta_table2.Value; } } + + static readonly byte[] PosTable24 = { + 4, 2, 1, 0, 4, 3, 1, 0, 3, 1, 1, 0, 4, 4, 1, 0, 5, 5, 1, 0, 5, 7, 1, 0, 3, 1, 1, 0, 5, 9, 1, 0, 4, + 2, 1, 0, 4, 3, 1, 0, 3, 1, 1, 0, 4, 4, 1, 0, 5, 6, 1, 0, 5, 8, 1, 0, 3, 1, 1, 0, 5, 10, 1, 0, + }; + static readonly sbyte[] PosTable32 = { + 0, 0, 1, 0, 2, 0, 3, 0, 4, 0, 5, 0, 6, 0, 7, 0, 8, 0, 9, 0, 10, 0, 0, -3, 1, -3, 2, -3, 3, -3, 4, + -3, 5, -3, 6, -3, 7, -3, 8, -3, 9, -3, 10, -3, 0, -1, 1, -1, 2, -1, 3, -1, 4, -1, 5, -1, 6, -1, 7, + -1, 8, -1, 9, -1, 10, -1, 0, 1, 1, 1, 2, 1, 3, 1, 4, 1, 5, 1, 6, 1, 7, 1, 8, 1, 9, 1, 10, 1, 0, + -2, 1, -2, 2, -2, 3, -2, 4, -2, 5, -2, 6, -2, 7, -2, 8, -2, 9, -2, 10, -2, 0, 2, 1, 2, 2, 2, 3, 2, + 4, 2, 5, 2, 6, 2, 7, 2, 8, 2, 9, 2, 10, 2, + }; + static readonly byte[] ColorCode = { + 7, 3, 3, 3, 3, 3, 5, 1, 1, 1, 1, 1, 5, 1, 1, 1, 1, 1, 5, 1, 1, 1, 1, 1, + 5, 1, 1, 1, 1, 1, 5, 1, 1, 1, 1, 1, 6, 2, 2, 2, 2, 2, 4, 0, 0, 0, 0, 0, + 4, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, + 6, 2, 2, 2, 2, 2, 4, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, + 4, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 6, 2, 2, 2, 2, 2, 4, 0, 0, 0, 0, 0, + 4, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, + 6, 2, 2, 2, 2, 2, 4, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, + 4, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 6, 2, 2, 2, 2, 2, 4, 0, 0, 0, 0, 0, + 4, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, + }; + + static readonly byte[] DeltaTable1 = { + 7, 0x13, 3, 0, 6, 0x0D, 4, 4, 5, 7, 3, 1, 5, 8, 4, 5, 4, 2, 3, 0, 4, 0x2B, 6, 0x10, 4, 3, 3, 1, 5, + 9, 5, 0x0B, 5, 6, 3, 0, 6, 0x0F, 4, 4, 6, 0x0C, 3, 1, 8, 0x23, 4, 5, 4, 2, 3, 0, 4, 0x2B, 6, 0x11, + 4, 3, 3, 1, 5, 0x0A, 8, 0x27, 7, 0x15, 3, 0, 6, 0x0E, 4, 4, 5, 7, 3, 1, 5, 8, 4, 5, 4, 2, 3, 0, 4, + 0x2B, 7, 0x1B, 4, 3, 3, 1, 5, 9, 5, 0x0B, 5, 6, 3, 0, 7, 0x18, 4, 4, 8, 0x22, 3, 1, 7, 0x19, 4, 5, + 4, 2, 3, 0, 4, 0x2B, 6, 0x12, 4, 3, 3, 1, 5, 0x0A, 6, 0x14, 8, 0x1D, 3, 0, 6, 0x0D, 4, 4, 5, 7, 3, + 1, 5, 8, 4, 5, 4, 2, 3, 0, 4, 0x2B, 6, 0x10, 4, 3, 3, 1, 5, 9, 5, 0x0B, 5, 6, 3, 0, 6, 0x0F, 4, 4, + 6, 0x0C, 3, 1, 7, 0x1A, 4, 5, 4, 2, 3, 0, 4, 0x2B, 6, 0x11, 4, 3, 3, 1, 5, 0x0A, 7, 0x1C, 8, 0x20, + 3, 0, 6, 0x0E, 4, 4, 5, 7, 3, 1, 5, 8, 4, 5, 4, 2, 3, 0, 4, 0x2B, 8, 0x28, 4, 3, 3, 1, 5, 9, 5, + 0x0B, 5, 6, 3, 0, 7, 0x17, 4, 4, 7, 0x16, 3, 1, 8, 0x26, 4, 5, 4, 2, 3, 0, 4, 0x2B, 6, 0x12, 4, 3, + 3, 1, 5, 0x0A, 6, 0x14, 7, 0x13, 3, 0, 6, 0x0D, 4, 4, 5, 7, 3, 1, 5, 8, 4, 5, 4, 2, 3, 0, 4, 0x2B, + 6, 0x10, 4, 3, 3, 1, 5, 9, 5, 0x0B, 5, 6, 3, 0, 6, 0x0F, 4, 4, 6, 0x0C, 3, 1, 8, 0x24, 4, 5, 4, 2, + 3, 0, 4, 0x2B, 6, 0x11, 4, 3, 3, 1, 5, 0x0A, 8, 0x29, 7, 0x15, 3, 0, 6, 0x0E, 4, 4, 5, 7, 3, 1, 5, + 8, 4, 5, 4, 2, 3, 0, 4, 0x2B, 7, 0x1B, 4, 3, 3, 1, 5, 9, 5, 0x0B, 5, 6, 3, 0, 7, 0x18, 4, 4, 8, + 0x21, 3, 1, 7, 0x19, 4, 5, 4, 2, 3, 0, 4, 0x2B, 6, 0x12, 4, 3, 3, 1, 5, 0x0A, 6, 0x14, 8, 0x1E, 3, + 0, 6, 0x0D, 4, 4, 5, 7, 3, 1, 5, 8, 4, 5, 4, 2, 3, 0, 4, 0x2B, 6, 0x10, 4, 3, 3, 1, 5, 9, 5, 0x0B, + 5, 6, 3, 0, 6, 0x0F, 4, 4, 6, 0x0C, 3, 1, 7, 0x1A, 4, 5, 4, 2, 3, 0, 4, 0x2B, 6, 0x11, 4, 3, 3, 1, + 5, 0x0A, 7, 0x1C, 8, 0x1F, 3, 0, 6, 0x0E, 4, 4, 5, 7, 3, 1, 5, 8, 4, 5, 4, 2, 3, 0, 4, 0x2B, 8, + 0x2A, 4, 3, 3, 1, 5, 9, 5, 0x0B, 5, 6, 3, 0, 7, 0x17, 4, 4, 7, 0x16, 3, 1, 8, 0x25, 4, 5, 4, 2, 3, + 0, 4, 0x2B, 6, 0x12, 4, 3, 3, 1, 5, 0x0A, 6, 0x14, + }; + static readonly byte[] DeltaTable3 = { + 0x03, 0xFD, 0x04, 0xFC, 0x05, 0xFB, 0x06, 0xFA, 0x07, 0xF9, 0x08, 0xF8, 0x09, 0xF7, 0x0A, 0xF6, + 0x0B, 0xF5, 0x0C, 0xF4, 0x0D, 0xF3, 0x0E, 0xF2, 0x0F, 0xF1, 0x10, 0xF0, 0x11, 0xEF, 0x12, 0xEE, + 0x13, 0xED, 0x14, 0xEC, 0x15, 0xEB, 0x16, 0xEA, 0x17, 0xE9, 0x18, 0xE8, 0x19, 0xE7, 0x1A, 0xE6, + 0x1B, 0xE5, 0x1C, 0xE4, 0x1D, 0xE3, 0x1E, 0xE2, 0x1F, 0xE1, 0x20, 0xE0, 0x21, 0xDF, 0x22, 0xDE, + 0x23, 0xDD, 0x24, 0xDC, 0x25, 0xDB, 0x26, 0xDA, 0x27, 0xD9, 0x28, 0xD8, 0x29, 0xD7, 0x2A, 0xD6, + 0x2B, 0xD5, 0x2C, 0xD4, 0x2D, 0xD3, 0x2E, 0xD2, 0x2F, 0xD1, 0x30, 0xD0, 0x31, 0xCF, 0x32, 0xCE, + 0x33, 0xCD, 0x34, 0xCC, 0x35, 0xCB, 0x36, 0xCA, 0x37, 0xC9, 0x38, 0xC8, 0x39, 0xC7, 0x3A, 0xC6, + 0x3B, 0xC5, 0x3C, 0xC4, 0x3D, 0xC3, 0x3E, 0xC2, 0x3F, 0xC1, 0x40, 0xC0, 0x41, 0xBF, 0x42, 0xBE, + 0x43, 0xBD, 0x44, 0xBC, 0x45, 0xBB, 0x46, 0xBA, 0x47, 0xB9, 0x48, 0xB8, 0x49, 0xB7, 0x4A, 0xB6, + 0x4B, 0xB5, 0x4C, 0xB4, 0x4D, 0xB3, 0x4E, 0xB2, 0x4F, 0xB1, 0x50, 0xB0, 0x51, 0xAF, 0x52, 0xAE, + 0x53, 0xAD, 0x54, 0xAC, 0x55, 0xAB, 0x56, 0xAA, 0x57, 0xA9, 0x58, 0xA8, 0x59, 0xA7, 0x5A, 0xA6, + 0x5B, 0xA5, 0x5C, 0xA4, 0x5D, 0xA3, 0x5E, 0xA2, 0x5F, 0xA1, 0x60, 0xA0, 0x61, 0x9F, 0x62, 0x9E, + 0x63, 0x9D, 0x64, 0x9C, 0x65, 0x9B, 0x66, 0x9A, 0x67, 0x99, 0x68, 0x98, 0x69, 0x97, 0x6A, 0x96, + 0x6B, 0x95, 0x6C, 0x94, 0x6D, 0x93, 0x6E, 0x92, 0x6F, 0x91, 0x70, 0x90, 0x71, 0x8F, 0x72, 0x8E, + 0x73, 0x8D, 0x74, 0x8C, 0x75, 0x8B, 0x76, 0x8A, 0x77, 0x89, 0x78, 0x88, 0x79, 0x87, 0x7A, 0x86, + 0x7B, 0x85, 0x7C, 0x84, 0x7D, 0x83, 0x7E, 0x82, 0x7F, 0x81, 0x80, 0, + }; + static readonly byte[] MapRed = { + 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, + 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, + 0xFD, 0xFD, 0xFD, 0xFD, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, + 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, + 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + }; + static readonly byte[] MapGreen = { + 0x0FD, 0x0FD, 0x0FD, 0x0FD, 0x0FD, 0x0FD, 0x0FE, 0x0FE, 0x0FE, 0x0FE, 0x0FE, 0x0FE, 0x0FF, 0x0FF, + 0x0FF, 0x0FF, 0x0FF, 0x0FF, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0x0FD, 0x0FD, + 0x0FD, 0x0FD, 0x0FD, 0x0FD, 0x0FE, 0x0FE, 0x0FE, 0x0FE, 0x0FE, 0x0FE, 0x0FF, 0x0FF, 0x0FF, 0x0FF, + 0x0FF, 0x0FF, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0x0FD, 0x0FD, 0x0FD, 0x0FD, + 0x0FD, 0x0FD, 0x0FE, 0x0FE, 0x0FE, 0x0FE, 0x0FE, 0x0FE, 0x0FF, 0x0FF, 0x0FF, 0x0FF, 0x0FF, 0x0FF, + 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0x0FD, 0x0FD, 0x0FD, 0x0FD, 0x0FD, 0x0FD, + 0x0FE, 0x0FE, 0x0FE, 0x0FE, 0x0FE, 0x0FE, 0x0FF, 0x0FF, 0x0FF, 0x0FF, 0x0FF, 0x0FF, 0, 0, 0, 0, 0, + 0, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0x0FD, 0x0FD, 0x0FD, 0x0FD, 0x0FD, 0x0FD, 0x0FE, 0x0FE, + 0x0FE, 0x0FE, 0x0FE, 0x0FE, 0x0FF, 0x0FF, 0x0FF, 0x0FF, 0x0FF, 0x0FF, 0, 0, 0, 0, 0, 0, 1, 1, 1, + 1, 1, 1, 2, 2, 2, 2, 2, 2, 0x0FD, 0x0FD, 0x0FD, 0x0FD, 0x0FD, 0x0FD, 0x0FE, 0x0FE, 0x0FE, 0x0FE, + 0x0FE, 0x0FE, 0x0FF, 0x0FF, 0x0FF, 0x0FF, 0x0FF, 0x0FF, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 2, 2, + 2, 2, 2, 2, + }; + static readonly byte[] MapBlue = { + 0x0FD, 0x0FE, 0x0FF, 0, 1, 2, 0x0FD, 0x0FE, 0x0FF, 0, 1, 2, + 0x0FD, 0x0FE, 0x0FF, 0, 1, 2, 0x0FD, 0x0FE, 0x0FF, 0, 1, 2, + 0x0FD, 0x0FE, 0x0FF, 0, 1, 2, 0x0FD, 0x0FE, 0x0FF, 0, 1, 2, + 0x0FD, 0x0FE, 0x0FF, 0, 1, 2, 0x0FD, 0x0FE, 0x0FF, 0, 1, 2, + 0x0FD, 0x0FE, 0x0FF, 0, 1, 2, 0x0FD, 0x0FE, 0x0FF, 0, 1, 2, + 0x0FD, 0x0FE, 0x0FF, 0, 1, 2, 0x0FD, 0x0FE, 0x0FF, 0, 1, 2, + 0x0FD, 0x0FE, 0x0FF, 0, 1, 2, 0x0FD, 0x0FE, 0x0FF, 0, 1, 2, + 0x0FD, 0x0FE, 0x0FF, 0, 1, 2, 0x0FD, 0x0FE, 0x0FF, 0, 1, 2, + 0x0FD, 0x0FE, 0x0FF, 0, 1, 2, 0x0FD, 0x0FE, 0x0FF, 0, 1, 2, + 0x0FD, 0x0FE, 0x0FF, 0, 1, 2, 0x0FD, 0x0FE, 0x0FF, 0, 1, 2, + 0x0FD, 0x0FE, 0x0FF, 0, 1, 2, 0x0FD, 0x0FE, 0x0FF, 0, 1, 2, + 0x0FD, 0x0FE, 0x0FF, 0, 1, 2, 0x0FD, 0x0FE, 0x0FF, 0, 1, 2, + 0x0FD, 0x0FE, 0x0FF, 0, 1, 2, 0x0FD, 0x0FE, 0x0FF, 0, 1, 2, + 0x0FD, 0x0FE, 0x0FF, 0, 1, 2, 0x0FD, 0x0FE, 0x0FF, 0, 1, 2, + 0x0FD, 0x0FE, 0x0FF, 0, 1, 2, 0x0FD, 0x0FE, 0x0FF, 0, 1, 2, + 0x0FD, 0x0FE, 0x0FF, 0, 1, 2, 0x0FD, 0x0FE, 0x0FF, 0, 1, 2, + 0x0FD, 0x0FE, 0x0FF, 0, 1, 2, 0x0FD, 0x0FE, 0x0FF, 0, 1, 2, + 0x0FD, 0x0FE, 0x0FF, 0, 1, 2, 0x0FD, 0x0FE, 0x0FF, 0, 1, 2, + }; + #endregion + } + + internal class ImgBitStream : BitStream + { + public ImgBitStream (Stream file, bool leave_open = false) : base (file, leave_open) + { + } + + public int GetBits (byte[] table, int count) + { + int n = PeekBits (count) * 2; + count = table[n]; + m_bits >>= count; + m_cached_bits -= count; + return table[n + 1]; + } + + public int PeekBits (int count) + { + if (m_cached_bits < count) + { + int b = m_input.ReadByte(); + if (-1 == b) + b = 0; // throw new EndOfStreamException(); + m_bits |= ByteMap[b] << m_cached_bits; + m_cached_bits += 8; + if (m_cached_bits < count) + { + b = m_input.ReadByte(); + if (-1 == b) + b = 0; // throw new EndOfStreamException(); + m_bits |= ByteMap[b] << m_cached_bits; + m_cached_bits += 8; + } + } + return m_bits & g_bitMask[count]; + } + + static readonly int[] g_bitMask = { + 0x00000000, 0x00000001, 0x00000003, 0x00000007, 0x0000000F, 0x0000001F, 0x0000003F, 0x0000007F, + 0x000000FF, 0x000001FF, 0x000003FF, 0x000007FF, 0x00000FFF, 0x00001FFF, 0x00003FFF, 0x00007FFF, + 0x0000FFFF, 0x0001FFFF, 0x0003FFFF, 0x0007FFFF, 0x000FFFFF, 0x001FFFFF, 0x003FFFFF, 0x007FFFFF, + 0x00FFFFFF, 0x01FFFFFF, 0x03FFFFFF, 0x07FFFFFF, 0x0FFFFFFF, 0x1FFFFFFF, 0x3FFFFFFF, 0x7FFFFFFF, + }; + + internal static readonly byte[] ByteMap = { + 0x00, 0x80, 0x40, 0xC0, 0x20, 0xA0, 0x60, 0xE0, 0x10, 0x90, 0x50, 0xD0, 0x30, 0xB0, 0x70, 0xF0, + 0x08, 0x88, 0x48, 0xC8, 0x28, 0xA8, 0x68, 0xE8, 0x18, 0x98, 0x58, 0xD8, 0x38, 0xB8, 0x78, 0xF8, + 0x04, 0x84, 0x44, 0xC4, 0x24, 0xA4, 0x64, 0xE4, 0x14, 0x94, 0x54, 0xD4, 0x34, 0xB4, 0x74, 0xF4, + 0x0C, 0x8C, 0x4C, 0xCC, 0x2C, 0xAC, 0x6C, 0xEC, 0x1C, 0x9C, 0x5C, 0xDC, 0x3C, 0xBC, 0x7C, 0xFC, + 0x02, 0x82, 0x42, 0xC2, 0x22, 0xA2, 0x62, 0xE2, 0x12, 0x92, 0x52, 0xD2, 0x32, 0xB2, 0x72, 0xF2, + 0x0A, 0x8A, 0x4A, 0xCA, 0x2A, 0xAA, 0x6A, 0xEA, 0x1A, 0x9A, 0x5A, 0xDA, 0x3A, 0xBA, 0x7A, 0xFA, + 0x06, 0x86, 0x46, 0xC6, 0x26, 0xA6, 0x66, 0xE6, 0x16, 0x96, 0x56, 0xD6, 0x36, 0xB6, 0x76, 0xF6, + 0x0E, 0x8E, 0x4E, 0xCE, 0x2E, 0xAE, 0x6E, 0xEE, 0x1E, 0x9E, 0x5E, 0xDE, 0x3E, 0xBE, 0x7E, 0xFE, + 0x01, 0x81, 0x41, 0xC1, 0x21, 0xA1, 0x61, 0xE1, 0x11, 0x91, 0x51, 0xD1, 0x31, 0xB1, 0x71, 0xF1, + 0x09, 0x89, 0x49, 0xC9, 0x29, 0xA9, 0x69, 0xE9, 0x19, 0x99, 0x59, 0xD9, 0x39, 0xB9, 0x79, 0xF9, + 0x05, 0x85, 0x45, 0xC5, 0x25, 0xA5, 0x65, 0xE5, 0x15, 0x95, 0x55, 0xD5, 0x35, 0xB5, 0x75, 0xF5, + 0x0D, 0x8D, 0x4D, 0xCD, 0x2D, 0xAD, 0x6D, 0xED, 0x1D, 0x9D, 0x5D, 0xDD, 0x3D, 0xBD, 0x7D, 0xFD, + 0x03, 0x83, 0x43, 0xC3, 0x23, 0xA3, 0x63, 0xE3, 0x13, 0x93, 0x53, 0xD3, 0x33, 0xB3, 0x73, 0xF3, + 0x0B, 0x8B, 0x4B, 0xCB, 0x2B, 0xAB, 0x6B, 0xEB, 0x1B, 0x9B, 0x5B, 0xDB, 0x3B, 0xBB, 0x7B, 0xFB, + 0x07, 0x87, 0x47, 0xC7, 0x27, 0xA7, 0x67, 0xE7, 0x17, 0x97, 0x57, 0xD7, 0x37, 0xB7, 0x77, 0xF7, + 0x0F, 0x8F, 0x4F, 0xCF, 0x2F, 0xAF, 0x6F, 0xEF, 0x1F, 0x9F, 0x5F, 0xDF, 0x3F, 0xBF, 0x7F, 0xFF, + }; + } +} diff --git a/ArcFormats/ScrPlayer/ImgControlTable1 b/ArcFormats/ScrPlayer/ImgControlTable1 new file mode 100644 index 0000000000000000000000000000000000000000..844a20caa0e87a97aff541ebb2abb70d9037205d GIT binary patch literal 16384 zcmeHO_nXvI6dlRVg6-5OL*s*|}p;$n{ioIY3vAZGy(xj~*iU^7dcI*W#pknU@ z0Tt{OL`CTc{{k=N=4EC#vzuhIo85Uo&X@0e_uV-;IXSuS(RSHOv$t0KW>lHyo8yg4 z_6~D<^N)%NMpeaa+2@Q*u9A$&{cc}hk{fMg$TsH7Fv+>jKQ%IJUk{&W{cXI#?^Ad< zj0}aFZ2UB`qmeN$fHt{1o$KZ$68|~k0jRL>WIBCX;d7O+ms6jNv+GZRGhn21ot$K3 z;5)>VZ^>mgewaB?;mwzzEg5CiXUCb(%X$yM4p^UFDEqku@qzu{Yrbac=(w-YvJiwvOwYG|KGyx|8T2+c!lKP zSGRtJtiK%b^lrG=?tk~9{jOgRx z^K`D$KRsHlA9f}~>A3~`x5xT6|ED6JeFm-V^-rglD}1h!9Z|?Xp8-3${Y(C7N5lv6 z-@)M@ZnXO!0xPjTKO_SGuS))3bL$uL|AN!MpMUl+-3Ph^^UvPF^~1pq|3FIef0yL{ zEVq6E|IiZ7tdajW*j0*ub_%1|Kl~#9znigs-T6oTvqkd${lvrP>25Cnixq#h>7PG@ zc&Y!dUH%6td_C%a7mI(|SLt8$iopNu`qe)@)a&2XKR>1<|H1nIr;hsn(&Im({@G&= zKbHRK54e6a&;E7!AFTMZ^v_xa>z{s)czTql|E>z3tE9IV>HjUi{`uL653hgvcLe^g z45|O?=({ELe=M#)IsMaTl>X_9w6n**rT-ZI&z4C)JsH<;<~eit=cg(DYSTX(j`+m= zf3>B5K2_;I4gSyG_WEy<{?D$!^_$WBe?P>tt3CY}_&*;|sDHMnU;lCZpLL6%|FHi5 zWNH8B+hPC7>7PBW^v`zX`xodxhX0EmQcv@7{gVF({?G15eA52UHx&CnUl)P@#QmTB zmv;Xb|JUsQ{KT65U(Bmd|7WxO`e&=~{z<3*ivw!$|CzY{OsK|LK1H$MJvuRD|b$SpRSC`G1a&{|Nps9zlH4{?DrC|3@S6 zpSb_?^=bG2M$Z3B{rczq@cv1s|Fbg6|KdkC|E~?<{|%h~#U`Eqz5EyP`=9^&e*@(I zFPe4!e-NKv$-Vy<<^Ll+{$qas=f}(Ze*>;R?fGAPR4xDaOr!s&KmYR&1M>e*HRpdm zqI~l|U(<~9zn=f=`Tv`~=f9r+Tl;4WJ^!!!`+uX(|4&Gx|Lgfb)$@P<{=35cUlI2I zB=!AY&;RA~Bj)};J^#n|2R;8!Z2wGY{ps|3B{h|62V28e#r_O)B%hp8uE5|9bu(^88Oj z<$uws3C#a{>-oPo|JC#V@aI3n?*FT8|G%F9yXTLS_J8rQp8wnDe`BBj)AN7l`vPx1+N&ehMePF=s|>U(wl1yrYKn(@Dv>dS}P|Kn!<<+GZ9(EOje`9I0}S04|V z|1~}T8*csw8UMFa|LUdo|Gb(1hWT%w!u;>1`lmJj_|xC>S8wv?uimHn@)4`3Z_WG% z{%iaHEb*`2=FeZY5dZPd6Y~F|Mfm@k!~d)N`K#9%{@(@ukAD7Wxc{G}{Qr?Z|5oBZ zQN{=~ogJDAhmQzNb=u zmR)ChUAT3nf7dmp*3_BrG1r^-EVkD%eNA;;*EOyi4fFkn)yr;MFFTG`e`5V!tDj6@ z^hILzMH!>7Dz5)s7=77e_2YSvexLK-iP1;TU-n!7$r#4JH~g<*{G;cu#{J+wS;h2U z2L7u#A8&yA?@RvgV*JDT_ay(5ZA||I|1STBK>hcH|2d3*IRC!n|1_rmfq%#U0jU4J z@IQ$059i;L{14}U%>JvN|A#^PzVN?<@ek+Um;7JE^grak&HuJP|F2{AADn+r@;}+c z^gr{r4sRXE6TZ{QHvslbHSo{vH2&p#J;9{|LrEoPSU9Kb-$D`>%ff9|h_A z!v6}!Kb(JG@_!lA|B(OT{BQoxG13`tgJ^v4Y`tM8r&tm+;`S&IN Lr!f6*7ykbMzmZwg literal 0 HcmV?d00001 diff --git a/ArcFormats/ScrPlayer/ImgControlTable32 b/ArcFormats/ScrPlayer/ImgControlTable32 new file mode 100644 index 0000000000000000000000000000000000000000..e46f629e08a71eeeacf66ad8f5c1e6a0e738ec69 GIT binary patch literal 16384 zcmeI2$8N(g5JaWeazrUi@6GAG_g?=0r=pk(0XZaxR?G&9hFP zXGM``-A?lirT1Rl{8r{!ud?;t*Y#q!Sr5(g&HgZv_J`5m?fRML+djD8uDemTPE zSF@J?IHMmXlYV^!=}(*gF-D*M{$VEhk86y78UC*r|Md5-);0K#mrVY_`5*LmeGBEU z(*Mtlf1H1j{*RAL{?5Pk{}PnH3jc?Uf1H1n{@-Qtcm9q4HzXXU<Hh=9KhD2O|L-vQJO9T2E0n(q|MQH0oPUx2_xYbW zf9?Fg4C$-zf6VyD`B&-xJtlwef1m#c^Zzw-{&4<9`aj+=`8)r1|DS{MSLy#v#y`%# RO8;*$`8)r{|NGDRe*lS1uLS@A literal 0 HcmV?d00001 diff --git a/ArcFormats/ScrPlayer/ImgDeltaTable2 b/ArcFormats/ScrPlayer/ImgDeltaTable2 new file mode 100644 index 0000000000000000000000000000000000000000..5c74fce2ae2d0378ce1a9bb99d876ff2203ce57a GIT binary patch literal 16384 zcmeHOg_{&N9L18!M7qd9Y2k{y6)MOwlmIp;1*?FPRzpw*Rzpny0svNrNUX2Q*Ei%m=Mf}ag z`KTR?k@YhpT_`t2_UAwEX{f)&I1({wK)%z|&J@r(Lr zr^D61PXDJ{-1%40Kf9{I|M!Xd58eO&^wK|jpkM#&Cinkm;p*SZ|H(S}|L>}QSO2Gb z{OF$^h^YVnCgT_NAGZI~Got^qM?w6b9Zp~W3IC_dAapcmBii|8Vj@yIa`*P5zg^e{SD@x>1AwuZ@)dXE*;p=l=gBn*L8u{6GID z|2zMGl=bt!Xyt#pTBHA~=Kpz_@qfC>)BjEWPo2N#pZ{Y0`Tv62{!df>U+Gm%8u&RPsMProsOgM#}%a^Z#T0`+s@`#^}mu`hS4@zs*nnXGcx`_uK!` z?PC6?i-Y8U`cL}$&p-b^tI_`z^FQ5?8UN32{-=9Q{+G}H!^!`q|Bs)4nEsz-x&MF5 z^Z(0e`oHP_z4!lNpZ`ZI|Fat!{a-czoBsd9{kLHEe>|N0OA4>m!sW{|+_QvG@ zDCB>3&-DNN{e%DfZ~A|~{jYrg|0(zX%hB|I)Bl_NFTTHWyZ@7a|IhUQ>hHfK{hRy$ zeE%c8|6^OTn*aZ+|9|1Fe|EnZ<$u%vi|_xu`~O$^^S|l;`Tl3K>Hmv(|9?{B`M>G^ P72dz=eE&-_djJ1DAx;>F literal 0 HcmV?d00001 From e0012ce692601dd8d387bb8a3671a57ddc06abef Mon Sep 17 00:00:00 2001 From: morkt Date: Thu, 7 Sep 2023 12:20:01 +0400 Subject: [PATCH 24/36] (Parsley): scn.dat archive format. --- ArcFormats/Software House Parsley/ArcScn.cs | 63 +++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 ArcFormats/Software House Parsley/ArcScn.cs diff --git a/ArcFormats/Software House Parsley/ArcScn.cs b/ArcFormats/Software House Parsley/ArcScn.cs new file mode 100644 index 00000000..9ad9bba9 --- /dev/null +++ b/ArcFormats/Software House Parsley/ArcScn.cs @@ -0,0 +1,63 @@ +//! \file ArcScn.cs +//! \date 2023 Aug 28 +//! \brief Software House Parsley scripts 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 System.Collections.Generic; +using System.ComponentModel.Composition; + +namespace GameRes.Formats.Parsley +{ + [Export(typeof(ArchiveFormat))] + public class ScnDatOpener : ArchiveFormat + { + public override string Tag { get => "DAT/SCN"; } + public override string Description { get => "Software House Parsley scenario archive"; } + public override uint Signature { get => 0; } + public override bool IsHierarchic { get => false; } + public override bool CanWrite { get => false; } + + public override ArcFile TryOpen (ArcView file) + { + if (!VFS.IsPathEqualsToFileName (file.Name, "scn.dat")) + return null; + uint base_offset = 0x1400; + uint index_pos = 0; + var dir = new List(); + while (index_pos < base_offset && file.View.ReadByte (index_pos) != 0) + { + var name = file.View.ReadString (index_pos, 0x20); + var entry = Create (name); + entry.Offset = file.View.ReadUInt32 (index_pos+0x20) + base_offset; + entry.Size = file.View.ReadUInt32 (index_pos+0x24); + if (!entry.CheckPlacement (file.MaxOffset)) + return null; + dir.Add (entry); + index_pos += 0x28; + } + if (dir.Count == 0) + return null; + return new ArcFile (file, this, dir); + } + } +} From e1741d06f79a18aedc461a5188df5357be9275ef Mon Sep 17 00:00:00 2001 From: morkt Date: Thu, 7 Sep 2023 12:20:36 +0400 Subject: [PATCH 25/36] (Unity): decrypt Utage data.dsm script. --- ArcFormats/Unity/ScriptDSM.cs | 87 +++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 ArcFormats/Unity/ScriptDSM.cs diff --git a/ArcFormats/Unity/ScriptDSM.cs b/ArcFormats/Unity/ScriptDSM.cs new file mode 100644 index 00000000..4254a350 --- /dev/null +++ b/ArcFormats/Unity/ScriptDSM.cs @@ -0,0 +1,87 @@ +//! \file ScriptDSM.cs +//! \date 2023 Sep 03 +//! \brief Decrypt data.dsm script. +// +// 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 System; +using System.ComponentModel.Composition; +using System.IO; +using System.Security.Cryptography; +using System.Text; + +namespace GameRes.Formats.NScripter +{ + [Export(typeof(ScriptFormat))] + public class DsmConverter : GenericScriptFormat + { + public override string Tag { get => "DSM/UTAGE"; } + public override string Description { get => "UTAGE Unity engine script file"; } + public override uint Signature { get => 0; } + + public override bool IsScript (IBinaryStream file) + { + return VFS.IsPathEqualsToFileName (file.Name, "data.dsm"); + } + + public override Stream ConvertFrom (IBinaryStream file) + { + using (var reader = new StreamReader (file.AsStream)) + { + var sourceString = reader.ReadToEnd(); + var text = DsmDecryptor.DecryptString (sourceString, "pass"); + return new BinMemoryStream (text, file.Name); + } + } + + public override Stream ConvertBack (IBinaryStream file) + { + throw new NotSupportedException(); + } + } + + internal static class DsmDecryptor + { + internal static void GenerateKeyFromPassword (string password, int keySize, out byte[] key, int blockSize, out byte[] iv) + { + var bytes = Encoding.UTF8.GetBytes ("saltは必ず8バイト以上"); + var derive = new Rfc2898DeriveBytes (password, bytes); + derive.IterationCount = 1000; + key = derive.GetBytes (keySize / 8); + iv = derive.GetBytes (blockSize / 8); + } + + internal static byte[] DecryptString (string sourceString, string password) + { + var rij = new RijndaelManaged(); + byte[] key, iv; + GenerateKeyFromPassword (password, rij.KeySize, out key, rij.BlockSize, out iv); + rij.Key = key; + rij.IV = iv; + var array = Convert.FromBase64String (sourceString); + using (var cryptoTransform = rij.CreateDecryptor()) + { + return cryptoTransform.TransformFinalBlock (array, 0, array.Length); + } + } + } +} From 8c345b5911b00ad52d3efbe25002910a54571629 Mon Sep 17 00:00:00 2001 From: morkt Date: Thu, 7 Sep 2023 12:21:15 +0400 Subject: [PATCH 26/36] Update ArcFormats.csproj --- ArcFormats/ArcFormats.csproj | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/ArcFormats/ArcFormats.csproj b/ArcFormats/ArcFormats.csproj index abb23ea5..7ecc2472 100644 --- a/ArcFormats/ArcFormats.csproj +++ b/ArcFormats/ArcFormats.csproj @@ -106,6 +106,11 @@ + + + + + @@ -137,13 +142,19 @@ + + + + + + - + @@ -602,7 +613,7 @@ - + @@ -746,7 +757,7 @@ - + @@ -827,6 +838,10 @@ + + + + Code @@ -964,7 +979,7 @@ - + @@ -1037,11 +1052,11 @@ - - - - - + + + + + From a2d011878446c3edbaa43aa38da0d1bb5afae3c0 Mon Sep 17 00:00:00 2001 From: morkt Date: Thu, 7 Sep 2023 12:23:55 +0400 Subject: [PATCH 27/36] (Experimental): limited support of EXE file resources. --- Experimental/Experimental.csproj | 2 + Experimental/Microsoft/ArcEXE.cs | 184 +++++++++++++++++++++++++++++++ Experimental/Microsoft/ArcNE.cs | 103 +++++++++++++++++ 3 files changed, 289 insertions(+) create mode 100644 Experimental/Microsoft/ArcEXE.cs create mode 100644 Experimental/Microsoft/ArcNE.cs diff --git a/Experimental/Experimental.csproj b/Experimental/Experimental.csproj index 3240aae5..99177b1c 100644 --- a/Experimental/Experimental.csproj +++ b/Experimental/Experimental.csproj @@ -114,6 +114,8 @@ + + diff --git a/Experimental/Microsoft/ArcEXE.cs b/Experimental/Microsoft/ArcEXE.cs new file mode 100644 index 00000000..336b46e8 --- /dev/null +++ b/Experimental/Microsoft/ArcEXE.cs @@ -0,0 +1,184 @@ +//! \file ArcEXE.cs +//! \date 2023 Aug 24 +//! \brief Access portable executable (PE) resources. +// +// 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.Utility; +using System; +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.IO; +using System.Linq; + +namespace GameRes.Formats.Microsoft +{ + [Export(typeof(ArchiveFormat))] + public class ExeOpener : ArchiveFormat + { + public override string Tag { get => "EXE"; } + public override string Description { get => "Windows executable resources"; } + public override uint Signature { get => 0; } + public override bool IsHierarchic { get => true; } + public override bool CanWrite { get => false; } + + public ExeOpener () + { + Extensions = new[] { "exe",/* "dll"*/ }; + } + + static readonly Dictionary RuntimeTypeMap = new Dictionary() { + { "#2", "RT_BITMAP" }, + { "#10", "RT_RCDATA" }, + }; + + static readonly Dictionary ExtensionTypeMap = new Dictionary() { + { "PNG", ".PNG" }, + { "WAVE", ".WAV" }, + { "MIDS", ".MID" }, + { "SCR", ".BIN" }, + { "#2", ".BMP" }, + { "#10", ".BIN" }, + }; + + public override ArcFile TryOpen (ArcView file) + { + if (!file.View.AsciiEqual (0, "MZ") || VFS.IsVirtual) + return null; + var res = new ExeFile.ResourceAccessor (file.Name); + try + { + var dir = new List(); + foreach (var type in res.EnumTypes()) + { + string dir_name = type; + if (type.StartsWith ("#") && !RuntimeTypeMap.TryGetValue (type, out dir_name)) + continue; + string ext; + if (!ExtensionTypeMap.TryGetValue (type, out ext)) + ext = ""; + foreach (var name in res.EnumNames (type)) + { + string full_name = name; + if (name.StartsWith ("#")) + full_name = IdToString (name); + full_name = string.Join ("/", dir_name, full_name) + ext; + var entry = Create (full_name); + entry.NativeName = name; + entry.NativeType = type; + entry.Offset = 0; + entry.Size = res.GetResourceSize (name, type); + dir.Add (entry); + } + } + if (0 == dir.Count) + { + res.Dispose(); + return null; + } + return new ResourcesArchive (file, this, dir, res); + } + catch + { + res.Dispose(); + throw; + } + } + + public override Stream OpenEntry (ArcFile arc, Entry entry) + { + var rarc = (ResourcesArchive)arc; + var rent = (ResourceEntry)entry; + var data = rarc.Accessor.GetResource (rent.NativeName, rent.NativeType); + if (null == data) + return Stream.Null; + return new BinMemoryStream (data, rent.Name); + } + + public override IImageDecoder OpenImage (ArcFile arc, Entry entry) + { + var rent = (ResourceEntry)entry; + if (rent.NativeType != "#2") + return base.OpenImage (arc, entry); + var rarc = (ResourcesArchive)arc; + var bitmap = new byte[14 + entry.Size]; + int length = rarc.Accessor.ReadResource (rent.NativeName, rent.NativeType, bitmap, 14); + + length += 14; + bitmap[0] = (byte)'B'; + bitmap[1] = (byte)'M'; + LittleEndian.Pack (length, bitmap, 2); + int bits_length = bitmap.ToInt32 (0x22); + int bits_pos = length - bits_length; + if (bits_length == 0) + bits_pos = bitmap.ToInt32 (14) + 22; + LittleEndian.Pack (bits_pos, bitmap, 10); + + var bm = new BinMemoryStream (bitmap, 0, length, entry.Name); + var info = ImageFormat.Bmp.ReadMetaData (bm); + if (null == info) + { + bm.Dispose(); + throw new InvalidFormatException ("Invalid bitmap resource."); + } + bm.Position = 0; + return new ImageFormatDecoder (bm, ImageFormat.Bmp, info); + } + + internal static string IdToString (string id) + { + if (id.Length > 1 && id[0] == '#' && char.IsDigit (id[1])) + id = id.Substring (1).PadLeft (5, '0'); + return id; + } + } + + internal class ResourceEntry : Entry + { + public string NativeName; + public string NativeType; + } + + internal class ResourcesArchive : ArcFile + { + public readonly ExeFile.ResourceAccessor Accessor; + + public ResourcesArchive (ArcView arc, ArchiveFormat impl, ICollection dir, ExeFile.ResourceAccessor acc) + : base (arc, impl, dir) + { + Accessor = acc; + } + + #region IDisposable Members + bool _acc_disposed = false; + protected override void Dispose (bool disposing) + { + if (_acc_disposed) + return; + if (disposing) + Accessor.Dispose(); + _acc_disposed = true; + base.Dispose (disposing); + } + #endregion + } +} diff --git a/Experimental/Microsoft/ArcNE.cs b/Experimental/Microsoft/ArcNE.cs new file mode 100644 index 00000000..ba795e57 --- /dev/null +++ b/Experimental/Microsoft/ArcNE.cs @@ -0,0 +1,103 @@ +//! \file ArcNE.cs +//! \date 2023 Aug 29 +//! \brief Access 16-bit NE executable resources. +// +// 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 System; +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.IO; + +namespace GameRes.Formats.Microsoft +{ + [Export(typeof(ArchiveFormat))] + public class PakOpener : ArchiveFormat + { + public override string Tag { get => "EXE/NE"; } + public override string Description { get => "Windows 16-bit executable resources"; } + public override uint Signature { get => 0; } + public override bool IsHierarchic { get => true; } + public override bool CanWrite { get => false; } + + static readonly Dictionary TypeMap = new Dictionary { + { 1, "RT_CURSOR" }, + { 2, "RT_BITMAP" }, + { 3, "RT_ICON" }, + { 4, "RT_MENU" }, + { 5, "RT_DIALOG" }, + { 6, "RT_STRING" }, + { 10, "RT_DATA" }, + { 11, "RT_MESSAGETABLE" }, + { 16, "RT_VERSION" }, + }; + + public override ArcFile TryOpen (ArcView file) + { + if (!file.View.AsciiEqual (0, "MZ")) + return null; + uint ne_offset = file.View.ReadUInt32 (0x3C); + if (!file.View.AsciiEqual (ne_offset, "NE")) + return null; + uint res_table_offset = file.View.ReadUInt16 (ne_offset+0x24) + ne_offset; + if (res_table_offset <= ne_offset || res_table_offset >= file.MaxOffset) + return null; + int shift = file.View.ReadUInt16 (res_table_offset); + res_table_offset += 2; + var dir = new List(); + while (res_table_offset + 1 < file.MaxOffset) + { + int type_id = file.View.ReadUInt16 (res_table_offset); + if (0 == type_id) + break; + string dir_name = null; + if ((type_id & 0x8000) != 0) + { + type_id &= 0x7FFF; + TypeMap.TryGetValue (type_id, out dir_name); + } + int count = file.View.ReadUInt16 (res_table_offset+2); + res_table_offset += 8; + if (null == dir_name) + dir_name = string.Format ("#{0}", type_id); + for (int i = 0; i < count; ++i) + { + int offset = file.View.ReadUInt16 (res_table_offset) << shift; + uint size = (uint)file.View.ReadUInt16 (res_table_offset+2) << shift; + int res_id = file.View.ReadUInt16 (res_table_offset+6); + res_table_offset += 12; + string name = res_id.ToString ("D5"); + name = string.Join ("/", dir_name, name); + var entry = new Entry { + Name = name, + Offset = offset, + Size = size, + }; + dir.Add (entry); + } + } + if (0 == dir.Count) + return null; + return new ArcFile (file, this, dir); + } + } +} From 78da1d7ab84c0cd5eb341151d0038f719f45cb20 Mon Sep 17 00:00:00 2001 From: morkt Date: Thu, 7 Sep 2023 12:26:07 +0400 Subject: [PATCH 28/36] (SakanaGL): support multiple archives. --- Experimental/SakanaGL/ArcSX.cs | 41 +++++++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/Experimental/SakanaGL/ArcSX.cs b/Experimental/SakanaGL/ArcSX.cs index b88f5e8e..44ef1e65 100644 --- a/Experimental/SakanaGL/ArcSX.cs +++ b/Experimental/SakanaGL/ArcSX.cs @@ -28,13 +28,16 @@ using System; using System.Collections.Generic; using System.ComponentModel.Composition; using System.IO; +using System.Linq; using System.Text; +using System.Text.RegularExpressions; namespace GameRes.Formats.Sakana { internal class SxEntry : PackedEntry { public ushort Flags; + public ushort ArcIndex; public bool IsEncrypted { get { return 0 == (Flags & 0x10); } } } @@ -49,13 +52,23 @@ namespace GameRes.Formats.Sakana public override bool CanWrite { get { return false; } } const uint DefaultKey = 0x2E76034B; + static readonly Regex ArchiveNameRe = new Regex (@"^(.*)-([^-]+)$"); public override ArcFile TryOpen (ArcView file) { - var base_name = Path.GetFileName (file.Name); + var base_name = Path.GetFileNameWithoutExtension (file.Name); var sx_name = base_name.Substring (0, 4) + "(00).sx"; sx_name = VFS.ChangeFileName (file.Name, sx_name); - if (!VFS.FileExists (sx_name) || file.Name.Equals (sx_name, StringComparison.InvariantCultureIgnoreCase)) + if (!VFS.FileExists (sx_name)) + { + var match = ArchiveNameRe.Match (base_name); + if (!match.Success) + return null; + sx_name = VFS.ChangeFileName (file.Name, match.Groups[1] + "(00).sx"); + if (!VFS.FileExists (sx_name)) + return null; + } + if (file.Name.Equals (sx_name, StringComparison.OrdinalIgnoreCase)) return null; byte[] index_data; using (var sx = VFS.OpenView (sx_name)) @@ -80,6 +93,8 @@ namespace GameRes.Formats.Sakana { var reader = new SxIndexDeserializer (index, file.MaxOffset); var dir = reader.Deserialize(); + if (null == dir || dir.Count == 0) + return null; return new ArcFile (file, this, dir); } } @@ -97,7 +112,11 @@ namespace GameRes.Formats.Sakana DecryptData (input, key_lo, key_hi); } if (sx_entry.IsPacked) + { input = UnpackZstd (input); + if (sx_entry.UnpackedSize == 0) + sx_entry.UnpackedSize = (uint)input.Length; + } return new BinMemoryStream (input, entry.Name); } @@ -170,7 +189,7 @@ namespace GameRes.Formats.Sakana m_dir = new List (count); for (int i = 0; i < count; ++i) { - m_index.ReadUInt16(); + ushort arc = Binary.BigEndian (m_index.ReadUInt16()); ushort flags = Binary.BigEndian (m_index.ReadUInt16()); uint offset = Binary.BigEndian (m_index.ReadUInt32()); uint size = Binary.BigEndian (m_index.ReadUInt32()); @@ -179,19 +198,21 @@ namespace GameRes.Formats.Sakana Offset = (long)offset << 4, Size = size, IsPacked = 0 != (flags & 0x03), + ArcIndex = arc, }; - if (!entry.CheckPlacement (m_max_offset)) - return null; m_dir.Add (entry); } - count = Binary.BigEndian (m_index.ReadUInt16()); - for (int i = 0; i < count; ++i) + int arc_count = Binary.BigEndian (m_index.ReadUInt16()); + int arc_index = -1; + for (int i = 0; i < arc_count; ++i) { m_index.ReadUInt32(); m_index.ReadUInt32(); m_index.ReadUInt32(); - Binary.BigEndian (m_index.ReadUInt32()); // archive body length + long arc_size = (long)Binary.BigEndian (m_index.ReadUInt32()) << 4; // archive body length + if (m_max_offset == arc_size) + arc_index = i; m_index.ReadUInt64(); m_index.Seek (16, SeekOrigin.Current); // MD5 sum } @@ -200,6 +221,10 @@ namespace GameRes.Formats.Sakana if (count > 0) m_index.Seek (count * 24, SeekOrigin.Current); DeserializeTree(); + if (arc_count > 1 && arc_index != -1) + { + return m_dir.Where (e => (e as SxEntry).ArcIndex == arc_index).ToList(); + } return m_dir; } From ef5a060ee54abf1a02e9e78ddd8163983cb0411e Mon Sep 17 00:00:00 2001 From: morkt Date: Thu, 7 Sep 2023 12:26:31 +0400 Subject: [PATCH 29/36] Update .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 5d9fb361..433191e0 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ Makefile packages Experimental .nuget +.vs From 97aed46293eeb5c805052146f587d401e097b833 Mon Sep 17 00:00:00 2001 From: morkt Date: Thu, 7 Sep 2023 12:28:24 +0400 Subject: [PATCH 30/36] (ReadString): support UTF-16 encodings. --- GameRes/ArcView.cs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/GameRes/ArcView.cs b/GameRes/ArcView.cs index 66747307..59a04d08 100644 --- a/GameRes/ArcView.cs +++ b/GameRes/ArcView.cs @@ -440,9 +440,22 @@ namespace GameRes { byte* s = m_mem + (offset - m_offset); uint string_length = 0; - while (string_length < size && 0 != s[string_length]) + // enc.WindowsCodePage property might throw an exception for some encodings, while + // CodePage is just a direct field access. + if (enc.CodePage == 1200 || enc.CodePage == 1201) // for UTF-16 encodings stop marker is 2-bytes long { - ++string_length; + ushort* u = (ushort*)s; + while (string_length + 1 < size && 0 != u[string_length >> 1]) + { + string_length += 2; + } + } + else + { + while (string_length < size && 0 != s[string_length]) + { + ++string_length; + } } return new string ((sbyte*)s, 0, (int)string_length, enc); } From a7d9b9c8dd0b4402ad4890678624ec1135839f9c Mon Sep 17 00:00:00 2001 From: morkt Date: Thu, 7 Sep 2023 12:30:14 +0400 Subject: [PATCH 31/36] (FileExists): check filename without directory first. --- GameRes/FileSystem.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/GameRes/FileSystem.cs b/GameRes/FileSystem.cs index 534dc61c..0fa33e6d 100644 --- a/GameRes/FileSystem.cs +++ b/GameRes/FileSystem.cs @@ -263,7 +263,9 @@ namespace GameRes public bool FileExists (string filename) { - return m_dir.ContainsKey (CombinePath (CurrentDirectory, filename)); + return m_dir.ContainsKey (filename) + || !string.IsNullOrEmpty (CurrentDirectory) + && m_dir.ContainsKey (CombinePath (CurrentDirectory, filename)); } public Stream OpenStream (Entry entry) @@ -408,6 +410,8 @@ namespace GameRes Entry entry = null; if (m_dir.TryGetValue (filename, out entry)) return entry; + if (m_dir.TryGetValue (CombinePath (CurrentDirectory, filename), out entry)) + return entry; var dir_name = filename + PathDelimiter; if (m_dir.Keys.Any (n => n.StartsWith (dir_name))) return new SubDirEntry (filename); From e14b7e444ca02f94fbd2c5860d0320d57e9b51b0 Mon Sep 17 00:00:00 2001 From: morkt Date: Thu, 7 Sep 2023 12:30:47 +0400 Subject: [PATCH 32/36] (ImageDecoder.SourceFormat): allow protected set. --- GameRes/ImageDecoder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GameRes/ImageDecoder.cs b/GameRes/ImageDecoder.cs index 0196854b..c5217681 100644 --- a/GameRes/ImageDecoder.cs +++ b/GameRes/ImageDecoder.cs @@ -122,7 +122,7 @@ namespace GameRes protected ImageData m_image; public Stream Source { get { m_input.Position = 0; return m_input.AsStream; } } - public ImageFormat SourceFormat { get { return null; } } + public ImageFormat SourceFormat { get; protected set; } public ImageMetaData Info { get; protected set; } public ImageData Image { get { return m_image ?? (m_image = GetImageData()); } } From e68b7a5edeb2f6c1a4830d90d5b54a8140afcea3 Mon Sep 17 00:00:00 2001 From: morkt Date: Thu, 7 Sep 2023 12:32:52 +0400 Subject: [PATCH 33/36] (BMP): inverted alpha channel. commented out for now, as there's no way to correctly recognize it. --- GameRes/ImageBMP.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/GameRes/ImageBMP.cs b/GameRes/ImageBMP.cs index 9c8af046..8ee0c054 100644 --- a/GameRes/ImageBMP.cs +++ b/GameRes/ImageBMP.cs @@ -220,6 +220,13 @@ namespace GameRes file.Read (pixels, dst, stride); for (int x = 3; !has_alpha && x < stride; x += 4) has_alpha = pixels[dst+x] != 0; + /* // sometimes alpha channel is inverted + for (int x = 3; x < stride; x += 4) + { + pixels[dst+x] ^= 0xFF; + has_alpha = has_alpha || pixels[dst+x] != 0; + } + */ } PixelFormat format = has_alpha ? PixelFormats.Bgra32 : PixelFormats.Bgr32; return ImageData.Create (info, format, null, pixels, stride); From 50168996e15430187774afa8da375d1c383e2c2a Mon Sep 17 00:00:00 2001 From: morkt Date: Thu, 7 Sep 2023 12:33:30 +0400 Subject: [PATCH 34/36] (AboutBox): sort audio formats by tag. --- GUI/AboutBox.xaml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/GUI/AboutBox.xaml b/GUI/AboutBox.xaml index 2ad63b3d..1b5ff33d 100644 --- a/GUI/AboutBox.xaml +++ b/GUI/AboutBox.xaml @@ -46,6 +46,11 @@ IN THE SOFTWARE. + + + + + @@ -126,7 +131,7 @@ IN THE SOFTWARE. - + From 8b23273fa92b004444f0d5cfab7eb2db90579537 Mon Sep 17 00:00:00 2001 From: morkt Date: Thu, 7 Sep 2023 12:34:54 +0400 Subject: [PATCH 35/36] (ImagePreview): better recognize new entry. use entry object instead of its name. --- GUI/ImagePreview.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/GUI/ImagePreview.cs b/GUI/ImagePreview.cs index 43ddf683..774aec29 100644 --- a/GUI/ImagePreview.cs +++ b/GUI/ImagePreview.cs @@ -79,9 +79,9 @@ namespace GARbro.GUI public string Name { get; set; } public Entry Entry { get; set; } - public bool IsEqual (IEnumerable path, string name) + public bool IsEqual (IEnumerable path, Entry entry) { - return Path != null && path.SequenceEqual (Path) && name.Equals (Name); + return Path != null && path.SequenceEqual (Path) && Entry == entry; } } @@ -139,7 +139,7 @@ namespace GARbro.GUI /// private void PreviewEntry (Entry entry) { - if (m_current_preview.IsEqual (ViewModel.Path, entry.Name)) + if (m_current_preview.IsEqual (ViewModel.Path, entry)) return; UpdatePreviewPane (entry); } From 419a5f4e31b7ec99a1cb95a15060e873be8aa324 Mon Sep 17 00:00:00 2001 From: morkt Date: Thu, 7 Sep 2023 12:47:22 +0400 Subject: [PATCH 36/36] (Legacy): bunch of formats. (GSX): K5 archives + K4 images. (HyperWorks): G images. (IDA): better support packed entries. (Logg): ARF, MBM archives, FRM images. (Omi): DAT archives. (Rare): X archives. (RHSS): 'CRG' archives. (SplushWave): better SWG images support. --- Legacy/Gsx/ArcK3.cs | 2 +- Legacy/Gsx/ArcK5.cs | 70 ++++++ Legacy/Gsx/ImageK4.cs | 214 ++++++++++++++++- Legacy/HyperWorks/ImageG.cs | 421 ++++++++++++++++++++++++++++++++++ Legacy/HyperWorks/ImageI24.cs | 49 ++-- Legacy/Inspire/ArcIDA.cs | 29 ++- Legacy/Legacy.csproj | 8 + Legacy/Logg/ArcARF.cs | 141 ++++++++++++ Legacy/Logg/ArcMBM.cs | 28 ++- Legacy/Logg/ImageFRM.cs | 63 +++++ Legacy/Omi/ArcDAT.cs | 213 +++++++++++++++++ Legacy/Rare/ArcX.cs | 132 +++++++++++ Legacy/Rhss/ArcCRG.cs | 83 +++++++ Legacy/SplushWave/ArcDAT.cs | 90 ++++++++ Legacy/SplushWave/ImageSWG.cs | 128 +++++------ 15 files changed, 1564 insertions(+), 107 deletions(-) create mode 100644 Legacy/Gsx/ArcK5.cs create mode 100644 Legacy/HyperWorks/ImageG.cs create mode 100644 Legacy/Logg/ArcARF.cs create mode 100644 Legacy/Logg/ImageFRM.cs create mode 100644 Legacy/Omi/ArcDAT.cs create mode 100644 Legacy/Rare/ArcX.cs create mode 100644 Legacy/Rhss/ArcCRG.cs diff --git a/Legacy/Gsx/ArcK3.cs b/Legacy/Gsx/ArcK3.cs index 00f12ea0..e9972854 100644 --- a/Legacy/Gsx/ArcK3.cs +++ b/Legacy/Gsx/ArcK3.cs @@ -26,7 +26,7 @@ using System.Collections.Generic; using System.ComponentModel.Composition; -// [000225][Light Plan] My Fairink Yousei Byakuya Monogatari +// [000225][Light Plan] My Fair Link Yousei Byakuya Monogatari namespace GameRes.Formats.Gsx { diff --git a/Legacy/Gsx/ArcK5.cs b/Legacy/Gsx/ArcK5.cs new file mode 100644 index 00000000..82787c6a --- /dev/null +++ b/Legacy/Gsx/ArcK5.cs @@ -0,0 +1,70 @@ +//! \file ArcK5.cs +//! \date 2023 Aug 27 +//! \brief GSX engine 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 System.Collections.Generic; +using System.ComponentModel.Composition; +using System.IO; +using System.Text; + +namespace GameRes.Formats.Gsx +{ + [Export(typeof(ArchiveFormat))] + public class K5Opener : ArchiveFormat + { + public override string Tag => "K5"; + public override string Description => "GSX engine resource archive"; + public override uint Signature => 0x01354B; // 'K5' + public override bool IsHierarchic => true; + public override bool CanWrite => false; + + public K5Opener () + { + ContainedFormats = new[] { "K4", "OGG" }; + } + + public override ArcFile TryOpen (ArcView file) + { + int count = file.View.ReadInt32 (4); + if (!IsSaneCount (count)) + return null; + uint index_offset = file.View.ReadUInt32 (8); + var dir = new List (count); + for (int i = 0; i < count; ++i) + { + var dir_name = file.View.ReadString (index_offset, 0x80, Encoding.Unicode); + var name = file.View.ReadString (index_offset+0x80, 0x40, Encoding.Unicode); + name = Path.Combine (dir_name, name); + var entry = Create (name); + entry.Offset = file.View.ReadUInt32 (index_offset+0xC8); + entry.Size = file.View.ReadUInt32 (index_offset+0xCC); + if (!entry.CheckPlacement (file.MaxOffset)) + return null; + dir.Add (entry); + index_offset += 0x100; + } + return new ArcFile (file, this, dir); + } + } +} diff --git a/Legacy/Gsx/ImageK4.cs b/Legacy/Gsx/ImageK4.cs index 93c3dca8..79866dc8 100644 --- a/Legacy/Gsx/ImageK4.cs +++ b/Legacy/Gsx/ImageK4.cs @@ -1,8 +1,8 @@ //! \file ImageK4.cs -//! \date 2019 Feb 07 -//! \brief Toyo GSX image format. +//! \date 2023 Aug 27 +//! \brief GSX engine image format. // -// Copyright (C) 2019 by morkt +// 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 @@ -23,6 +23,8 @@ // IN THE SOFTWARE. // +using GameRes.Utility; +using System; using System.ComponentModel.Composition; using System.IO; using System.Windows.Media; @@ -33,16 +35,16 @@ namespace GameRes.Formats.Gsx { internal class K4MetaData : ImageMetaData { - public bool HasAlpha; + public byte AlphaMode; public int FrameCount; } [Export(typeof(ImageFormat))] public class K4Format : ImageFormat { - public override string Tag { get { return "K4"; } } - public override string Description { get { return "Toyo GSX image format"; } } - public override uint Signature { get { return 0x0201344B; } } // 'K4' + public override string Tag => "K4"; + public override string Description => "GSX engine image format"; + public override uint Signature => 0x0201344B; // 'K4' public override ImageMetaData ReadMetaData (IBinaryStream file) { @@ -51,20 +53,22 @@ namespace GameRes.Formats.Gsx return null; if (header[2] != 1 || header[3] != 2) return null; + int frame_count = header.ToInt16 (0xC); + if (frame_count <= 0) + return null; return new K4MetaData { Width = header.ToUInt16 (4), Height = header.ToUInt16 (6), BPP = header[0xF], - HasAlpha = header[0xB] != 0, - FrameCount = header.ToUInt16 (0xC), + AlphaMode = header[0xB], + FrameCount = frame_count, }; } public override ImageData Read (IBinaryStream file, ImageMetaData info) { - var meta = (K4MetaData)info; - - return ImageData.Create (info, format, palette, pixels); + var reader = new K4Reader (file, (K4MetaData)info); + return reader.Unpack(); } public override void Write (Stream file, ImageData image) @@ -72,4 +76,190 @@ namespace GameRes.Formats.Gsx throw new System.NotImplementedException ("K4Format.Write not implemented"); } } + + internal sealed class K4Reader + { + IBinaryStream m_input; + K4MetaData m_info; + + public K4Reader (IBinaryStream input, K4MetaData info) + { + m_input = input; + m_info = info; + } + + int m_stride; + int m_pixel_size; + + public ImageData Unpack () + { + uint base_offset = 0x30; + m_input.Position = base_offset; + m_info.Width = m_input.ReadUInt16(); + m_info.Height = m_input.ReadUInt16(); + m_input.Seek (8, SeekOrigin.Current); + int bpp = m_input.ReadUInt16(); + int flags = m_input.ReadUInt16(); + m_input.Seek (4, SeekOrigin.Current); + uint alpha_pos = m_input.ReadUInt32(); + m_input.Seek (12, SeekOrigin.Current); + int ctl_length = m_input.ReadInt32(); + + m_pixel_size = bpp / 8; + m_stride = (m_info.iWidth * m_pixel_size + 3) & ~3; + var pixels = new byte[m_info.iHeight * m_stride]; + int dst = 0; + bool do_delta = (flags & 1) != 0; + var control_bytes = m_input.ReadBytes (ctl_length - 0x10); + using (var mem = new MemoryStream (control_bytes)) + using (var ctl = new MsbBitStream (mem)) + using (var data = new MsbBitStream (m_input.AsStream, true)) + { + while (dst < pixels.Length) + { + int b = ctl.GetNextBit(); + if (-1 == b) + break; + if (b != 0) + { + if (!do_delta) + { + pixels[dst++] = (byte)data.GetBits (8); + } + else if (dst >= m_pixel_size) + { + pixels[dst] = (byte)(pixels[dst - m_pixel_size] + data.GetBits (9) + 1); + ++dst; + } + else + { + pixels[dst++] = (byte)data.GetBits (9); + } + } + else + { + int pos, count; + if (ctl.GetNextBit() != 0) + { + pos = data.GetBits (14); + count = data.GetBits (4) + 3; + } + else + { + pos = data.GetBits (9); + count = data.GetBits (3) + 2; + } + int src = dst - pos - 1; + count = Math.Min (count, pixels.Length - dst); + if (!do_delta || dst < m_pixel_size) + { + Binary.CopyOverlapped (pixels, src, dst, count); + dst += count; + } + else + { + while (count --> 0) + { + pixels[dst++] = (byte)(pixels[src] + pixels[src + pos - m_pixel_size + 1] - pixels[src - m_pixel_size]); + ++src; + } + } + } + } + } + if (0 == alpha_pos) + { + if (24 == bpp) + return ImageData.CreateFlipped (m_info, PixelFormats.Bgr24, null, pixels, m_stride); + else + return ImageData.CreateFlipped (m_info, PixelFormats.Bgr32, null, pixels, m_stride); + } + if (0xFF == m_info.AlphaMode) + pixels = UnpackAlphaFF (alpha_pos + base_offset, pixels); + else if (0xFE == m_info.AlphaMode) + pixels = UnpackAlphaFE (alpha_pos + base_offset, pixels); + else + throw new NotSupportedException (string.Format ("Not supported alpha channel mode 0x{0:X2}", m_info.AlphaMode)); + m_stride = m_info.iWidth * 4; + return ImageData.Create (m_info, PixelFormats.Bgra32, null, pixels, m_stride); + } + + byte[] UnpackAlphaFF (uint alpha_pos, byte[] pixels) + { + m_input.Position = alpha_pos; + var offsets = new int[m_info.iHeight]; + for (int i = 0; i < offsets.Length; ++i) + offsets[i] = m_input.ReadInt32(); + + var output = new byte[m_info.iWidth * m_info.iHeight * 4]; + int dst = 0; + for (int y = 0; y < m_info.iHeight; y++) + { + m_input.Position = alpha_pos + offsets[y]; + int src = (m_info.iHeight - y - 1) * m_stride; + int dst_a = dst + 3; + for (int x = 0; x < m_info.iWidth; ++x) + { + output[dst ] = pixels[src ]; + output[dst+1] = pixels[src+1]; + output[dst+2] = pixels[src+2]; + dst += 4; + src += m_pixel_size; + } + for (int x = 0; x < m_info.iWidth; ) + { + byte alpha = m_input.ReadUInt8(); + int count = m_input.ReadUInt8(); + count = Math.Min (count, m_info.iWidth - x); + x += count; + if (alpha > 0) + { + alpha = (byte)((alpha * 0xFF) >> 7); + while (count --> 0) + { + output[dst_a] = alpha; + dst_a += 4; + } + } + else + { + dst_a += 4 * count; + } + } + } + return output; + } + + byte[] UnpackAlphaFE (uint alpha_pos, byte[] pixels) + { + m_input.Position = alpha_pos; + var output = new byte[m_info.iWidth * m_info.iHeight * 4]; + int dst = 0; + for (int y = 0; y < m_info.iHeight; y++) + { + int src = (m_info.iHeight - y - 1) * m_stride; + int dst_a = dst + 3; + for (int x = 0; x < m_info.iWidth; ++x) + { + output[dst ] = pixels[src ]; + output[dst+1] = pixels[src+1]; + output[dst+2] = pixels[src+2]; + dst += 4; + src += m_pixel_size; + } + for (int x = 0; x < m_info.iWidth; x += 8) + { + byte alpha = m_input.ReadUInt8(); + int count = Math.Min (8, m_info.iWidth - x); + for (int i = 0; i < count; ++i) + { + output[dst_a] = (byte)-(alpha & 1); + dst_a += 4; + alpha >>= 1; + } + } + } + return output; + } + } } diff --git a/Legacy/HyperWorks/ImageG.cs b/Legacy/HyperWorks/ImageG.cs new file mode 100644 index 00000000..0e83d113 --- /dev/null +++ b/Legacy/HyperWorks/ImageG.cs @@ -0,0 +1,421 @@ +//! \file ImageG.cs +//! \date 2023 Aug 28 +//! \brief HyperWorks image format. +// +// 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.Utility; +using System; +using System.ComponentModel.Composition; +using System.IO; +using System.Windows.Media; +using System.Windows.Media.Imaging; + +// [951207][Love Gun] ACE OF SPADES +// I24 predecessor + +namespace GameRes.Formats.HyperWorks +{ + [Export(typeof(ImageFormat))] + public class GFormat : ImageFormat + { + public override string Tag => "G"; + public override string Description => "HyperWorks indexed image format"; + public override uint Signature => 0x1A477D00; + + public GFormat () + { + Signatures = new[] { 0x1A477D00u, 0u }; + } + + public override ImageMetaData ReadMetaData (IBinaryStream file) + { + var header = file.ReadHeader (12); + if (header.ToUInt16 (2) != 0x1A47) + return null; + // not sure if 0x7D00 is a required signature, so rely on filename + if (header.ToUInt16 (0) != 0x7D00 && !file.Name.HasExtension (".G")) + return null; + return new ImageMetaData { + Width = header.ToUInt16 (8), + Height = header.ToUInt16 (10), + OffsetX = header.ToInt16 (4), + OffsetY = header.ToInt16 (6), + BPP = 8, + }; + } + + public override ImageData Read (IBinaryStream file, ImageMetaData info) + { + var reader = new GReader (file, info); + return reader.Unpack(); + } + + public override void Write (Stream file, ImageData image) + { + throw new System.NotImplementedException ("GFormat.Write not implemented"); + } + } + + internal sealed class GReader + { + IBinaryStream m_input; + ImageMetaData m_info; + int m_stride; + byte[] m_palette_data; + byte[] m_output; + ushort[] m_buffer; + int[] m_line_ptr; + + public GReader (IBinaryStream input, ImageMetaData info) + { + m_input = input; + m_info = info; + m_stride = (m_info.iWidth * m_info.BPP / 8 + 1) & -2; + int s = Math.Max (0x142, m_stride / 2 + 2); // line buffer size + m_buffer = new ushort[s * 3 + 1]; + m_line_ptr = new int[3] { 1, 1 + s, 1 + s*2 }; + } + + public ImageData Unpack () + { + m_input.Position = 0x0C; + m_palette_data = m_input.ReadBytes (0x30); + m_input.Position = 0x40; + int width = ((m_info.iWidth + 7) & -8); + int rows = ((m_info.iHeight + 1) & -2); + m_output = new byte[m_stride * rows]; + InitColorTable(); + int blockW = width >> 1; + int blockH = rows >> 1; + SetupBitReader(); + for (int y = 0; y < blockH; ++y) + { + var dst = m_line_ptr[2]; + m_line_ptr[2] = m_line_ptr[1]; + m_line_ptr[1] = m_line_ptr[0]; + m_line_ptr[0] = dst; + int x = 0; + while (x < blockW) + { + if (GetNextBit() != 0) + { + m_buffer[dst++] = GetColorFromTable (x); + ++x; + } + else + { + int count = ExtractBits (BitTable1); + if (count >= 0x40) + count += ExtractBits (BitTable1); + int idx = ExtractBits (BitTable2) * 2; + int src = m_line_ptr[OffTable[idx + 1]]; + src += OffTable[idx] + x; + x += count; + while (count --> 0) + { + m_buffer[dst++] = m_buffer[src++]; + } + } + } + UnpackRow (y, blockW, m_line_ptr[0]); + } + var palette = UnpackPalette(); + return ImageData.Create (m_info, PixelFormats.Indexed8, palette, m_output, m_stride); + } + + void UnpackRow (int y, int width, int buf_pos) + { + int row1 = m_stride * y * 2; + int row2 = row1 + m_stride; + for (int i = 0; i < width; ++i) + { + ushort v = m_buffer[buf_pos++]; + ushort v0 = (ushort)((v & 0xF00 | ((v & 0xF000) >> 12)) + 0xA0A); + LittleEndian.Pack (v0, m_output, row2); + row2 += 2; + ushort v1 = (ushort)((((v & 0xF) << 8) | ((v & 0xF0) >> 4)) + 0xA0A); + LittleEndian.Pack (v1, m_output, row1); + row1 += 2; + } + } + + byte[] g_palIndexes = { 0, 1, 4, 5, 2, 3, 6, 7, 8, 9, 0xC, 0xD, 0xA, 0xB, 0xE, 0xF }; + + BitmapPalette UnpackPalette () + { + var colors = new Color[42]; + for (int i = 0; i < 16; ++i) + { + int R = m_palette_data[3 * g_palIndexes[i] ]; R |= R << 4; + int G = m_palette_data[3 * g_palIndexes[i] + 1]; G |= G << 4; + int B = m_palette_data[3 * g_palIndexes[i] + 2]; B |= B << 4; + colors[i+10] = Color.FromRgb ((byte)R, (byte)G, (byte)B); + + int b = R & 1; + int c = (sbyte)R >> 1; + if (c < 0) + c += b; + colors[i+26].R = (byte)c; + + b = G & 1; + c = (sbyte)G >> 1; + if (c < 0) + c += b; + colors[i+26].G = (byte)c; + + b = B & 1; + c = (sbyte)B >> 1; + if (c < 0) + c += b; + colors[i+26].B = (byte)c; + } + return new BitmapPalette (colors); + } + + byte[] g_colorTable = new byte[256]; + + void InitColorTable () + { + int dst = 0; + for (int i = 0; i < 16; ++i) + for (int j = 0; j < 16; ++j) + g_colorTable[dst++] = (byte)((j + i + 1) & 0xF); + } + + ushort GetColorFromTable (int x) + { + ushort b0 = m_buffer[m_line_ptr[1] + x]; + int n0 = b0 & 0xF; + int n1 = (b0 >> 4) & 0xF; + int n2 = (b0 >> 8) & 0xF; + int n3 = (b0 >> 12) & 0xF; + + ushort b1 = m_buffer[m_line_ptr[1] + x - 1]; + int m0 = b1 & 0xF; + int m1 = (b1 >> 4) & 0xF; + int m2 = (b1 >> 8) & 0xF; + int m3 = (b1 >> 12) & 0xF; + + ushort b2 = m_buffer[m_line_ptr[0] + x - 1]; + int p0 = b2 & 0xF; + int p1 = (b2 >> 4) & 0xF; + int p2 = (b2 >> 8) & 0xF; + int p3 = (b2 >> 12) & 0xF; + + int r1 = n1; + if (n1 != n3 && (n1 != m1 || n1 != p1)) + { + if (p0 == p1) + r1 = p0; + else + r1 = m2; + } + if (GetNextBit() != 0) + r1 = AdjustColorTable (r1); + + int r0 = n0; + if (n0 != n2 && (n0 != m0 || n0 != p0)) + { + if (r1 == p1) + r0 = p1; + else + r0 = n3; + } + if (GetNextBit() != 0) + r0 = AdjustColorTable (r0); + + int r3 = n3; + if (r1 != n3 && (n3 != m3 || n3 != p3)) + { + if (p2 == p3) + r3 = p2; + else + r3 = p0; + } + if (GetNextBit() != 0) + r3 = AdjustColorTable (r3); + + int r2 = n2; + if (n2 != r0 && (n2 != m2 || n2 != p2)) + { + if (p3 == r3) + r2 = p3; + else + r2 = r1; + } + if (GetNextBit() != 0) + r2 = AdjustColorTable (r2); + + return (ushort)((r3 << 12) | (r2 << 8) | (r1 << 4) | r0); + } + + byte AdjustColorTable (int idx) + { + int shift_count = ExtractBits (BitTable3); + int i = 16 * idx + shift_count; + byte c = g_colorTable[i]; + if (shift_count != 0) + { + while (shift_count --> 0) + { + g_colorTable[i] = g_colorTable[i-1]; + --i; + } + g_colorTable[i] = c; + } + return c; + } + + int ExtractBits (byte[] table) + { + int idx = ((bits >> 8) & 0xFF) << 1; + int n = table[idx]; + if (n != 0) + { + if (n >= bitCount) + { + n -= bitCount; + bits <<= bitCount; + int b = m_input.ReadByte(); + if (b != -1) // XXX ignore EOF + bits |= b; + bitCount = 8; + } + bits <<= n; + bitCount -= n; + return table[idx+1]; + } + else + { + bits <<= bitCount; + int b = m_input.ReadByte(); + if (b != -1) // XXX ignore EOF + bits |= b; + bits <<= 8 - bitCount; + int t = table[idx+1]; + do + { + int i = GetNextBit(); + t = OffTable[2 * t + 54 + i]; + } + while (OffTable[2 * t + 54] != 0); + return OffTable[2 * t + 55]; + } + } + + int bits; + int bitCount; + + private void SetupBitReader () + { + bits = m_input.ReadUInt8() << 8; + bits |= m_input.ReadUInt8(); + bitCount = 8; + } + + private int GetNextBit () + { + bits <<= 1; + if (0 == --bitCount) + { + int b = m_input.ReadByte(); + if (b != -1) // XXX ignore EOF + bits |= b; + bitCount = 8; + } + return (bits >> 16) & 1; + } + + static readonly byte[] BitTable1 = new byte[] { + 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, + 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 8, + 0xD, 0, 0, 0, 1, 8, 0xE, 6, 7, 6, 7, 6, 7, 6, 7, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 7, 0xA, 7, 0xA, 0, 2, 0, 7, 7, 0xB, 7, 0xB, 8, 0xF, 0, 3, 6, 8, 6, 8, 6, 8, 6, 8, 8, 0x10, 0, 4, + 0, 5, 8, 0x11, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, + 4, 4, 4, 4, 5, 6, 5, 6, 5, 6, 5, 6, 5, 6, 5, 6, 5, 6, 5, 6, 7, 0xC, 7, 0xC, 0, 8, 0, 6, 6, 9, 6, + 9, 6, 9, 6, 9, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + }; + static readonly byte[] BitTable2 = new byte[] { + 5, 4, 5, 4, 5, 4, 5, 4, 5, 4, 5, 4, 5, 4, 5, 4, 7, 0x17, 7, 0x17, 7, 0x16, 7, 0x16, 6, 0x0E, 6, + 0x0E, 6, 0x0E, 6, 0x0E, 4, 3, 4, 3, 4, 3, 4, 3, 4, 3, 4, 3, 4, 3, 4, 3, 4, 3, 4, 3, 4, 3, 4, 3, 4, + 3, 4, 3, 4, 3, 4, 3, 6, 0x0D, 6, 0x0D, 6, 0x0D, 6, 0x0D, 6, 0x0C, 6, 0x0C, 6, 0x0C, 6, 0x0C, 5, 6, + 5, 6, 5, 6, 5, 6, 5, 6, 5, 6, 5, 6, 5, 6, 5, 7, 5, 7, 5, 7, 5, 7, 5, 7, 5, 7, 5, 7, 5, 7, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 8, 5, 8, 5, 8, 5, 8, 5, 8, 5, 8, 5, 8, 5, 8, 7, 0x14, 7, + 0x14, 7, 0x18, 7, 0x18, 6, 0x10, 6, 0x10, 6, 0x10, 6, 0x10, 6, 0x11, 6, 0x11, 6, 0x11, 6, 0x11, 6, + 0x0F, 6, 0x0F, 6, 0x0F, 6, 0x0F, 5, 0x0A, 5, 0x0A, 5, 0x0A, 5, 0x0A, 5, 0x0A, 5, 0x0A, 5, 0x0A, 5, + 0x0A, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, + 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, + 6, 0x13, 6, 0x13, 6, 0x13, 6, 0x13, 6, 0x12, 6, 0x12, 6, 0x12, 6, 0x12, 5, 0x0B, 5, 0x0B, 5, 0x0B, + 5, 0x0B, 5, 0x0B, 5, 0x0B, 5, 0x0B, 5, 0x0B, 7, 0x19, 7, 0x19, 7, 0x1A, 7, 0x1A, 6, 0x15, 6, 0x15, + 6, 0x15, 6, 0x15, 5, 9, 5, 9, 5, 9, 5, 9, 5, 9, 5, 9, 5, 9, 5, 9, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, + 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, + 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 2, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, + 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 0, + 2, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, + 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 0, + 2, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 0, + }; + static readonly byte[] BitTable3 = new byte[] { + 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 5, + 4, 5, 4, 5, 4, 5, 4, 5, 4, 5, 4, 5, 4, 5, 4, 6, 6, 6, 6, 6, 6, 6, 6, 7, 8, 7, 8, 8, 0x0A, 8, 0x0B, + 4, 3, 4, 3, 4, 3, 4, 3, 4, 3, 4, 3, 4, 3, 4, 3, 4, 3, 4, 3, 4, 3, 4, 3, 4, 3, 4, 3, 4, 3, 4, 3, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 7, 6, 7, 6, 7, 6, 7, 7, 9, 7, 9, 0, 0x5F, 8, 0x0C, + 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, + 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, + 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, + 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 1, 0, 1, 0, + 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, + 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, + 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, + 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, + 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, + 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, + 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, + 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, + }; + static readonly short[] OffTable = new short[] { + 0, 1, 1, 1, -1, 0, -1, 1, -4, 0, -2, 0, 2, 1, -2, 1, 4, 1, 0, 2, 1, 2, -1, 2, -8, 0, -4, 1, 8, 1, + -2, 2, 2, 2, -4, 2, 4, 2, 8, 2, -8, 1, -0x10, 0, -8, 2, 0x10, 1, 0x10, 2, -0x10, 1, -0x10, 2, + 0x2C, 9, 0x2D, 0x0A, 0x2E, 0x0B, 0x2F, 0x0C, 0x30, 0x0D, 0x31, 0x32, 0x0E, 0x33, 0x0F, 0x10, 0x11, + 0x12, 0x13, 0x14, 0x34, 0x15, 0x37, 0x35, 0x16, 0x38, 0x17, 0x39, 0x3D, 0x18, 0x19, 0x36, 0x1A, + 0x1B, 0x3A, 0x3C, 0x1C, 0x3B, 0x1D, 0x3E, 0x3F, 0x1E, 0x40, 0x1F, 0x45, 0x46, 0x43, 0x20, 0x21, + 0x48, 0x22, 0x41, 0x23, 0x42, 0x24, 0x25, 0x44, 0x47, 0x4F, 0x4A, 0x49, 0x53, 0x4B, 0x4D, 0x57, + 0x52, 0x59, 0x58, 0x4E, 0x50, 0x56, 0x26, 0x54, 0x4C, 0x51, 0x55, 0x27, 0x28, 0x5A, 0x2B, 0x29, + 0x5B, 0x2A, 0x5C, 0x5D, 0x5E, 0, 0, 0, 0x12, 0, 0x13, 0, 0x14, 0, 0x15, 0, 0x16, 0, 0x17, 0, 0x18, + 0, 0x19, 0, 0x1A, 0, 0x1B, 0, 0x1C, 0, 0x1D, 0, 0x1E, 0, 0x1F, 0, 0x20, 0, 0x21, 0, 0x22, 0, 0x23, + 0, 0x24, 0, 0x25, 0, 0x26, 0, 0x27, 0, 0x28, 0, 0x29, 0, 0x2A, 0, 0x2B, 0, 0x2C, 0, 0x2D, 0, 0x2E, + 0, 0x2F, 0, 0x30, 0, 0x31, 0, 0x32, 0, 0x33, 0, 0x34, 0, 0x35, 0, 0x36, 0, 0x37, 0, 0x38, 0, 0x39, + 0, 0x3A, 0, 0x3B, 0, 0x3C, 0, 0x3D, 0, 0x3E, 0, 0x3F, 0, 0x40, 0, 0x80, 0, 0x0C0, 0, 0x100, 0, + 0x140, 0x60, 0x61, 0, 0x0D, 0, 0x0E, + }; + } +} diff --git a/Legacy/HyperWorks/ImageI24.cs b/Legacy/HyperWorks/ImageI24.cs index ea822005..6b905437 100644 --- a/Legacy/HyperWorks/ImageI24.cs +++ b/Legacy/HyperWorks/ImageI24.cs @@ -23,22 +23,32 @@ // IN THE SOFTWARE. // -using GameRes.Utility; using System; using System.ComponentModel.Composition; using System.IO; using System.Windows.Media; +// [970905][Ucom] Winter Kiss // [980626][Love Gun] ACE OF SPADES 2 namespace GameRes.Formats.HyperWorks { + internal class I24MetaData : ImageMetaData + { + public byte Version; + } + [Export(typeof(ImageFormat))] public class I24Format : ImageFormat { - public override string Tag { get { return "I24"; } } - public override string Description { get { return "HyperWorks image format"; } } - public override uint Signature { get { return 0x41343249; } } // 'I24A' + public override string Tag => "I24"; + public override string Description => "HyperWorks RGB image format"; + public override uint Signature => 0x41343249; // 'I24A' + + public I24Format () + { + Signatures = new[] { 0x41343249u, 0x20343249u }; + } public override ImageMetaData ReadMetaData (IBinaryStream file) { @@ -46,16 +56,17 @@ namespace GameRes.Formats.HyperWorks int bpp = header.ToInt16 (0x10); if (bpp != 24) return null; - return new ImageMetaData { + return new I24MetaData { Width = header.ToUInt16 (0xC), Height = header.ToUInt16 (0xE), BPP = bpp, + Version = header[3], }; } public override ImageData Read (IBinaryStream file, ImageMetaData info) { - var reader = new I24Decoder (file, info); + var reader = new I24Decoder (file, (I24MetaData)info); return reader.Unpack(); } @@ -68,9 +79,9 @@ namespace GameRes.Formats.HyperWorks internal class I24Decoder { IBinaryStream m_input; - ImageMetaData m_info; + I24MetaData m_info; - public I24Decoder (IBinaryStream input, ImageMetaData info) + public I24Decoder (IBinaryStream input, I24MetaData info) { m_input = input; m_info = info; @@ -170,14 +181,24 @@ namespace GameRes.Formats.HyperWorks short s2 = shift_table[shift_idx + 1]; if (shift_token != 0) { - while (shift_token --> 0) + if (m_info.Version == 'A') { - shift_table[shift_idx] = shift_table[shift_idx - 2]; - shift_table[shift_idx+1] = shift_table[shift_idx - 1]; - shift_idx -= 2; + while (shift_idx > 0) + { + shift_table[shift_idx] = shift_table[shift_idx - 2]; + shift_table[shift_idx+1] = shift_table[shift_idx - 1]; + shift_idx -= 2; + } + shift_table[0] = s1; + shift_table[1] = s2; + } + else + { + shift_table[shift_idx ] = shift_table[shift_idx - 2]; + shift_table[shift_idx + 1] = shift_table[shift_idx - 1]; + shift_table[shift_idx - 2] = s1; + shift_table[shift_idx - 1] = s2; } - shift_table[0] = s1; - shift_table[1] = s2; } int src = 4 * (x + s1); if (color_token >= 216) diff --git a/Legacy/Inspire/ArcIDA.cs b/Legacy/Inspire/ArcIDA.cs index 21d92289..a5e767ee 100644 --- a/Legacy/Inspire/ArcIDA.cs +++ b/Legacy/Inspire/ArcIDA.cs @@ -30,19 +30,20 @@ using System.IO; using System.Text; using GameRes.Compression; +// [971205][Azlocks] Isle Mystique // [991001][Inspire] days innocent // [000707][inspire] ambience namespace GameRes.Formats.Inspire { - internal class IdaEntry : Entry + internal class IdaEntry : PackedEntry { public uint Flags; public uint Key; } [Export(typeof(ArchiveFormat))] - public class PakOpener : ArchiveFormat + public class IdaOpener : ArchiveFormat { public override string Tag { get { return "IDA"; } } public override string Description { get { return "Inspire resource archive"; } } @@ -50,6 +51,11 @@ namespace GameRes.Formats.Inspire public override bool IsHierarchic { get { return false; } } public override bool CanWrite { get { return false; } } + public IdaOpener () + { + Extensions = new[] { "ida", "mha" }; + } + public override ArcFile TryOpen (ArcView file) { int version = file.View.ReadInt32 (4); @@ -58,8 +64,9 @@ namespace GameRes.Formats.Inspire using (var index = file.CreateStream()) { var dir = new List(); + bool has_packed = false; long index_pos = 8; - for (;;) + do { index.Position = index_pos; uint entry_length = index.ReadUInt32(); @@ -76,15 +83,27 @@ namespace GameRes.Formats.Inspire var entry = FormatCatalog.Instance.Create (name); entry.Offset = offset; - entry.Size = size; - if (!entry.CheckPlacement (file.MaxOffset)) + entry.Size = entry.UnpackedSize = size; + if (offset > file.MaxOffset || offset < index_pos) return null; + entry.IsPacked = (flags & 0x14) != 0; entry.Flags = flags; entry.Key = key; + has_packed = has_packed || entry.IsPacked; dir.Add (entry); } + while (index_pos < dir[0].Offset); if (0 == dir.Count) return null; + if (has_packed) // set proper sizes + { + long last_offset = file.MaxOffset; + for (int i = dir.Count - 1; i >= 0; --i) + { + dir[i].Size = (uint)(last_offset - dir[i].Offset); + last_offset = dir[i].Offset; + } + } return new ArcFile (file, this, dir); } } diff --git a/Legacy/Legacy.csproj b/Legacy/Legacy.csproj index 86c05aad..ed3f5dc5 100644 --- a/Legacy/Legacy.csproj +++ b/Legacy/Legacy.csproj @@ -90,8 +90,11 @@ + + + @@ -125,6 +128,8 @@ + + @@ -132,6 +137,7 @@ + @@ -145,8 +151,10 @@ + + diff --git a/Legacy/Logg/ArcARF.cs b/Legacy/Logg/ArcARF.cs new file mode 100644 index 00000000..446ab626 --- /dev/null +++ b/Legacy/Logg/ArcARF.cs @@ -0,0 +1,141 @@ +//! \file ArcARF.cs +//! \date 2023 Sep 03 +//! \brief Logg 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.Utility; +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.IO; + +// [980417][Logg] Tenshi Kourin +// [980828][Logg] Kazeiro no Romance + +namespace GameRes.Formats.Logg +{ + [Export(typeof(ArchiveFormat))] + public class ArfOpener : ArchiveFormat + { + public override string Tag => "ARF"; + public override string Description => "Logg archive file"; + public override uint Signature => 0; + public override bool IsHierarchic => true; + public override bool CanWrite => false; + + public override ArcFile TryOpen (ArcView file) + { + int count = file.View.ReadInt32 (0); + if (!IsSaneCount (count)) + return null; + + uint index = 4; + var dir = new List (count); + for (int i = 0; i < count; ++i) + { + uint offset = file.View.ReadUInt32 (index); + if (offset <= index || offset > file.MaxOffset) + return null; + uint size = file.View.ReadUInt32 (index+4); + byte name_len = file.View.ReadByte (index+8); + var name = file.View.ReadString (index+9, name_len); + var entry = Create (name); + entry.Offset = offset; + entry.UnpackedSize = size; + dir.Add (entry); + index += name_len + 9u; + if (index > dir[0].Offset) + return null; + } + long last_offset = file.MaxOffset; + for (int i = count-1; i >= 0; --i) + { + var entry = dir[i] as PackedEntry; + entry.Size = (uint)(last_offset - entry.Offset); + last_offset = entry.Offset; + if (string.IsNullOrEmpty (entry.Name)) + dir.RemoveAt (i); + else + entry.IsPacked = entry.Size != entry.UnpackedSize; + } + return new ArcFile (file, this, dir); + } + + public override Stream OpenEntry (ArcFile arc, Entry entry) + { + var pent = (PackedEntry)entry; + if (!pent.IsPacked) + return base.OpenEntry (arc, entry); + var input = arc.File.CreateStream (pent.Offset, pent.Size); + var output = new byte[pent.UnpackedSize]; + Decompress (input, output); + return new BinMemoryStream (output, pent.Name); + } + + void Decompress (IBinaryStream input, byte[] output) + { + using (var bits = new LsbBitStream (input.AsStream, true)) + { + int dst = 0; + while (dst < output.Length) + { + if (bits.GetNextBit() == 0) + { + output[dst++] = (byte)bits.GetBits (8); + } + else + { + int count; + if (bits.GetNextBit() == 0) + count = 2; + else if (bits.GetNextBit() == 0) + count = 3; + else if (bits.GetNextBit() == 0) + count = 4; + else if (bits.GetNextBit() == 0) + count = 5; + else + { + switch (bits.GetBits (2)) + { + case 0: count = 6; break; + case 1: count = bits.GetBits (2) + 7; break; + case 2: count = bits.GetBits (4) + 11; break; + case 3: count = bits.GetBits (10) + 26; break; + default: throw new EndOfStreamException(); + } + } + int offset; + if (bits.GetNextBit() == 0) + offset = bits.GetBits (8); + else if (bits.GetNextBit() == 0) + offset = bits.GetBits (10) + 0x100; + else + offset = bits.GetBits (12) + 0x500; + Binary.CopyOverlapped (output, dst - offset - 1, dst, count); + dst += count; + } + } + } + } + } +} diff --git a/Legacy/Logg/ArcMBM.cs b/Legacy/Logg/ArcMBM.cs index dbb057e6..7d5870bf 100644 --- a/Legacy/Logg/ArcMBM.cs +++ b/Legacy/Logg/ArcMBM.cs @@ -36,11 +36,11 @@ namespace GameRes.Formats.Logg [Export(typeof(ArchiveFormat))] public class MbmOpener : ArchiveFormat { - public override string Tag { get { return "MBM"; } } - public override string Description { get { return "Logg Adv engine resource archive"; } } - public override uint Signature { get { return 0; } } - public override bool IsHierarchic { get { return false; } } - public override bool CanWrite { get { return false; } } + public override string Tag => "MBM"; + public override string Description => "Logg Adv engine resource archive"; + public override uint Signature => 0; + public override bool IsHierarchic => false; + public override bool CanWrite => false; public override ArcFile TryOpen (ArcView file) { @@ -63,19 +63,27 @@ namespace GameRes.Formats.Logg IDictionary GetArchiveIndex (ArcView file) { - uint last_offset = FileListMap.Value.Keys.Last(); + string list_name; + if (!ArcSizeToFileListMap.TryGetValue ((uint)file.MaxOffset, out list_name)) + return null; + var file_map = ReadFileList (list_name); + uint last_offset = file_map.Keys.Last(); if (last_offset != file.MaxOffset) return null; - return FileListMap.Value; + return file_map; } - Lazy> FileListMap = new Lazy> (ReadFileList); + static readonly Dictionary ArcSizeToFileListMap = new Dictionary { + { 0x0AB0F5F4, "logg_pl.lst" }, + { 0x0BFFD3DA, "logg_ak.lst" }, + { 0x09809196, "logg_th.lst" }, + }; - static IDictionary ReadFileList () + static IDictionary ReadFileList (string list_name) { var file_map = new SortedDictionary(); var comma = new char[] {','}; - FormatCatalog.Instance.ReadFileList ("logg_pl.lst", line => { + FormatCatalog.Instance.ReadFileList (list_name, line => { var parts = line.Split (comma, 2); uint offset = uint.Parse (parts[0], NumberStyles.HexNumber); if (2 == parts.Length) diff --git a/Legacy/Logg/ImageFRM.cs b/Legacy/Logg/ImageFRM.cs new file mode 100644 index 00000000..145cedfc --- /dev/null +++ b/Legacy/Logg/ImageFRM.cs @@ -0,0 +1,63 @@ +//! \file ImageFRM.cs +//! \date 2023 Sep 03 +//! \brief Logg image file. +// +// 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 System.ComponentModel.Composition; +using System.IO; +using System.Windows.Media; + +namespace GameRes.Formats.Logg +{ + [Export(typeof(ImageFormat))] + public class FrmFormat : ImageFormat + { + public override string Tag { get => "FRM"; } + public override string Description { get => "Logg image format"; } + public override uint Signature { get => 0x4D5246; } // 'FRM' + + public override ImageMetaData ReadMetaData (IBinaryStream file) + { + var header = file.ReadHeader (0x10); + return new ImageMetaData { + Width = header.ToUInt32 (4), + Height = header.ToUInt32 (8), + BPP = 8, + }; + } + + public override ImageData Read (IBinaryStream file, ImageMetaData info) + { + file.Position = 0x0C; + int stride = file.ReadInt32(); + var palette = ReadPalette (file.AsStream); + var pixels = file.ReadBytes (stride * info.iHeight); + return ImageData.Create (info, PixelFormats.Indexed8, palette, pixels, stride); + } + + public override void Write (Stream file, ImageData image) + { + throw new System.NotImplementedException ("FrmFormat.Write not implemented"); + } + } +} diff --git a/Legacy/Omi/ArcDAT.cs b/Legacy/Omi/ArcDAT.cs new file mode 100644 index 00000000..c2f5a7fb --- /dev/null +++ b/Legacy/Omi/ArcDAT.cs @@ -0,0 +1,213 @@ +//! \file ArcDAT.cs +//! \date 2023 Sep 04 +//! \brief OMI Script Engine 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.Utility; +using System; +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.IO; +using System.Text; + +namespace GameRes.Formats.Omi +{ + [Export(typeof(ArchiveFormat))] + public class DatOpener : ArchiveFormat + { + public override string Tag => "DAT/OMI"; + public override string Description => "OMI Script Engine resource archive"; + public override uint Signature => 0; + public override bool IsHierarchic => false; + public override bool CanWrite => false; + + internal const uint DefaultKey = 7654321u; + + public DatOpener () + { + ContainedFormats = new[] { "BMP", "TGA", "WAV", "TXT" }; + } + + public override ArcFile TryOpen (ArcView file) + { + if (!VFS.IsPathEqualsToFileName (file.Name, "scrdat")) + return null; + using (var input = file.CreateStream()) + using (var index = new DecryptedStream (input, DefaultKey, 0)) + { + var line = index.ReadLine(); + int count = int.Parse (line); + if (!IsSaneCount (count)) + return null; + var dir = new List (count); + for (int i = 0; i < count; ++i) + { + var name = index.ReadLine(); + line = index.ReadLine(); + uint size = uint.Parse (line); + var entry = Create (name); + entry.Size = size; + entry.IsPacked = entry.Type == "image"; + dir.Add (entry); + } + long data_pos = index.Position; + for (int i = 0; i < count; ++i) + { + dir[i].Offset = data_pos; + if (!dir[i].CheckPlacement (file.MaxOffset)) + return null; + data_pos += dir[i].Size; + } + return new ArcFile (file, this, dir); + } + } + + public override Stream OpenEntry (ArcFile arc, Entry entry) + { + var pent = (PackedEntry)entry; + Stream input = arc.File.CreateStream (entry.Offset, entry.Size); + input = new DecryptedStream (input, DefaultKey, (uint)entry.Offset); + if (!pent.IsPacked) + return input; + using (var packed = new BinaryStream (input, pent.Name)) + { + var unpacked = DecompressRle (packed); + if (pent.UnpackedSize == 0) + pent.UnpackedSize = (uint)unpacked.Length; + return new BinMemoryStream (unpacked, pent.Name); + } + } + + internal static byte[] DecompressRle (IBinaryStream input) + { + int size = input.ReadInt32(); + var output = new byte[size * 2]; + ushort rle_marker = input.ReadUInt16(); + int dst = 0; + while (dst < output.Length) + { + input.Read (output, dst, 2); + if (output.ToUInt16 (dst) == rle_marker) + { + input.Read (output, dst, 2); + dst += 2; + int count = input.ReadUInt16() - 1; + if (count > 0) + { + count *= 2; + Binary.CopyOverlapped (output, dst-2, dst, count); + dst += count; + } + } + else + { + dst += 2; + } + } + return output; + } + } + + internal class DecryptedStream : InputProxyStream + { + private uint m_key; + + static readonly Encoding Encoding = Encodings.cp932; + + public override bool CanSeek { get => false; } + public override long Position + { + get => BaseStream.Position; + set => throw new NotSupportedException ("Stream.Position property is not supported"); + } + + public DecryptedStream (Stream stream, uint key, uint start_offset) : base (stream) + { + if (start_offset > 0) + { + do + { + key = 5 * key - 3; + } + while (--start_offset > 0); + } + m_key = key; + } + + public override int Read (byte[] buffer, int offset, int count) + { + int read = BaseStream.Read (buffer, offset, count); + Decrypt (buffer, offset, read); + return read; + } + + byte[] m_byte_buffer = new byte[1]; + + public override int ReadByte () + { + int b = BaseStream.ReadByte(); + if (-1 != b) + { + m_byte_buffer[0] = (byte)b; + Decrypt (m_byte_buffer, 0, 1); + b = m_byte_buffer[0]; + } + return b; + } + + internal void Decrypt (byte[] data, int offset, int count) + { + for (int i = 0; i < count; ++i) + { + data[offset+i] = (byte)(Binary.RotByteR (data[offset+i], 1) - m_key); + m_key = 5 * m_key - 3; + } + } + + byte[] m_buffer; + + public string ReadLine () + { + if (null == m_buffer) + m_buffer = new byte[32]; + int size = 0; + for (;;) + { + int b = ReadByte(); + if (-1 == b || '\n' == b) + break; + if (m_buffer.Length == size) + { + Array.Resize (ref m_buffer, checked(size/2*3)); + } + m_buffer[size++] = (byte)b; + } + return Encoding.GetString (m_buffer, 0, size); + } + + public override long Seek (long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } + } +} diff --git a/Legacy/Rare/ArcX.cs b/Legacy/Rare/ArcX.cs new file mode 100644 index 00000000..4cca0ad0 --- /dev/null +++ b/Legacy/Rare/ArcX.cs @@ -0,0 +1,132 @@ +//! \file ArcX.cs +//! \date 2023 Sep 02 +//! \brief Rare archive format. +// +// 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 System; +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.IO; + +// [990528][Rare] Seisen Ren'ai Yuugi + +namespace GameRes.Formats.Rare +{ + [Export(typeof(ArchiveFormat))] + public class XOpener : ArchiveFormat + { + public override string Tag => "X/RARE"; + public override string Description => "Rare resource archive"; + public override uint Signature => 0; + public override bool IsHierarchic => false; + public override bool CanWrite => false; + + static readonly Dictionary> KnownExeMap = new Dictionary> { + { "seisen.exe", Tuple.Create (0x3A9A0u, 715) }, + }; + + public override ArcFile TryOpen (ArcView file) + { + if (!VFS.IsPathEqualsToFileName (file.Name, "PP.X")) + return null; + string full_exe_name = null; + Tuple index_pos = null; + foreach (var exe_name in KnownExeMap.Keys) + { + full_exe_name = VFS.ChangeFileName (file.Name, exe_name); + if (VFS.FileExists (full_exe_name)) + { + index_pos = KnownExeMap[exe_name]; + break; + } + } + if (null == index_pos) + return null; + uint index_offset = index_pos.Item1; + int count = index_pos.Item2; + using (var index = VFS.OpenView (full_exe_name)) + { + index.View.Reserve (index_offset, (uint)count * 12u); + var dir = new List (count); + for (int i = 0; i < count; ++i) + { + var entry = new PackedEntry { + Name = string.Format ("PP#{0:D5}.BMP", i), + Type = "image", + Offset = index.View.ReadUInt32 (index_offset), + Size = index.View.ReadUInt32 (index_offset+4), + UnpackedSize = index.View.ReadUInt32 (index_offset+8), + IsPacked = true, + }; + if (!entry.CheckPlacement (file.MaxOffset)) + return null; + dir.Add (entry); + index_offset += 12; + } + return new ArcFile (file, this, dir); + } + } + + public override Stream OpenEntry (ArcFile arc, Entry entry) + { + var pent = (PackedEntry)entry; + using (var input = arc.File.CreateStream (entry.Offset, entry.Size)) + { + var output = new byte[pent.UnpackedSize]; + Decompress (input, output); + return new BinMemoryStream (output, entry.Name); + } + } + + internal static void Decompress (IBinaryStream input, byte[] output) + { + var frame = new byte[0x400]; + int frame_pos = 1; + int dst = 0; + using (var bits = new MsbBitStream (input.AsStream, true)) + { + while (dst < output.Length) + { + int ctl = bits.GetNextBit(); + if (-1 == ctl) + break; + if (ctl != 0) + { + int v = bits.GetBits (8); + output[dst++] = frame[frame_pos++ & 0x3FF] = (byte)v; + } + else + { + int offset = bits.GetBits (10); + int count = bits.GetBits (5) + 2; + while (count --> 0) + { + byte v = frame[offset++ & 0x3FF]; + output[dst++] = frame[frame_pos++ & 0x3FF] = v; + } + } + } + } + } + } +} diff --git a/Legacy/Rhss/ArcCRG.cs b/Legacy/Rhss/ArcCRG.cs new file mode 100644 index 00000000..8a04632a --- /dev/null +++ b/Legacy/Rhss/ArcCRG.cs @@ -0,0 +1,83 @@ +//! \file ArcCRG.cs +//! \date 2023 Aug 28 +//! \brief Raishū Hyōjun Script System 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 System.Collections.Generic; +using System.ComponentModel.Composition; +using System.IO; + +namespace GameRes.Formats.Rhss +{ + [Export(typeof(ArchiveFormat))] + public class PakOpener : ArchiveFormat + { + public override string Tag => "DAT/CRG"; + public override string Description => "RHSS engine resource archive"; + public override uint Signature => 0x00475243; // 'CRG' + public override bool IsHierarchic => false; + public override bool CanWrite => false; + + public override ArcFile TryOpen (ArcView file) + { + int count = file.View.ReadInt32 (4); + if (!IsSaneCount (count)) + return null; + uint index_pos = 8; + var dir = new List (count); + for (int i = 0; i < count; ++i) + { + var name = file.View.ReadString (index_pos+8, 0x30); + var entry = Create (name); + entry.Offset = file.View.ReadUInt32 (index_pos); + entry.Size = file.View.ReadUInt32 (index_pos+4); + if (!entry.CheckPlacement (file.MaxOffset)) + return null; + dir.Add (entry); + index_pos += 60; + } + return new ArcFile (file, this, dir); + } + + public override Stream OpenEntry (ArcFile arc, Entry entry) + { + var pent = entry as PackedEntry; + if (null != pent) + { + if (!pent.IsPacked && arc.File.View.AsciiEqual (entry.Offset, "CMP\0")) + { + pent.IsPacked = true; + pent.UnpackedSize = arc.File.View.ReadUInt32 (entry.Offset + 0x4C); + } + if (pent.IsPacked) + { + Stream input = arc.File.CreateStream (entry.Offset+0x50, entry.Size-0x50); + input = new ZLibStream (input, CompressionMode.Decompress); + return new XoredStream (input, 0xFF); + } + } + return base.OpenEntry (arc, entry); + } + } +} diff --git a/Legacy/SplushWave/ArcDAT.cs b/Legacy/SplushWave/ArcDAT.cs index 05b39396..dfd90fe9 100644 --- a/Legacy/SplushWave/ArcDAT.cs +++ b/Legacy/SplushWave/ArcDAT.cs @@ -27,6 +27,9 @@ using System; using System.Collections.Generic; using System.ComponentModel.Composition; using System.IO; +using System.Windows.Media; + +// [030817][Splush Wave] Knock Out -Taisengata Datsui Mahjong- namespace GameRes.Formats.SplushWave { @@ -129,5 +132,92 @@ namespace GameRes.Formats.SplushWave } return new BinMemoryStream (output, 0, dst); } + + public override IImageDecoder OpenImage (ArcFile arc, Entry entry) + { + var fent = (FlkEntry)entry; + var input = BinaryStream.FromStream (OpenEntry (arc, fent), fent.Name); + if ((fent.Flags & 0x10) == 0) + return ImageFormatDecoder.Create (input); + try + { + var info = Swg.ReadMetaData (input) as SwgMetaData; + if (null == info) + { + input.Position = 0; + return new ImageFormatDecoder(input); + } + return new Swg1ImageDecoder (input, info); + } + catch + { + input.Dispose(); + throw; + } + } + + static readonly ResourceInstance s_swg = new ResourceInstance ("SWG"); + + internal static SwgFormat Swg { get => s_swg.Value; } + } + + internal sealed class Swg1ImageDecoder : BinaryImageDecoder + { + SwgMetaData m_info; + + public Swg1ImageDecoder (IBinaryStream input, SwgMetaData info) : base (input, info) + { + SourceFormat = DatOpener.Swg; + m_info = info; + } + + static readonly byte[] PlaneMap = { 3, 2, 1, 0 }; + + protected override ImageData GetImageData () + { + m_input.Position = m_info.DataOffset; + int stride = 4 * m_info.iWidth; + int plane_size = m_info.iWidth * m_info.iHeight; + var output = new byte[stride * m_info.iHeight]; + ushort[] ctl_buf = new ushort[m_info.iHeight]; + for (int c = 0; c < 4; ++c) + { + int compress_method = ReadU16BE(); + if (0 == compress_method) + { + int dst = PlaneMap[c] + stride * (m_info.iHeight - 1); + for (int y = 0; y < m_info.iHeight; ++y) + { + for (int x = 0; x < stride; x += 4) + { + output[dst+x] = m_input.ReadUInt8(); + } + dst -= stride; + } + continue; + } + if (compress_method != 1) + throw new InvalidFormatException(); + for (int y = 0; y < m_info.iHeight; ++y) + { + ctl_buf[y] = ReadU16BE(); + } + int row = PlaneMap[c]; + for (int y = 0; y < m_info.iHeight; ++y) + { + int dst = row; + int row_size = ctl_buf[y]; + SwgFormat.DecompressRow (m_input, row_size, output, dst, 4); + row += stride; + } + } + return ImageData.Create (m_info, PixelFormats.Bgra32, null, output, stride); + } + + ushort ReadU16BE () + { + ushort le = m_input.ReadUInt16(); + return (ushort)(le >> 8 | le << 8); + } } } diff --git a/Legacy/SplushWave/ImageSWG.cs b/Legacy/SplushWave/ImageSWG.cs index 41274bb9..490fe560 100644 --- a/Legacy/SplushWave/ImageSWG.cs +++ b/Legacy/SplushWave/ImageSWG.cs @@ -68,7 +68,7 @@ namespace GameRes.Formats.SplushWave { var meta = (SwgMetaData)info; PixelFormat format = meta.BPP == 8 ? PixelFormats.Indexed8 - : meta.BPP == 32 ? PixelFormats.Bgr32 : PixelFormats.Bgr24; + : meta.BPP == 32 ? PixelFormats.Bgra32 : PixelFormats.Bgr24; BitmapPalette palette = null; if (meta.BPP == 8) { @@ -77,36 +77,40 @@ namespace GameRes.Formats.SplushWave } int stride = meta.iWidth * meta.BPP / 8; file.Position = meta.DataOffset; -// var pixels = new byte[stride * meta.iHeight]; - var pixels = new byte[4 * meta.iWidth * meta.iHeight]; + var pixels = new byte[stride * meta.iHeight]; if (!meta.IsCompressed) { file.Read (pixels, 0, pixels.Length); return ImageData.CreateFlipped (meta, format, palette, pixels, stride); } - var input = file.ReadBytes ((int)(file.Length - file.Position)); - if (!Decompress (input, pixels, meta.Depth + 2, meta.iWidth, meta.iHeight)) + if (!Decompress (file, pixels, meta.Depth + 2, meta.iWidth, meta.iHeight)) throw new InvalidFormatException ("Invalid SWG file."); return ImageData.CreateFlipped (meta, format, palette, pixels, stride); } - bool Decompress (byte[] input, byte[] output, int channels, int width, int height) + static readonly byte[] PlaneMap = { 2, 1, 0, 3 }; + + bool Decompress (IBinaryStream input, byte[] output, int channels, int width, int height) { - int src = 0; - if (input[0] != 0 || input[1] != 1) + long start_pos = input.Position; + byte hi = input.ReadUInt8(); + byte lo = input.ReadUInt8(); + if (hi != 0 || lo != 1) { + input.Position = start_pos; int n = 0; for (int i = 0; i < channels; ++i) { - if (0 == input[i]) + if (0 == input.ReadByte()) ++n; } if (n != channels) return false; - src = 4; + input.Position = start_pos + 4; + hi = input.ReadUInt8(); + lo = input.ReadUInt8(); } - int compress_method = input[src+1] + (input[src] << 8); - src += 2; + int compress_method = lo | hi << 8; if (0 == compress_method) { for (int i = 0; i < channels; ++i) @@ -115,7 +119,7 @@ namespace GameRes.Formats.SplushWave int count = height * width; while (count --> 0) { - output[pos] = input[src++]; + output[pos] = input.ReadUInt8(); pos += channels; } } @@ -123,63 +127,57 @@ namespace GameRes.Formats.SplushWave } if (compress_method != 1) return false; - int dst = 0; - int v33 = src; - int v37 = height * channels; - src += 2 * v37; - for (int row = 0; row < v37; ++row) + int stride = width * channels; + var row_sizes = input.ReadBytes (2 * height * channels); + int ctl_pos = 0; + for (int c = 0; c < channels; ++c) + for (int y = height - 1; y >= 0; --y) { - int y = row % height; - dst = channels * (width * (height - y - 1) + 1) - row / height - 1; - if (dst > output.Length) - return true; - int v24 = 0; - int v36 = input[v33+1] + (input[v33] << 8); - v33 += 2; - do - { - byte lo = input[src]; - byte hi = input[src+1]; - if (lo != 0) - { - if (lo < 0x81) - { - ++src; - int count = lo + 1; - v24 += count + 1; - while (count --> 0) - { - output[dst] = input[src++]; - dst += channels; - } - } - else - { - src += 2; - v24 += 2; - int count = Math.Min (0x101 - lo, output.Length - dst); - while (count --> 0) - { - output[dst] = hi; - dst += channels; - } - } - } - else - { - src += 2; - v24 += 2; - output[dst] = hi; - dst += channels; - } - if (dst >= output.Length) - return true; - } - while (v24 < v36); + int dst = stride * y + PlaneMap[c]; + int row_size = row_sizes[ctl_pos+1] | row_sizes[ctl_pos] << 8; + ctl_pos += 2; + DecompressRow (input, row_size, output, dst, channels); } return true; } + internal static void DecompressRow (IBinaryStream input, int row_size, byte[] output, int dst, int step) + { + int x = 0; + while (x < row_size) + { + byte ctl = input.ReadUInt8(); + if (ctl == 0) + { + byte v = input.ReadUInt8(); + x += 2; + output[dst] = v; + dst += step; + } + else if (ctl < 0x81u) + { + int count = ctl + 1; + x += count + 1; + while (count --> 0) + { + output[dst] = input.ReadUInt8(); + dst += step; + } + } + else + { + byte v = input.ReadUInt8(); + x += 2; + int count = 0x101 - ctl; + while (count --> 0) + { + output[dst] = v; + dst += step; + } + } + } + } + public override void Write (Stream file, ImageData image) { throw new System.NotImplementedException ("SwgFormat.Write not implemented");