diff --git a/ArcFormats/ArcFormats.csproj b/ArcFormats/ArcFormats.csproj index 40710671..fa2ac926 100644 --- a/ArcFormats/ArcFormats.csproj +++ b/ArcFormats/ArcFormats.csproj @@ -69,6 +69,9 @@ + + + diff --git a/ArcFormats/Propeller/ArcMGR.cs b/ArcFormats/Propeller/ArcMGR.cs new file mode 100644 index 00000000..42c011a3 --- /dev/null +++ b/ArcFormats/Propeller/ArcMGR.cs @@ -0,0 +1,136 @@ +//! \file ArcMGR.cs +//! \date Sat Nov 21 02:05:59 2015 +//! \brief Propeller multi-frame image. +// +// Copyright (C) 2015 by morkt +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +// IN THE SOFTWARE. +// + +using System; +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.IO; +using System.Linq; +using GameRes.Utility; + +namespace GameRes.Formats.Propeller +{ + [Export(typeof(ArchiveFormat))] + public class MgrOpener : ArchiveFormat + { + public override string Tag { get { return "MGR"; } } + public override string Description { get { return "Propeller multi-frame image"; } } + public override uint Signature { get { return 0; } } + public override bool IsHierarchic { get { return false; } } + public override bool CanCreate { get { return false; } } + + public override ArcFile TryOpen (ArcView file) + { + if (!file.Name.EndsWith (".mgr", StringComparison.InvariantCultureIgnoreCase)) + return null; + int count = file.View.ReadInt16 (0); + if (count <= 0 || count >= 0x100) + return null; + uint current = 2; + uint first_offset = current; + if (count > 1) + { + first_offset = file.View.ReadUInt32 (current); + if (first_offset != 2 + count * 4) + return null; + } + if (!file.View.AsciiEqual (first_offset+9, "BM")) + return null; + string base_name = Path.GetFileNameWithoutExtension (file.Name); + var dir = new List (count); + if (count > 1) + { + for (int i = 0; i < count; ++i) + { + var entry = new PackedEntry { + Name = string.Format ("{0}#{1:D4}.bmp", base_name, i), + Type = "image", + Offset = file.View.ReadUInt32 (current), + }; + if (entry.Offset < first_offset || entry.Offset >= file.MaxOffset) + return null; + dir.Add (entry); + current += 4; + } + } + else + { + dir.Add (new PackedEntry { Name = base_name+".bmp", Type = "image", Offset = current }); + } + foreach (var entry in dir.Cast()) + { + entry.UnpackedSize = file.View.ReadUInt32 (entry.Offset); + entry.Size = file.View.ReadUInt32 (entry.Offset+4); + if (entry.UnpackedSize < 0x36 || entry.Size > file.MaxOffset-entry.Offset) + return null; + entry.Offset += 8; + } + return new ArcFile (file, this, dir); + } + + public override Stream OpenEntry (ArcFile arc, Entry entry) + { + using (var input = arc.File.CreateStream (entry.Offset, entry.Size)) + { + var bmp = new byte[(entry as PackedEntry).UnpackedSize]; + Decompress (input, bmp); + return new MemoryStream (bmp); + } + } + + static public int Decompress (Stream input, byte[] output) + { + int dst = 0; + while (dst < output.Length) + { + int count = input.ReadByte(); + if (-1 == count) + break; + if (count < 0x20) + { + count = Math.Min (count+1, output.Length-dst); + int read = input.Read (output, dst, count); + dst += read; + if (read < count) + break; + } + else + { + int offset = ((count & 0x1F) << 8) + 1; + count >>= 5; + if (7 == count) + count += input.ReadByte(); + offset += input.ReadByte(); + if (offset >= dst) + throw new InvalidFormatException(); + count = Math.Min (count+2, output.Length-dst); + Binary.CopyOverlapped (output, dst - offset, dst, count); + dst += count; + } + } + return dst; + } + } +} diff --git a/ArcFormats/Propeller/ArcMPK.cs b/ArcFormats/Propeller/ArcMPK.cs new file mode 100644 index 00000000..9eae2473 --- /dev/null +++ b/ArcFormats/Propeller/ArcMPK.cs @@ -0,0 +1,87 @@ +//! \file ArcMPK.cs +//! \date Sat Nov 21 00:56:44 2015 +//! \brief Propeller resource archive. +// +// Copyright (C) 2015 by morkt +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +// IN THE SOFTWARE. +// + +using System; +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.IO; +using GameRes.Utility; + +namespace GameRes.Formats.Propeller +{ + [Export(typeof(ArchiveFormat))] + public class MpkOpener : ArchiveFormat + { + public override string Tag { get { return "MPK"; } } + public override string Description { get { return "Propeller resources archive"; } } + public override uint Signature { get { return 0; } } + public override bool IsHierarchic { get { return false; } } + public override bool CanCreate { get { return false; } } + + public override ArcFile TryOpen (ArcView file) + { + uint index_offset = file.View.ReadUInt32 (0); + int count = file.View.ReadInt32 (4); + if (index_offset >= file.MaxOffset || !IsSaneCount (count)) + return null; + uint index_size = (uint)count * 0x28u; + if (index_size > file.MaxOffset - index_offset) + return null; + var index = file.View.ReadBytes (index_offset, index_size); + // last byte of the first filename presumably is zero + byte key = index[0x1F]; + for (int i = 0; i < index.Length; ++i) + index[i] ^= key; + + int current = 0; + var dir = new List (count); + for (int i = 0; i < count; ++i) + { + int name_offset = '\\' == index[current] ? 1 : 0; + var name = Binary.GetCString (index, current+name_offset, 0x20-name_offset); + if (0 == name.Length) + return null; + var entry = FormatCatalog.Instance.Create (name); + entry.Offset = LittleEndian.ToUInt32 (index, current+0x20); + entry.Size = LittleEndian.ToUInt32 (index, current+0x24); + if (!entry.CheckPlacement (file.MaxOffset)) + return null; + dir.Add (entry); + current += 0x28; + } + return new ArcFile (file, this, dir); + } + + public override Stream OpenEntry (ArcFile arc, Entry entry) + { + if (!entry.Name.EndsWith (".msc", StringComparison.InvariantCultureIgnoreCase)) + return base.OpenEntry (arc, entry); + var data = arc.File.View.ReadBytes (entry.Offset, entry.Size); + for (int i = 0; i < data.Length; ++i) + data[i] ^= 0x88; + return new MemoryStream (data); + } + } +} diff --git a/ArcFormats/Propeller/ImageMGR.cs b/ArcFormats/Propeller/ImageMGR.cs new file mode 100644 index 00000000..d3a82dc4 --- /dev/null +++ b/ArcFormats/Propeller/ImageMGR.cs @@ -0,0 +1,121 @@ +//! \file ImageMGR.cs +//! \date Sat Nov 21 01:58:44 2015 +//! \brief Propeller compressed bitmap. +// +// Copyright (C) 2015 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; + +namespace GameRes.Formats.Propeller +{ + internal class MgrMetaData : ImageMetaData + { + public int Offset; + public int PackedSize; + public int UnpackedSize; + } + + [Export(typeof(ImageFormat))] + public class MgrFormat : ImageFormat + { + public override string Tag { get { return "MGR"; } } + public override string Description { get { return "Propeller image format"; } } + public override uint Signature { get { return 0; } } + + public override ImageMetaData ReadMetaData (Stream stream) + { + using (var reader = new ArcView.Reader (stream)) + { + int count = reader.ReadInt16(); + if (count <= 0 || count >= 0x100) + return null; + int offset; + if (count > 1) + { + offset = reader.ReadInt32(); + if (offset != 2 + count * 4) + return null; + } + else + offset = 2; + stream.Position = offset; + int unpacked_size = reader.ReadInt32(); + int packed_size = reader.ReadInt32(); + offset += 8; + if (offset + packed_size > stream.Length) + return null; + byte[] header = new byte[0x36]; + if (0x36 != MgrOpener.Decompress (stream, header) + || header[0] != 'B' || header[1] != 'M') + return null; + using (var bmp = new MemoryStream (header)) + { + var info = Bmp.ReadMetaData (bmp); + if (null == info) + return null; + return new MgrMetaData + { + Width = info.Width, + Height = info.Height, + BPP = info.BPP, + Offset = offset, + PackedSize = packed_size, + UnpackedSize = unpacked_size, + }; + } + } + } + + public override ImageData Read (Stream stream, ImageMetaData info) + { + var meta = (MgrMetaData)info; + stream.Position = meta.Offset; + var data = new byte[meta.UnpackedSize]; + if (data.Length != MgrOpener.Decompress (stream, data)) + throw new InvalidFormatException(); + if (meta.BPP != 32) + { + using (var bmp = new MemoryStream (data)) + return Bmp.Read (bmp, info); + } + // special case for 32bpp bitmaps with alpha-channel + int stride = (int)meta.Width * 4; + var pixels = new byte[stride * (int)meta.Height]; + int src = LittleEndian.ToInt32 (data, 0xA); + for (int dst = stride*((int)meta.Height-1); dst >= 0; dst -= stride) + { + Buffer.BlockCopy (data, src, pixels, dst, stride); + src += stride; + } + return ImageData.Create (info, PixelFormats.Bgra32, null, pixels); + } + + public override void Write (Stream file, ImageData image) + { + throw new System.NotImplementedException ("MgrFormat.Write not implemented"); + } + } +} diff --git a/supported.html b/supported.html index 9e0e46d8..272b5a8c 100644 --- a/supported.html +++ b/supported.html @@ -53,6 +53,7 @@ Saimin Gakuen
*.bmzZLC3Yes *.intKIFYes1CatSystem2 Grisaia no Kajitsu
+Kamikaze ☆ Explorer!
Makai Tenshi Djibril -Episode 4-
Shukufuku no Campanella
TsunaBan ♥ Love Mix
@@ -153,6 +154,7 @@ Shikkoku no Sharnoth
*.lwgLGNo *.wcgWGqYes *.xp3XP3YesKiriKiri +Aozora Gakko no Sensei-kun
Cafe Sourire
Coμ
Damegane
@@ -168,6 +170,7 @@ Nuki Doki!
Okiba ga Nai!
Ore no Saimin Fantasia
Seirei Tenshou
+Se-kirara
Sharin no Kuni, Himawari no Shoujo
Swan Song
Zecchou Spiral!!
@@ -206,12 +209,14 @@ Nikutai Ten'i
*.dat
*.arc-
M2TYPENoFFA System/G-SYS Dokusen Kango
Hensai Keikaku
+Inen no Yakata
Inran OL Sawatari Tokiko
Kunoichi Kikyou ~Gensou Kannou Emaki~
Momo x Momi
Michibikareshi Mono-tachi no Rakuen ~BEDLAM~
Onsen Kankou Yukemuri Chijou
Ryoujoku Costume Play
+Seikoujo Claudia
*.pt1-No *.wa1-No @@ -333,6 +338,7 @@ Maid no Yakata ~Zetsubou Hen~
*.pcmXPCMNo *.pakCHERRY PACK 2.0
-NoCherry Double
+Exile ~Blood Royal 2~
Kimon Youitan
Unbalance
@@ -390,6 +396,7 @@ Mamagoto
*.bin+*.pakhedNoelfAdult Video King *.hip
*.hiz
hip
hizNo *.gpk+*.gtb
*.vpk+*.vtb-NoBlack Cyc +Before Dawn Daybreak ~Shinen no Utahime~
Gun-Katana
Yami no Koe Zero
@@ -498,6 +505,10 @@ Ippai Shimasho
*.gpc+*.gph
*.snd+*.snh
*.snr+*.snh-NoEushully Genrin no Kishougun
+*.mpk-Nopropeller +Bullet Butlers
+ +*.mgr-No

1 Non-encrypted only