diff --git a/ArcFormats/ArcFormats.csproj b/ArcFormats/ArcFormats.csproj index 2126c32b..5a46885f 100644 --- a/ArcFormats/ArcFormats.csproj +++ b/ArcFormats/ArcFormats.csproj @@ -89,6 +89,9 @@ + + + diff --git a/ArcFormats/RealLive/ArcOVK.cs b/ArcFormats/RealLive/ArcOVK.cs new file mode 100644 index 00000000..b6d8a9d9 --- /dev/null +++ b/ArcFormats/RealLive/ArcOVK.cs @@ -0,0 +1,75 @@ +//! \file ArcOVK.cs +//! \date Mon Apr 18 14:59:12 2016 +//! \brief RealLive engine audio 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; + +namespace GameRes.Formats.RealLive +{ + [Export(typeof(ArchiveFormat))] + public class OvkOpener : ArchiveFormat + { + public override string Tag { get { return "OVK"; } } + public override string Description { get { return "RealLive 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) + { + int count = file.View.ReadInt32 (0); + if (!IsSaneCount (count)) + return null; + uint data_offset = 4 + (uint)count * 0x10; + if (data_offset >= file.MaxOffset) + return null; + + var base_name = Path.GetFileNameWithoutExtension (file.Name); + uint index_offset = 4; + var dir = new List (count); + for (int i = 0; i < count; ++i) + { + uint size = file.View.ReadUInt32 (index_offset); + uint offset = file.View.ReadUInt32 (index_offset+4); + uint id = file.View.ReadUInt32 (index_offset+8); + if (offset < data_offset) + return null; + var entry = new Entry { + Name = string.Format ("{0}#{1:D5}.ogg", base_name, id), + Type = "audio", + Offset = offset, + Size = size, + }; + if (!entry.CheckPlacement (file.MaxOffset)) + return null; + dir.Add (entry); + index_offset += 0x10; + } + return new ArcFile (file, this, dir); + } + } +} diff --git a/ArcFormats/RealLive/AudioNWA.cs b/ArcFormats/RealLive/AudioNWA.cs new file mode 100644 index 00000000..05d2eed6 --- /dev/null +++ b/ArcFormats/RealLive/AudioNWA.cs @@ -0,0 +1,252 @@ +//! \file AudioNWA.cs +//! \date Mon Apr 18 15:10:59 2016 +//! \brief RealLive audio 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; +using System.ComponentModel.Composition; +using System.IO; +using GameRes.Utility; + +namespace GameRes.Formats.RealLive +{ + internal class NwaMetaData + { + public WaveFormat Format; + public int Compression; + public bool RunLengthEncoded; + public int BlockCount; + public int PcmSize; + public int PackedSize; + public int SampleCount; + public int BlockSize; + public int FinalBlockSize; + } + + [Export(typeof(AudioFormat))] + public class NwaAudio : AudioFormat + { + public override string Tag { get { return "NWA"; } } + public override string Description { get { return "RealLive engine audio format"; } } + public override uint Signature { get { return 0; } } + + public override SoundInput TryOpen (Stream file) + { + var header = new byte[0x28]; + if (header.Length != file.Read (header, 0, header.Length)) + return null; + ushort channels = LittleEndian.ToUInt16 (header, 0); + if (0 == channels || channels > 2) + return null; + ushort bps = LittleEndian.ToUInt16 (header, 2); + if (bps != 8 && bps != 16) + return null; + var info = new NwaMetaData + { + Compression = LittleEndian.ToInt32 (header, 8), + RunLengthEncoded = 0 != LittleEndian.ToInt32 (header, 0xC), + BlockCount = LittleEndian.ToInt32 (header, 0x10), + PcmSize = LittleEndian.ToInt32 (header, 0x14), + PackedSize = LittleEndian.ToInt32 (header, 0x18), + SampleCount = LittleEndian.ToInt32 (header, 0x1C), + BlockSize = LittleEndian.ToInt32 (header, 0x20), + FinalBlockSize = LittleEndian.ToInt32 (header, 0x24), + }; + if (info.PcmSize <= 0) + return null; + info.Format.FormatTag = 1; + info.Format.Channels = channels; + info.Format.BitsPerSample = bps; + info.Format.SamplesPerSecond = LittleEndian.ToUInt32 (header, 4); + info.Format.BlockAlign = (ushort)(channels * bps/8); + info.Format.AverageBytesPerSecond = info.Format.BlockAlign * info.Format.SamplesPerSecond; + if (-1 == info.Compression) + { + if (info.PcmSize > file.Length - 0x2C) + return null; + return new RawPcmInput (new StreamRegion (file, 0x2C, info.PcmSize), info.Format); + } + if (info.Compression > 5) + return null; + if (info.PcmSize != info.SampleCount * bps / 8) + return null; + using (var decoder = new NwaDecoder (file, info)) + { + decoder.Decode(); + var pcm = new MemoryStream (decoder.Output); + var sound = new RawPcmInput (pcm, info.Format); + file.Dispose(); + return sound; + } + } + } + + internal sealed class NwaDecoder : IDisposable + { + BinaryReader m_input; + byte[] m_output; + NwaMetaData m_info; + short[] m_sample; + LsbBitStream m_bits; + + public byte[] Output { get { return m_output; } } + + + public NwaDecoder (Stream input, NwaMetaData info) + { + m_input = new ArcView.Reader (input); + m_info = info; + m_output = new byte[m_info.PcmSize]; + m_sample = new short[2]; + m_bits = new LsbBitStream (input, true); + } + + int m_dst; + + public void Decode () + { + m_input.BaseStream.Position = 0x2C; + var offsets = new uint[m_info.BlockCount]; + for (int i = 0; i < offsets.Length; ++i) + offsets[i] = m_input.ReadUInt32(); + + m_dst = 0; + for (int i = 0; i < offsets.Length-1; ++i) + { + m_input.BaseStream.Position = offsets[i]; + DecodeBlock (m_info.BlockSize); + } + m_input.BaseStream.Position = offsets[offsets.Length-1]; + if (m_info.FinalBlockSize > 0) + DecodeBlock (m_info.FinalBlockSize); + else + DecodeBlock (m_info.BlockSize); + } + + void DecodeBlock (int block_size) + { + int channel_count = m_info.Format.Channels; + for (int c = 0; c < channel_count; ++c) + { + if (8 == m_info.Format.BitsPerSample) + m_sample[c] = m_input.ReadByte(); + else + m_sample[c] = m_input.ReadInt16(); + } + m_bits.Reset(); + int channel = 0; + int repeat_count = 0; + for (int i = 0; i < block_size; ++i) + { + if (0 == repeat_count) + { + int ctl = m_bits.GetBits (3); + if (7 == ctl) + { + if (1 == m_bits.GetNextBit()) + { + m_sample[channel] = 0; + } + else + { + int bits = 8; + int shift = 9; + if (m_info.Compression < 3) + { + bits -= m_info.Compression; + shift += m_info.Compression; + } + int sign_bit = 1 << (bits - 1); + int mask = sign_bit - 1; + int val = m_bits.GetBits (bits); + if (0 != (val & sign_bit)) + m_sample[channel] -= (short)((val & mask) << shift); + else + m_sample[channel] += (short)((val & mask) << shift); + } + } + else if (ctl != 0) + { + int bits, shift; + if (m_info.Compression < 3) + { + bits = 5 - m_info.Compression; + shift = 2 + ctl + m_info.Compression; + } + else + { + bits = 3 + m_info.Compression; + shift = 1 + ctl; + } + int sign_bit = 1 << (bits - 1); + int mask = sign_bit - 1; + int val = m_bits.GetBits (bits); + if (0 != (val & sign_bit)) + m_sample[channel] -= (short)((val & mask) << shift); + else + m_sample[channel] += (short)((val & mask) << shift); + } + else if (m_info.RunLengthEncoded) + { + repeat_count = m_bits.GetNextBit(); + if (1 == repeat_count) + { + repeat_count = m_bits.GetBits (2); + if (3 == repeat_count) + repeat_count = m_bits.GetBits (8); + } + } + } + else + { + --repeat_count; + } + + if (8 == m_info.Format.BitsPerSample) + { + m_output[m_dst++] = (byte)m_sample[channel]; + } + else + { + LittleEndian.Pack (m_sample[channel], m_output, m_dst); + m_dst += 2; + } + if (2 == channel_count) + channel ^= 1; + } + } + + #region IDisposable Members + bool _disposed = false; + public void Dispose () + { + if (!_disposed) + { + m_bits.Dispose(); + m_input.Dispose(); + _disposed = true; + } + } + #endregion + } +} diff --git a/ArcFormats/RealLive/ImageG00.cs b/ArcFormats/RealLive/ImageG00.cs new file mode 100644 index 00000000..c0065b3e --- /dev/null +++ b/ArcFormats/RealLive/ImageG00.cs @@ -0,0 +1,270 @@ +//! \file ImageG00.cs +//! \date Mon Apr 18 14:06:48 2016 +//! \brief RealLive engine 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; +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.IO; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using GameRes.Utility; + +namespace GameRes.Formats.RealLive +{ + internal class G00MetaData : ImageMetaData + { + public int Type; + } + + [Export(typeof(ImageFormat))] + public class G00Format : ImageFormat + { + public override string Tag { get { return "G00"; } } + public override string Description { get { return "RealLive engine image format"; } } + public override uint Signature { get { return 0; } } + + public override ImageMetaData ReadMetaData (Stream stream) + { + int type = stream.ReadByte(); + if (type > 2) + return null; + using (var reader = new ArcView.Reader (stream)) + { + uint width = reader.ReadUInt16(); + uint height = reader.ReadUInt16(); + if (0 == width || width > 0x8000 || 0 == height || height > 0x8000) + return null; + if (2 == type) + { + int count = reader.ReadInt32(); + if (count <= 0 || count > 0x100) + return null; + } + else + { + uint length = reader.ReadUInt32(); + if (length + 5 != stream.Length) + return null; + } + return new G00MetaData { + Width = width, + Height = height, + BPP = 1 == type ? 8 : 24, + Type = type, + }; + } + } + + public override ImageData Read (Stream stream, ImageMetaData info) + { + using (var reader = new G00Reader (stream, (G00MetaData)info)) + { + reader.Unpack(); + return ImageData.Create (info, reader.Format, reader.Palette, reader.Data); + } + } + + public override void Write (Stream file, ImageData image) + { + throw new NotImplementedException ("G00Format.Write not implemented"); + } + } + + internal class Tile + { + public int X; + public int Y; + public uint Offset; + public int Length; + } + + internal sealed class G00Reader : IDisposable + { + BinaryReader m_input; + byte[] m_output; + int m_width; + int m_height; + int m_type; + int m_bytes_pp; + + public byte[] Data { get { return m_output; } } + public PixelFormat Format { get; private set; } + public BitmapPalette Palette { get; private set; } + + public G00Reader (Stream input, G00MetaData info) + { + m_width = (int)info.Width; + m_height = (int)info.Height; + m_type = info.Type; + m_input = new ArcView.Reader (input); + } + + public void Unpack () + { + m_input.BaseStream.Position = 5; + if (0 == m_type) + UnpackV0(); + else if (1 == m_type) + UnpackV1(); + else + UnpackV2(); + } + + void UnpackV0 () + { + m_bytes_pp = 3; + m_output = LzDecompress (1); + Format = PixelFormats.Bgr24; + } + + void UnpackV1 () + { + m_bytes_pp = 1; + m_output = LzDecompress (2); + int colors = LittleEndian.ToUInt16 (m_output, 0); + int src = 2; + var palette = new Color[colors]; + for (int i = 0; i < colors; ++i) + { + palette[i] = Color.FromArgb (m_output[src+3], m_output[src+2], m_output[src+1], m_output[src]); + src += 4; + } + Palette = new BitmapPalette (palette); + Format = PixelFormats.Indexed8; + Buffer.BlockCopy (m_output, src, m_output, 0, m_output.Length-src); + } + + void UnpackV2 () + { + m_bytes_pp = 1; + Format = PixelFormats.Bgra32; + int tile_count = m_input.ReadInt32(); + var tiles = new List (tile_count); + for (int i = 0; i < tile_count; ++i) + { + var tile = new Tile(); + tile.X = m_input.ReadInt32(); + tile.Y = m_input.ReadInt32(); + tiles.Add (tile); + m_input.BaseStream.Seek (0x10, SeekOrigin.Current); + } + using (var input = new MemoryStream (LzDecompress (2))) + using (var reader = new BinaryReader (input)) + { + if (reader.ReadInt32() != tile_count) + throw new InvalidFormatException(); + int dst_stride = m_width * 4; + m_output = new byte[m_height * dst_stride]; + for (int i = 0; i < tile_count; ++i) + { + tiles[i].Offset = reader.ReadUInt32(); + tiles[i].Length = reader.ReadInt32(); + } + foreach (var tile in tiles) + { + if (0 == tile.Length) + continue; + input.Position = tile.Offset; + int tile_type = reader.ReadUInt16(); + int count = reader.ReadUInt16(); + if (tile_type != 1) + throw new InvalidFormatException(); + input.Seek (0x70, SeekOrigin.Current); + for (int i = 0; i < count; ++i) + { + int tile_x = reader.ReadUInt16(); + int tile_y = reader.ReadUInt16(); + reader.ReadInt16(); + int tile_width = reader.ReadUInt16(); + int tile_height = reader.ReadUInt16(); + input.Seek (0x52, SeekOrigin.Current); + + tile_x += tile.X; + tile_y += tile.Y; + if (tile_x + tile_width > m_width || tile_y + tile_height > m_height) + throw new InvalidFormatException(); + int dst = tile_y * dst_stride + tile_x * 4; + int tile_stride = tile_width * 4; + for (int row = 0; row < tile_height; ++row) + { + reader.Read (m_output, dst, tile_stride); + dst += dst_stride; + } + } + } + } + } + + byte[] LzDecompress (int min_count) + { + int packed_size = m_input.ReadInt32() - 8; + int output_size = m_input.ReadInt32(); + var output = new byte[output_size]; + int dst = 0; + int bits = 2; + while (dst < output.Length && packed_size > 0) + { + bits >>= 1; + if (1 == bits) + { + bits = m_input.ReadByte() | 0x100; + --packed_size; + } + if (0 != (bits & 1)) + { + m_input.Read (output, dst, m_bytes_pp); + dst += m_bytes_pp; + packed_size -= m_bytes_pp; + } + else + { + if (packed_size < 2) + break; + int offset = m_input.ReadUInt16(); + packed_size -= 2; + int count = (offset & 0xF) + min_count; + offset >>= 4; + offset *= m_bytes_pp; + count *= m_bytes_pp; + Binary.CopyOverlapped (output, dst-offset, dst, count); + dst += count; + } + } + return output; + } + + #region IDisposable Members + bool _disposed = false; + public void Dispose () + { + if (!_disposed) + { + m_input.Dispose(); + _disposed = true; + } + } + #endregion + } +} diff --git a/supported.html b/supported.html index 3ee4df58..5bffe42e 100644 --- a/supported.html +++ b/supported.html @@ -250,9 +250,11 @@ Soukan Yuugi 2
*.asd-No *.dat-YesStudio e.go!Men at Work 2 *.mbl-NoMarble +Assault Angel Canon
Chikatetsu Fuusa Jiken
Eien no Owari ni
Ikusa Otome Valkyrie
+Mahou Tenshi Misaki 2
Onsoku Hishou Sonic Mercedes
Sakura Machizaka Stories vol.1
Sakura Machizaka Stories vol.2
@@ -306,6 +308,7 @@ Chikan Circle ShiinaRio v2.46
Chikan Circle 2 ShiinaRio v2.47
Chuuchuu Nurse ShiinaRio v2.45
Classmate no Okaa-san ShiinaRio v2.37
+Draculius ShiinaRio v2.38
Enkaku Sousa 2.36 or 2.37
Helter Skelter ShiinaRio v2.40
Hitozuma Onna Kyoushi Reika ShiinaRio v2.39
@@ -780,6 +783,11 @@ Angenehm Platz -Kleiner Garten Sie Erstellen-
*.zitZTNoSilky's Sweet ~Hanjuku na Tenshi-tachi~
+*.ovk-NoRealLive +Shiawase na Ohime-sama
+ +*.nwa-No +*.g00-No

1 Non-encrypted only