diff --git a/ArcFormats/ArcFormats.csproj b/ArcFormats/ArcFormats.csproj index 3fc0f578..6fe9a7a5 100644 --- a/ArcFormats/ArcFormats.csproj +++ b/ArcFormats/ArcFormats.csproj @@ -86,6 +86,7 @@ + @@ -123,6 +124,9 @@ + + + diff --git a/ArcFormats/ArcGPK.cs b/ArcFormats/ArcGPK.cs new file mode 100644 index 00000000..75ceab2e --- /dev/null +++ b/ArcFormats/ArcGPK.cs @@ -0,0 +1,89 @@ +//! \file ArcGPK.cs +//! \date Sat Aug 01 12:01:26 2015 +//! \brief Black Cyc engine images 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.BlackCyc +{ + [Export(typeof(ArchiveFormat))] + public class GpkOpener : ArchiveFormat + { + public override string Tag { get { return "GPK"; } } + public override string Description { get { return "Black Cyc engine images 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) + { + if (!file.Name.EndsWith (".gpk", StringComparison.InvariantCultureIgnoreCase)) + return null; + var gtb_name = Path.ChangeExtension (file.Name, "gtb"); + var gtb_info = new FileInfo (gtb_name); + if (!gtb_info.Exists) + return null; + using (var gtb = new ArcView (gtb_name)) + { + int count = gtb.View.ReadInt32 (0); + if (!IsSaneCount (count)) + return null; + + gtb.View.Reserve (0, (uint)gtb.MaxOffset); + int name_index = 4; + int offsets_index = name_index + count * 4; + int name_base = offsets_index + count * 4; + if (name_base >= gtb.MaxOffset) + return null; + uint next_offset = gtb.View.ReadUInt32 (offsets_index); + var dir = new List (count); + for (int i = 0; i < count; ++i) + { + offsets_index += 4; + int name_offset = name_base + gtb.View.ReadInt32 (name_index); + name_index += 4; + if (name_offset < name_base || name_offset >= gtb.MaxOffset) + return null; + string name = gtb.View.ReadString (name_offset, (uint)(gtb.MaxOffset-name_offset)); + name += ".dwq"; + var entry = FormatCatalog.Instance.CreateEntry (name); + entry.Offset = next_offset; + if (i + 1 == count) + next_offset = (uint)file.MaxOffset; + else + next_offset = gtb.View.ReadUInt32 (offsets_index); + entry.Size = next_offset - (uint)entry.Offset; + if (!entry.CheckPlacement (file.MaxOffset)) + return null; + dir.Add (entry); + } + return new ArcFile (file, this, dir); + } + } + } +} diff --git a/ArcFormats/ArcVPK.cs b/ArcFormats/ArcVPK.cs new file mode 100644 index 00000000..1b843dff --- /dev/null +++ b/ArcFormats/ArcVPK.cs @@ -0,0 +1,77 @@ +//! \file ArcVPK.cs +//! \date Sat Aug 01 11:37:55 2015 +//! \brief Black Cyc engine audio 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.BlackCyc +{ + [Export(typeof(ArchiveFormat))] + public class VpkOpener : ArchiveFormat + { + public override string Tag { get { return "VPK"; } } + public override string Description { get { return "Black Cyc engine audio 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) + { + if (!file.Name.EndsWith (".vpk", StringComparison.InvariantCultureIgnoreCase)) + return null; + var vtb_name = Path.ChangeExtension (file.Name, "vtb"); + var vtb_info = new FileInfo (vtb_name); + if (!vtb_info.Exists) + return null; + int count = (int)(vtb_info.Length / 0x0C) - 1; + if (!IsSaneCount (count)) + return null; + + using (var vtb = new ArcView (vtb_name)) + { + vtb.View.Reserve (0, (uint)vtb.MaxOffset); + uint index_offset = 0; + uint next_offset = vtb.View.ReadUInt32 (8); + var dir = new List (count); + for (int i = 0; i < count; ++i) + { + string name = vtb.View.ReadString (index_offset, 8) + ".vaw"; + var entry = FormatCatalog.Instance.CreateEntry (name); + entry.Offset = next_offset; + index_offset += 0xC; + next_offset = vtb.View.ReadUInt32 (index_offset+8); + entry.Size = next_offset - (uint)entry.Offset; + if (!entry.CheckPlacement (file.MaxOffset)) + return null; + dir.Add (entry); + } + return new ArcFile (file, this, dir); + } + } + } +} diff --git a/ArcFormats/AudioVAW.cs b/ArcFormats/AudioVAW.cs new file mode 100644 index 00000000..3be0032b --- /dev/null +++ b/ArcFormats/AudioVAW.cs @@ -0,0 +1,96 @@ +//! \file AudioVAW.cs +//! \date Sat Aug 01 12:28:22 2015 +//! \brief Black Cyc audio file. +// +// 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.ComponentModel.Composition; +using System.IO; +using System.Linq; +using System.Text; +using GameRes.Utility; + +namespace GameRes.Formats.BlackCyc +{ + [Export(typeof(AudioFormat))] + public class VawAudio : AudioFormat + { + public override string Tag { get { return "VAW"; } } + public override string Description { get { return "Black Cyc audio format"; } } + public override uint Signature { get { return 0; } } + + public VawAudio () + { + Extensions = new string[] { "vaw", "wgq" }; + } + + static readonly Lazy WavFormat = new Lazy (() => FormatCatalog.Instance.AudioFormats.FirstOrDefault (x => x.Tag == "WAV")); + static readonly Lazy OggFormat = new Lazy (() => FormatCatalog.Instance.AudioFormats.FirstOrDefault (x => x.Tag == "OGG")); + + public override SoundInput TryOpen (Stream file) + { + var header = ResourceHeader.Read (file); + if (null == header) + return null; + AudioFormat format; + int offset; + if (0 == header.PackType) + { + if (4 != file.Read (header.Bytes, 0, 4)) + return null; + if (!Binary.AsciiEqual (header.Bytes, "RIFF")) + return null; + format = WavFormat.Value; + offset = 0x40; + } + else if (2 == header.PackType) + { + format = OggFormat.Value; + offset = 0x6C; + } + else if (6 == header.PackType && Binary.AsciiEqual (header.Bytes, 0x10, "OGG ")) + { + format = OggFormat.Value; + offset = 0x40; + } + else + return null; + var input = new StreamRegion (file, offset, file.Length-offset, true); + try + { + return format.TryOpen (input); + // FIXME file will be left undisposed + } + catch + { + input.Dispose(); + throw; + } + } + + public override void Write (SoundInput source, Stream output) + { + throw new System.NotImplementedException ("EdimFormat.Write not implemenented"); + } + } +} diff --git a/ArcFormats/ImageDWQ.cs b/ArcFormats/ImageDWQ.cs new file mode 100644 index 00000000..795a0831 --- /dev/null +++ b/ArcFormats/ImageDWQ.cs @@ -0,0 +1,296 @@ +//! \file ImageDWQ.cs +//! \date Sat Aug 01 13:18:46 2015 +//! \brief Black Cyc image format. +// +// 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.ComponentModel.Composition; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using GameRes.Utility; + +namespace GameRes.Formats.BlackCyc +{ + internal class ResourceHeader + { + public static readonly Regex PackTypeRe = new Regex (@"^PACKTYPE=(\d+)(A?) +$"); + + public byte[] Bytes { get; private set; } + public int PackType { get; private set; } + public bool AType { get; private set; } + + public static ResourceHeader Read (Stream file) + { + var header = new ResourceHeader { Bytes = new byte[0x40] }; + if (0x40 != file.Read (header.Bytes, 0, 0x40)) + return null; + + var header_string = Encoding.ASCII.GetString (header.Bytes, 0x30, 0x10); + var match = PackTypeRe.Match (header_string); + if (!match.Success) + return null; + header.PackType = ushort.Parse (match.Groups[1].Value); + header.AType = match.Groups[2].Value.Length > 0; + return header; + } + } + + internal class DwqMetaData : ImageMetaData + { + public string BaseType; + public int PackedSize; + public int PackType; + public bool AType; + } + + [Export(typeof(ImageFormat))] + public class DwqFormat : ImageFormat + { + public override string Tag { get { return "DWQ"; } } + public override string Description { get { return "Black Cyc image format"; } } + public override uint Signature { get { return 0; } } + + public DwqFormat () + { + Signatures = new uint[] { 0x4745504A, 0x20504D42, 0x20474E50, 0x4B434150 }; + } + + static ImageFormat GetFormat (string tag) + { + return FormatCatalog.Instance.ImageFormats.FirstOrDefault (x => x.Tag == tag); + } + + static readonly Lazy JpegFormat = new Lazy (() => GetFormat ("JPEG")); + static readonly Lazy PngFormat = new Lazy (() => GetFormat ("PNG")); + + public override ImageMetaData ReadMetaData (Stream stream) + { + var header = ResourceHeader.Read (stream); + if (null == header) + return null; + return new DwqMetaData + { + Width = LittleEndian.ToUInt32 (header.Bytes, 0x24), + Height = LittleEndian.ToUInt32 (header.Bytes, 0x28), + BPP = 32, + BaseType = Encoding.ASCII.GetString (header.Bytes, 0, 0x10).TrimEnd(), + PackedSize = LittleEndian.ToInt32 (header.Bytes, 0x20), + PackType = header.PackType, + AType = header.AType, + }; + } + + public override ImageData Read (Stream stream, ImageMetaData info) + { + var meta = info as DwqMetaData; + if (null == meta) + throw new ArgumentException ("DwqFormat.Read should be supplied with DwqMetaData", "info"); + + BitmapSource bitmap = null; + switch (meta.PackType) + { + case 5: // JPEG + using (var jpeg = new StreamRegion (stream, 0x40, stream.Length-0x40, true)) + return JpegFormat.Value.Read (jpeg, info); + + case 8: // PNG + using (var png = new StreamRegion (stream, 0x40, stream.Length-0x40, true)) + return PngFormat.Value.Read (png, info); + + case 0: // BMP + using (var bmp = new StreamRegion (stream, 0x40, stream.Length-0x40, true)) + { + var decoder = new BmpBitmapDecoder (bmp, BitmapCreateOptions.None, BitmapCacheOption.OnLoad); + // non-conforming BMP, flip image vertically + bitmap = new TransformedBitmap (decoder.Frames[0], new ScaleTransform { ScaleY = -1 }); + return new ImageData (bitmap, info); + } + + case 7: // JPEG+MASK + using (var jpeg = new StreamRegion (stream, 0x40, meta.PackedSize, true)) + { + var decoder = new JpegBitmapDecoder (jpeg, BitmapCreateOptions.None, BitmapCacheOption.OnLoad); + bitmap = decoder.Frames[0]; + } + break; + + case 3: // PACKBMP+MASK + using (var bmp = new StreamRegion (stream, 0x40, meta.PackedSize, true)) + { + var reader = new DwqBmpReader (bmp, meta); + reader.Unpack(); + bitmap = BitmapSource.Create ((int)info.Width, (int)info.Height, + ImageData.DefaultDpiX, ImageData.DefaultDpiY, + reader.Format, reader.Palette, reader.Data, reader.Stride); + } + break; + } + if (null == bitmap) + throw new NotImplementedException(); + if (meta.AType) + { + int mask_offset = 0x40+meta.PackedSize; + using (var mask = new StreamRegion (stream, mask_offset, stream.Length-mask_offset, true)) + { + var reader = new DwqBmpReader (mask, meta); + if (8 == reader.Format.BitsPerPixel) // mask should be represented as 8bpp bitmap + { + reader.Unpack(); + var alpha = reader.Data; + var palette = reader.Palette.Colors; + for (int i = 0; i < alpha.Length; ++i) + { + var color = palette[alpha[i]]; + int A = (color.R + color.G + color.B) / 3; + alpha[i] = (byte)A; + } + bitmap = ApplyAlphaChannel (bitmap, reader.Data); + } + } + } + bitmap.Freeze(); + return new ImageData (bitmap, meta); + } + + public override void Write (Stream file, ImageData image) + { + throw new NotImplementedException ("DwqFormat.Write not implemented"); + } + + private BitmapSource ApplyAlphaChannel (BitmapSource bitmap, byte[] alpha) + { + if (bitmap.Format.BitsPerPixel != 32) + bitmap = new FormatConvertedBitmap (bitmap, PixelFormats.Bgr32, null, 0); + + int stride = bitmap.PixelWidth * 4; + byte[] pixels = new byte[stride * bitmap.PixelHeight]; + int asrc = 0; + bitmap.CopyPixels (pixels, stride, 0); + for (int dst = 3; dst < pixels.Length; dst += 4) + { + pixels[dst] = alpha[asrc++]; + } + return BitmapSource.Create (bitmap.PixelWidth, bitmap.PixelHeight, + ImageData.DefaultDpiX, ImageData.DefaultDpiY, + PixelFormats.Bgra32, null, pixels, stride); + } + } + + internal class DwqBmpReader + { + Stream m_input; + byte[] m_pixels; + int m_width; + int m_height; + + public byte[] Data { get { return m_pixels; } } + public int Stride { get; private set; } + public PixelFormat Format { get; private set; } + public BitmapPalette Palette { get; private set; } + + public DwqBmpReader (Stream input, DwqMetaData info) + { + m_input = input; + m_width = (int)info.Width; + m_height = (int)info.Height; + var header = new byte[0x36]; + if (header.Length != m_input.Read (header, 0, header.Length)) + throw new InvalidFormatException(); + int w = LittleEndian.ToInt32 (header, 0x12); + int h = LittleEndian.ToInt32 (header, 0x16); + if (w != m_width || h != m_height) + throw new InvalidFormatException(); + + int bpp = LittleEndian.ToUInt16 (header, 0x1C); + switch (bpp) + { + case 8: Format = PixelFormats.Indexed8; Stride = m_width; break; + case 16: Format = PixelFormats.Bgr565; Stride = m_width*2; break; + case 24: Format = PixelFormats.Bgr24; Stride = m_width*3; break; + case 32: Format = PixelFormats.Bgr32; Stride = m_width*4; break; + default: throw new InvalidFormatException(); + } + if (8 == bpp) + { + int colors = Math.Min (LittleEndian.ToInt32 (header, 0x2E), 0x100); + ReadPalette (colors); + } + uint data_position = LittleEndian.ToUInt32 (header, 0xA); + m_input.Position = data_position; + m_pixels = new byte[Stride*m_height]; + } + + private void ReadPalette (int colors) + { + int palette_size = colors * 4; + var palette_data = new byte[palette_size]; + if (palette_size != m_input.Read (palette_data, 0, palette_size)) + throw new InvalidFormatException(); + var palette = new Color[colors]; + for (int i = 0; i < palette.Length; ++i) + { + byte r = palette_data[i*4]; + byte g = palette_data[i*4+1]; + byte b = palette_data[i*4+2]; + palette[i] = Color.FromRgb (r, g, b); + } + Palette = new BitmapPalette (palette); + } + + public void Unpack () // sub_408990 + { + var prev_line = new byte[Stride]; + int dst = 0; + for (int y = 0; y < m_height; ++y) + { + for (int x = 0; x < Stride; ) + { + int b = m_input.ReadByte(); + if (0 != b) + { + if (-1 == b) + throw new EndOfStreamException(); + m_pixels[dst + x++] = (byte)b; + } + else + { + int count = m_input.ReadByte(); + if (-1 == count) + throw new EndOfStreamException(); + for (int i = 0; i < count; ++i) + m_pixels[dst + x++] = 0; + } + } + for (int i = 0; i < Stride; ++i) + { + m_pixels[dst] ^= prev_line[i]; + prev_line[i] = m_pixels[dst++]; + } + } + } + } +} diff --git a/ArcFormats/Properties/AssemblyInfo.cs b/ArcFormats/Properties/AssemblyInfo.cs index a7bfbed3..3c117ea7 100644 --- a/ArcFormats/Properties/AssemblyInfo.cs +++ b/ArcFormats/Properties/AssemblyInfo.cs @@ -32,5 +32,5 @@ using System.Runtime.InteropServices; // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion ("1.1.8.87")] -[assembly: AssemblyFileVersion ("1.1.8.87")] +[assembly: AssemblyVersion ("1.1.8.88")] +[assembly: AssemblyFileVersion ("1.1.8.88")] diff --git a/supported.html b/supported.html index d5f76c45..1b0e1624 100644 --- a/supported.html +++ b/supported.html @@ -37,7 +37,7 @@ Shoujo Settai
*.binACPXPK01NoUnison ShiftWasurenagusa ~Forget-me-Not~ *.gsp-NoBlack RainbowSaimin Gakuen *.bmzZLC3Yes -*.intKIFYes[1]CatSystem2 +*.intKIFYes1CatSystem2 Grisaia no Kajitsu
Makai Tenshi Djibril -Episode 4-
Shukufuku no Campanella
@@ -216,6 +216,7 @@ Mainichi Shabutte Ii Desu ka?
*.datSPackNoGoku-FeroInchuu Reiki Elenova *.fpk-NoCandy SoftJii -Nozoki no Houshuu- *.noa
*.datEntis\x1aNoEntis GLS +Alea Akaki Tsuki o Haruka ni Nozomi
Konneko
Yatohime Zankikou
@@ -286,6 +287,11 @@ Mamagoto
*.iaf-No *.bin+*.pakhedNoelfAdult Video King *.hip
*.hiz
hip
hizNo +*.gpk+*.gtb
*.vpk+*.vtb-NoBlack Cyc +Yami no Koe Zero
+ +*.dwqBMP
JPEG
PNG
JPEG+MASK
PACKBMP+MASK
No +*.vaw
*.wgqIF PACKTYPE==
OGGNo

1 Non-encrypted only