diff --git a/ArcFormats/ArcFormats.csproj b/ArcFormats/ArcFormats.csproj index 411a0fa9..6c1df55b 100644 --- a/ArcFormats/ArcFormats.csproj +++ b/ArcFormats/ArcFormats.csproj @@ -86,6 +86,8 @@ + + diff --git a/ArcFormats/CaramelBox/ArcARC4.cs b/ArcFormats/CaramelBox/ArcARC4.cs new file mode 100644 index 00000000..37f3c4c9 --- /dev/null +++ b/ArcFormats/CaramelBox/ArcARC4.cs @@ -0,0 +1,368 @@ +//! \file ArcARC4.cs +//! \date Wed Aug 10 15:50:21 2016 +//! \brief Caramel BOX resource archive. +// +// 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; +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.IO; +using GameRes.Utility; + +namespace GameRes.Formats.CaramelBox +{ + internal class Arc4Entry : PackedEntry + { + public List Segments; + } + + [Export(typeof(ArchiveFormat))] + public class Arc4Opener : ArchiveFormat + { + public override string Tag { get { return "ARC4"; } } + public override string Description { get { return "Caramel BOX resource archive"; } } + public override uint Signature { get { return 0x34435241; } } // 'ARC4' + public override bool IsHierarchic { get { return false; } } + public override bool CanCreate { get { return false; } } + + public Arc4Opener () + { + Extensions = new string[] { "bin", "dat", "データ" }; + } + + public override ArcFile TryOpen (ArcView file) + { + if (0x010000 != file.View.ReadUInt32 (4)) + return null; + int count = file.View.ReadInt32 (0x10); + if (!IsSaneCount (count)) + return null; + uint index_length = file.View.ReadUInt32 (8); + uint alignment = file.View.ReadUInt32 (0xC); + int index_offset = file.View.ReadInt32 (0x14); + int names_offset = file.View.ReadInt32 (0x1C) - index_offset; + int segment_table = file.View.ReadInt32 (0x24) - index_offset; + uint base_offset = file.View.ReadUInt32 (0x2C); + if (0 == alignment || index_offset <= 0 || names_offset <= 0 || segment_table <= 0) + return null; + if (!file.View.AsciiEqual (index_offset, "tZ")) + return null; + byte[] index; + using (var packed = file.CreateStream (index_offset, index_length)) + using (var tz = new TzCompression (packed)) + index = tz.Unpack(); + + int current_offset = 0; + var dir = new List (count); + for (int i = 0; i < count; ++i) + { + int name_pos = ReadInt24 (index, current_offset) * 2; + int name_length = index[current_offset+3]; + int chunk_count = index[current_offset+4]; + int offset = ReadInt24 (index, current_offset+5); + var name = Binary.GetCString (index, names_offset + name_pos, name_length); + var entry = FormatCatalog.Instance.Create (name); + entry.Segments = new List (chunk_count); + if (chunk_count > 1) + { + int segment_pos = segment_table + 3 * offset; + for (int j = 0; j < chunk_count; ++j) + { + entry.Segments.Add (ReadInt24 (index, segment_pos) + base_offset); + segment_pos += 3; + } + } + else + entry.Segments.Add (offset + base_offset); + + for (int j = 0; j < chunk_count; ++j) + entry.Segments[j] *= alignment; + dir.Add (entry); + current_offset += 8; + } + foreach (Arc4Entry entry in dir) + { + uint size = 0; + foreach (var segment in entry.Segments) + { + size += Binary.BigEndian (file.View.ReadUInt32 (segment+4)); + } + entry.Offset = entry.Segments[0] + 0x10; + entry.Size = size; + entry.IsPacked = file.View.AsciiEqual (entry.Offset, "tZ"); + if (entry.IsPacked) + entry.UnpackedSize = file.View.ReadUInt32 (entry.Offset+2); + else + entry.UnpackedSize = size; + } + return new ArcFile (file, this, dir); + } + + public override Stream OpenEntry (ArcFile arc, Entry entry) + { + var xent = (Arc4Entry)entry; + Stream input; + if (1 == xent.Segments.Count) + input = arc.File.CreateStream (entry.Offset, entry.Size); + else + input = new Arc4Stream (arc.File, xent); + if (!xent.IsPacked) + return input; + using (input) + using (var tz = new TzCompression (input)) + return new MemoryStream (tz.Unpack()); + } + + static int ReadInt24 (byte[] data, int pos) + { + return data[pos] << 16 | data[pos+1] << 8 | data[pos+2]; + } + } + + internal sealed class TzCompression : IDisposable + { + BinaryReader m_input; + + public TzCompression (Stream stream) + { + m_input = new ArcView.Reader (stream); + } + + public byte[] Unpack () + { + int signature = m_input.ReadUInt16(); + uint unpacked_size = m_input.ReadUInt32(); + var data = new byte[unpacked_size]; + int dst = 0; + var buffer = new byte[0x10000]; + while (dst < data.Length) + { + signature = m_input.ReadUInt16(); + ushort block_size = m_input.ReadUInt16(); + ushort unpacked_block_size = m_input.ReadUInt16(); + if (0 == unpacked_block_size) + throw new InvalidFormatException(); + + ushort key = m_input.ReadUInt16(); + if (block_size != m_input.Read (buffer, 0, block_size)) + throw new EndOfStreamException(); + DecryptBlock (buffer, 0, block_size, key); + if (0x7453 == signature) // 'St' + Buffer.BlockCopy (buffer, 0, data, dst, unpacked_block_size); + else if (0x745A == signature) // 'Zt' + UnpackBlock (buffer, 0, block_size, data, dst, unpacked_block_size); + else + throw new InvalidFormatException(); + dst += unpacked_block_size; + } + return data; + } + + unsafe void DecryptBlock (byte[] data, int pos, int count, uint key) + { + fixed (byte* data8 = &data[pos]) + { + ushort* data16 = (ushort*)data8; + for (int i = count / 2; i > 0; --i) + { + key *= 0x1465D9; + key += 0x0FB5; + *data16++ -= (ushort)(key >> 16); + } + } + } + + void UnpackBlock (byte[] input, int src, int input_size, + byte[] output, int dst, int output_size) + { + int src_end = src + input_size; + while (src < src_end) + { + int ctl = input[src++]; + if (0 == ctl) + break; + if (0 != (ctl & 0x80)) + { + int offset, count; + if (0 != (ctl & 0x40)) + { + if (0 != (ctl & 0x20)) + { + ctl = (ctl << 8) | input[src++]; + ctl = (ctl << 8) | input[src++]; + + count = (ctl & 0x3F) + 4; + offset = (ctl >> 6) & 0x7FFF; + } + else + { + ctl = (ctl << 8) | input[src++]; + + count = (ctl & 7) + 3; + offset = (ctl >> 3) & 0x3FF; + } + } + else + { + count = (ctl & 3) + 2; + offset = (ctl >> 2) & 0xF; + } + ++offset; + Binary.CopyOverlapped (output, dst - offset, dst, count); + dst += count; + } + else + { + Buffer.BlockCopy (input, src, output, dst, ctl); + src += ctl; + dst += ctl; + } + } + } + + #region IDisposable Members + bool _disposed = false; + public void Dispose () + { + if (!_disposed) + { + m_input.Dispose(); + _disposed = true; + } + } + #endregion + } + + internal class Arc4Stream : Stream + { + ArcView m_file; + IEnumerator m_segment; + Stream m_stream; + bool m_eof = false; + + public override bool CanRead { get { return !_disposed; } } + public override bool CanSeek { get { return false; } } + public override bool CanWrite { get { return false; } } + public override long Length { get { throw new NotSupportedException ("Arc4Stream.Length not supported"); } } + public override long Position + { + get { throw new NotSupportedException ("Arc4Stream.Position not supported."); } + set { throw new NotSupportedException ("Arc4Stream.Position not supported."); } + } + + public Arc4Stream (ArcView file, Arc4Entry entry) + { + m_file = file; + m_segment = entry.Segments.GetEnumerator(); + NextSegment(); + } + + private void NextSegment () + { + if (!m_segment.MoveNext()) + { + m_eof = true; + return; + } + var prev_stream = m_stream; + long offset = m_segment.Current; + var segment_size = Binary.BigEndian (m_file.View.ReadUInt32 (offset+4)); + m_stream = m_file.CreateStream (offset+0x10, segment_size); + if (null != prev_stream) + prev_stream.Dispose(); + } + + public override int Read (byte[] buffer, int offset, int count) + { + int total = 0; + while (!m_eof && count > 0) + { + int read = m_stream.Read (buffer, offset, count); + if (0 != read) + { + total += read; + offset += read; + count -= read; + } + if (0 != count) + NextSegment(); + } + return total; + } + + public override int ReadByte () + { + int b = -1; + while (!m_eof) + { + b = m_stream.ReadByte(); + if (-1 != b) + break; + NextSegment(); + } + return b; + } + + public override void Flush () + { + } + + public override long Seek (long offset, SeekOrigin origin) + { + throw new NotSupportedException ("Arc4Stream.Seek method is not supported"); + } + + public override void SetLength (long length) + { + throw new NotSupportedException ("Arc4Stream.SetLength method is not supported"); + } + + public override void Write (byte[] buffer, int offset, int count) + { + throw new NotSupportedException ("Arc4Stream.Write method is not supported"); + } + + public override void WriteByte (byte value) + { + throw new NotSupportedException("Arc4Stream.WriteByte method is not supported"); + } + + #region IDisposable Members + bool _disposed = false; + protected override void Dispose (bool disposing) + { + if (!_disposed) + { + if (disposing) + { + if (null != m_stream) + m_stream.Dispose(); + m_segment.Dispose(); + } + _disposed = true; + base.Dispose (disposing); + } + } + #endregion + } +} diff --git a/ArcFormats/CaramelBox/ImageFCB.cs b/ArcFormats/CaramelBox/ImageFCB.cs new file mode 100644 index 00000000..7d6daebc --- /dev/null +++ b/ArcFormats/CaramelBox/ImageFCB.cs @@ -0,0 +1,208 @@ +//! \file ImageFCB.cs +//! \date Thu Aug 11 04:21:02 2016 +//! \brief Caramel BOX compressed image format. +// +// 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 System.Windows.Media; +using GameRes.Compression; +using GameRes.Utility; + +namespace GameRes.Formats.CaramelBox +{ + internal class FcbMetaData : ImageMetaData + { + public int Method; + } + + [Export(typeof(ImageFormat))] + public class FcbFormat : ImageFormat + { + public override string Tag { get { return "FCB"; } } + public override string Description { get { return "Caramel BOX image format"; } } + public override uint Signature { get { return 0x31626366; } } // 'fcb1' + + public override ImageMetaData ReadMetaData (Stream stream) + { + var header = new byte[0x10]; + if (header.Length != stream.Read (header, 0, header.Length)) + return null; + return new FcbMetaData + { + Width = LittleEndian.ToUInt32 (header, 4), + Height = LittleEndian.ToUInt32 (header, 8), + Method = LittleEndian.ToInt32 (header, 12), + BPP = 32, + }; + } + + public override ImageData Read (Stream stream, ImageMetaData info) + { + var meta = (FcbMetaData)info; + byte[] input; + if (1 == meta.Method) + { + stream.Position = 0x14; + using (var reader = new ArcView.Reader (stream)) + { + int unpacked_size = Binary.BigEndian (reader.ReadInt32()); + reader.ReadInt32(); // packed_size + input = new byte[unpacked_size]; + using (var z = new ZLibStream (stream, CompressionMode.Decompress, true)) + if (unpacked_size != z.Read (input, 0, unpacked_size)) + throw new EndOfStreamException(); + } + } + else if (0 == meta.Method) + { + stream.Position = 0x10; + using (var tz = new TzCompression (stream)) + input = tz.Unpack(); + } + else + throw new InvalidFormatException(); + var pixels = Unpack (input, info); + return ImageData.Create (info, PixelFormats.Bgra32, null, pixels); + } + + public override void Write (Stream file, ImageData image) + { + throw new System.NotImplementedException ("FcbFormat.Write not implemented"); + } + + byte[] Unpack (byte[] input, ImageMetaData info) + { + byte[] ref_pixel = { 0x80, 0x80, 0x80, 0xFF }; + var pixel = new byte[4]; + var delta = new int[4]; + + var output = new byte[info.Width * info.Height * 4]; + int src = 0; + int dst = 0; + for (uint y = 0; y < info.Height; ++y) + { + pixel[0] = ref_pixel[0]; + pixel[1] = ref_pixel[1]; + pixel[2] = ref_pixel[2]; + pixel[3] = ref_pixel[3]; + + for (uint x = 0; x < info.Width; ++x) + { + int v = input[src++]; + if (0 != (v & 0x80)) + { + if (0 != (v & 0x40)) + { + if (0 != (v & 0x20)) + { + if (0 != (v & 0x10)) + { + if (0 != (v & 0x08)) + { + if (v == 0xFE) + { + delta[0] = input[src++] - 128; + delta[1] = input[src++] - 128; + delta[2] = input[src++] - 128; + delta[3] = 0; + } + else + { + delta[0] = input[src++] - 128; + delta[1] = input[src++] - 128; + delta[2] = input[src++] - 128; + delta[3] = input[src++] - 128; + } + } + else + { + v = input[src++] | v << 8; + v = input[src++] | v << 8; + v = input[src++] | v << 8; + delta[0] = ((v >> 20) & 0x7F) - 64; + delta[1] = ((v >> 14) & 0x3F) - 32; + delta[2] = ((v >> 8) & 0x3F) - 32; + delta[3] = v - 128; + } + } + else + { + v = input[src++] | v << 8; + v = input[src++] | v << 8; + delta[0] = ((v >> 14) & 0x3F) - 32; + delta[1] = ((v >> 10) & 0x0F) - 8; + delta[2] = ((v >> 6) & 0x0F) - 8; + delta[3] = (v & 0x3F) - 32; + } + } + else + { + v = input[src++] | v << 8; + v = input[src++] | v << 8; + delta[0] = ((v >> 13) & 0xFF) - 128; + delta[1] = ((v >> 7) & 0x3F) - 32; + delta[2] = (v & 0x7F) - 64; + delta[3] = 0; + } + } + else + { + v = input[src++] | v << 8; + delta[0] = ((v >> 8) & 0x3F) - 32; + delta[1] = ((v >> 4) & 0x0F) - 8; + delta[2] = (v & 0xf) - 8; + delta[3] = 0; + } + } + else + { + delta[0] = ((v >> 4) & 7) - 4; + delta[1] = ((v >> 2) & 3) - 2; + delta[2] = (v & 3) - 2; + delta[3] = 0; + } + + pixel[0] += (byte)(delta[0] + delta[1]); + pixel[1] += (byte)delta[0]; + pixel[2] += (byte)(delta[0] + delta[2]); + pixel[3] += (byte)delta[3]; + + output[dst++] = pixel[0]; + output[dst++] = pixel[1]; + output[dst++] = pixel[2]; + output[dst++] = pixel[3]; + + if (0 == x) + { + ref_pixel[0] = pixel[0]; + ref_pixel[1] = pixel[1]; + ref_pixel[2] = pixel[2]; + ref_pixel[3] = pixel[3]; + } + } + } + return output; + } + } +} diff --git a/supported.html b/supported.html index 6fc8ce97..13f9ae53 100644 --- a/supported.html +++ b/supported.html @@ -517,6 +517,7 @@ X Change 2R Eve to Iu Na no Omocha Hirusagari no Danchi In Tsuma Hissatsu Chikannin II +Kahanshin no Kabe ~ Shin Ryoujoku Chikan Jigoku Otomegari Ryoujoku Gojuusou Shinjin Kangofu Miho @@ -987,11 +988,13 @@ Kara no Shoujo 2 Aniyome Kyouka-san to Sono Haha Chikako-san Aru Kazoku no Kankeizu Cosplay Ecchi ~Layer Kana no Yuuutsu~ +Gakkou Yarashii Kaidan Hanamaru! 2 Hime Kami 1/2 In'youchuu Goku ~Ryoujoku Jigoku Taimaroku~ In'youchuu Rei ~Ryoujoku Shiro Taima Emaki~ Kuraibito +Naze ka Kanojo ga Boku ni Ecchi o Sematte Kuru Ken Onna Kyoushi Suzune Oshioki ~Gakuen Reijou Kousei Keikaku~ Ore Maou! ~Kudake Chitta Tamashii @@ -1030,6 +1033,10 @@ Uchuu Keiji Soldivan *.ttd.FRCNoMorning Binkan Ecchi! ~Futari no Oyatsu wa Tokunou Milk~ +*.binARC4NoCaramel BOX +Boku no Te no Naka no Rakuen + +*.fcbfcb1No 1 Non-encrypted only
1 Non-encrypted only