From ecdb6d147fc3964f3c10211efb65f11b65e07e78 Mon Sep 17 00:00:00 2001 From: morkt Date: Mon, 3 Oct 2016 23:22:37 +0400 Subject: [PATCH] (GrpOpener): implemented TPW compression. --- ArcFormats/Ankh/ArcGRP.cs | 174 ++++++++++++++++++++++++++++++------ ArcFormats/Ankh/AudioPCM.cs | 74 +++++++++++++++ supported.html | 8 +- 3 files changed, 230 insertions(+), 26 deletions(-) create mode 100644 ArcFormats/Ankh/AudioPCM.cs diff --git a/ArcFormats/Ankh/ArcGRP.cs b/ArcFormats/Ankh/ArcGRP.cs index e1dcf007..e0bed9a7 100644 --- a/ArcFormats/Ankh/ArcGRP.cs +++ b/ArcFormats/Ankh/ArcGRP.cs @@ -34,23 +34,23 @@ namespace GameRes.Formats.Ankh [Export(typeof(ArchiveFormat))] public class GrpOpener : ArchiveFormat { - public override string Tag { get { return "GRP/ANKH"; } } - public override string Description { get { return "Ankh resource archive"; } } + public override string Tag { get { return "GRP/ICE"; } } + public override string Description { get { return "Ice Soft resource archive"; } } public override uint Signature { get { return 0; } } public override bool IsHierarchic { get { return false; } } public override bool CanCreate { get { return false; } } public GrpOpener () { - Extensions = new string[] { "grp" }; + Extensions = new string[] { "grp", "bin" }; } public override ArcFile TryOpen (ArcView file) { uint first_offset = file.View.ReadUInt32 (0); - if (first_offset < 8 || first_offset >= file.MaxOffset) + if (first_offset < 8 || first_offset >= file.MaxOffset || 0 != (first_offset & 3)) return null; - int count = (int)(first_offset - 8) / 4; + int count = (int)(first_offset - 4) / 4; if (!IsSaneCount (count)) return null; @@ -58,7 +58,7 @@ namespace GameRes.Formats.Ankh uint index_offset = 0; uint next_offset = first_offset; var dir = new List (count); - for (int i = 0; i < count; ++i) + for (int i = 0; i < count && next_offset < file.MaxOffset; ++i) { var entry = new PackedEntry { Offset = next_offset }; index_offset += 4; @@ -77,57 +77,106 @@ namespace GameRes.Formats.Ankh } if (0 == dir.Count) return null; + DetectFileTypes (file, dir); + return new ArcFile (file, this, dir); + } + + void DetectFileTypes (ArcView file, List dir) + { foreach (PackedEntry entry in dir) { - if (entry.Size < 4) + if (entry.Size <= 8) continue; - uint unpacked_size = file.View.ReadUInt32 (entry.Offset); - if (entry.Size > 8 && file.View.AsciiEqual (entry.Offset+4, "HDJ\0")) + if (file.View.AsciiEqual (entry.Offset, "TPW")) + { + entry.IsPacked = file.View.ReadByte (entry.Offset+3) != 0; + long start_offset = entry.Offset+4; + if (entry.IsPacked) + { + entry.UnpackedSize = file.View.ReadUInt32 (start_offset); + start_offset = entry.Offset+11; + } + else + { + entry.Offset = start_offset; + entry.Size -= 4; + } + if (file.View.AsciiEqual (start_offset, "BM")) + { + entry.Name = Path.ChangeExtension (entry.Name, "bmp"); + entry.Type = "image"; + } + } + else if (file.View.AsciiEqual (entry.Offset+4, "HDJ\0")) { if (file.View.AsciiEqual (entry.Offset+12, "BM")) { entry.Name = Path.ChangeExtension (entry.Name, "bmp"); entry.Type = "image"; } - entry.UnpackedSize = unpacked_size; + entry.UnpackedSize = file.View.ReadUInt32 (entry.Offset); entry.IsPacked = true; } + else if (file.View.AsciiEqual (entry.Offset+4, "OggS")) + { + entry.Name = Path.ChangeExtension (entry.Name, "ogg"); + entry.Type = "audio"; + entry.Offset += 4; + entry.Size -= 4; + } else if (entry.Size > 12 && file.View.AsciiEqual (entry.Offset+8, "RIFF")) { entry.Name = Path.ChangeExtension (entry.Name, "wav"); entry.Type = "audio"; - entry.UnpackedSize = unpacked_size; + entry.UnpackedSize = file.View.ReadUInt32 (entry.Offset); entry.IsPacked = true; } - else if (0x4D42 == (unpacked_size & 0xFFFF)) + else if (file.View.AsciiEqual (entry.Offset, "BM")) { entry.Name = Path.ChangeExtension (entry.Name, "bmp"); entry.Type = "image"; } + else if (entry.Size > 0x16 && IsAudioEntry (file, entry)) + { + entry.Type = "audio"; + } } - return new ArcFile (file, this, dir); + } + + bool IsAudioEntry (ArcView file, Entry entry) + { + uint signature = file.View.ReadUInt32 (entry.Offset); + if (signature != 0x010001 && signature != 0x020001) + return false; + int extra = file.View.ReadUInt16 (entry.Offset+0x10); + if (extra != 0) + return false; + uint size = file.View.ReadUInt32 (entry.Offset+0x12); + return 0x16 + size == entry.Size; } public override Stream OpenEntry (ArcFile arc, Entry entry) { - if (entry.Size > 8 && arc.File.View.AsciiEqual (entry.Offset+4, "HDJ\0")) - return OpenImage (arc, entry); - else if (entry.Size > 12 && 'W' == arc.File.View.ReadByte (entry.Offset+4) - && arc.File.View.AsciiEqual (entry.Offset+8, "RIFF")) - return OpenAudio (arc, entry); - else - return base.OpenEntry (arc, entry); + var pent = entry as PackedEntry; + if (pent != null && pent.IsPacked && pent.Size > 8) + { + if (arc.File.View.AsciiEqual (entry.Offset, "TPW")) + return OpenTpw (arc, pent); + if (arc.File.View.AsciiEqual (entry.Offset+4, "HDJ\0")) + return OpenImage (arc, pent); + if (entry.Size > 12 && 'W' == arc.File.View.ReadByte (entry.Offset+4) + && arc.File.View.AsciiEqual (entry.Offset+8, "RIFF")) + return OpenAudio (arc, entry); + } + return base.OpenEntry (arc, entry); } - Stream OpenImage (ArcFile arc, Entry entry) + Stream OpenImage (ArcFile arc, PackedEntry entry) { - int unpacked_size = arc.File.View.ReadInt32 (entry.Offset); - if (unpacked_size <= 0) - return base.OpenEntry (arc, entry); using (var packed = arc.File.CreateStream (entry.Offset+8, entry.Size-8)) using (var reader = new GrpUnpacker (packed)) { - var unpacked = new byte[unpacked_size]; + var unpacked = new byte[entry.UnpackedSize]; reader.UnpackHDJ (unpacked, 0); return new MemoryStream (unpacked); } @@ -155,6 +204,81 @@ namespace GameRes.Formats.Ankh return new MemoryStream (unpacked); } } + + Stream OpenTpw (ArcFile arc, PackedEntry entry) + { + var output = new byte[entry.UnpackedSize]; + using (var file = arc.File.CreateStream (entry.Offset, entry.Size)) + using (var input = new BinaryReader (file)) + { + file.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.ReadByte(); + if (0 == ctl) + break; + int count; + if (ctl < 0x40) + { + input.Read (output, dst, ctl); + dst += ctl; + } + else if (ctl <= 0x6F) + { + if (0x6F == ctl) + count = input.ReadUInt16(); + else + count = (ctl + 0xC3) & 0xFF; + byte v = input.ReadByte(); + while (count --> 0) + output[dst++] = v; + } + else if (ctl <= 0x9F) + { + if (ctl == 0x9F) + count = input.ReadUInt16(); + else + count = (ctl + 0x92) & 0xFF; + byte v1 = input.ReadByte(); + byte v2 = input.ReadByte(); + while (count --> 0) + { + output[dst++] = v1; + output[dst++] = v2; + } + } + else if (ctl <= 0xBF) + { + if (ctl == 0xBF) + count = input.ReadUInt16(); + else + count = ((ctl + 0x62) & 0xFF); + input.Read (output, dst, 3); + if (count > 0) + { + count *= 3; + Binary.CopyOverlapped (output, dst, dst+3, count-3); + dst += count; + } + } + else + { + count = (ctl & 0x3F) + 3; + int offset = input.ReadByte(); + offset = (offset & 0x3F) - offsets[offset >> 6]; + Binary.CopyOverlapped (output, dst+offset, dst, count); + dst += count; + } + } + return new MemoryStream (output); + } + } } internal sealed class GrpUnpacker : IDisposable diff --git a/ArcFormats/Ankh/AudioPCM.cs b/ArcFormats/Ankh/AudioPCM.cs new file mode 100644 index 00000000..f839b714 --- /dev/null +++ b/ArcFormats/Ankh/AudioPCM.cs @@ -0,0 +1,74 @@ +//! \file AudioPCM.cs +//! \date Mon Oct 03 22:21:15 2016 +//! \brief Ice Soft PCM audio file. +// +// Copyright (C) 2016 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 GameRes.Utility; + +namespace GameRes.Formats.Ice +{ + [Export(typeof(AudioFormat))] + public class IceAudio : AudioFormat + { + public override string Tag { get { return "PCM/ICE"; } } + public override string Description { get { return "Ice Soft PCM audio"; } } + public override uint Signature { get { return 0; } } + + public IceAudio () + { + Extensions = new string[] { "" }; + Signatures = new uint[] { 0x010001, 0x020001 }; + } + + public override SoundInput TryOpen (Stream file) + { + var header = new byte[0x16]; + if (header.Length != file.Read (header, 0, header.Length)) + return null; + ushort tag = LittleEndian.ToUInt16 (header, 0); + if (tag != 1) + return null; + int extra_size = LittleEndian.ToUInt16 (header, 0x10); + if (0 != extra_size) + return null; + uint pcm_size = LittleEndian.ToUInt32 (header, 0x12); + if (pcm_size + 0x16 != file.Length) + return null; + var format = new WaveFormat { FormatTag = tag }; + format.Channels = LittleEndian.ToUInt16 (header, 2); + if (format.Channels != 1 && format.Channels != 2) + return null; + format.SamplesPerSecond = LittleEndian.ToUInt32 (header, 4); + format.AverageBytesPerSecond = LittleEndian.ToUInt32 (header, 8); + format.BlockAlign = LittleEndian.ToUInt16 (header, 0xC); + format.BitsPerSample = LittleEndian.ToUInt16 (header, 0xE); + if (0 == format.AverageBytesPerSecond + || format.SamplesPerSecond * format.BlockAlign != format.AverageBytesPerSecond) + return null; + var pcm = new StreamRegion (file, 0x16, pcm_size); + return new RawPcmInput (pcm, format); + } + } +} diff --git a/supported.html b/supported.html index bafd7429..ae10a1b1 100644 --- a/supported.html +++ b/supported.html @@ -179,6 +179,7 @@ Binary Pot
Chou Gedou Yuusha
Shinshoku ~Inma no Ikenie~
Shinshoku 2 ~Inma no Ikenie~
+Shokusou Tenshi Serika
Tsukihime
Umineko
@@ -265,6 +266,7 @@ Kurenai no Tsuki
LOVELY x CATION
Mayoeru Futari to Sekai no Subete
Mahoutsukai no Yoru
+Mizukoi
Nakadashi Hara Maid series
Natsupochi
Natsuzora Kanata
@@ -411,6 +413,7 @@ Chikan Circle 2 ShiinaRio v2.47
Chuuchuu Nurse ShiinaRio v2.45
Classmate no Okaa-san ShiinaRio v2.37
Cleavage ShiinaRio v2.33
+Danchi Wife ~Hitozuma Nude Model no Kenshin~ ShiinaRio v2.47
Doushite Daite Kurenai no!?ShiinaRio v2.49
Draculius ShiinaRio v2.38
Enkaku Sousa 2.36 or 2.37
@@ -832,6 +835,7 @@ Trouble Trap Laboratory
*.ngpNGPNo *.hxpHim4
Him5
SHS6
SHS7NoSH System Natsumero
+Sumeragi Ryouko no Bitch na 1 nichi
*.det
+*.nme
+*.atm-Noμ-GameOperationSystem Ippai Shimasho
@@ -981,7 +985,8 @@ Hitomi no Rakuin ~Inbaku no Mesu Dorei~
Jokujima
Saiin Haramase Keyword
-*.grp-NoAnkh +*.grp
*.bin-
TPWNoTirol
Ankh +Aigan Shoujo
Mozu
*.gpcGpc7NoSuper NekoX @@ -1105,6 +1110,7 @@ Soushinjutsu 2
Rakuin Hime Runed Princess
Shinshoku -Zero- ~Inma no Ikenie~
Shinshoku 3 ~Inma no Ikenie~
+Shokusou Tenshi Serika 2
*.dat-NoNekoSDK Elevator Panic ~Misshitsu no Inkou~