2023-10-27 04:06:23 +04:00

620 lines
23 KiB
C#

//! \file ArcGRP.cs
//! \date Sun Mar 20 02:07:17 2016
//! \brief Ankh 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.Compression;
using GameRes.Utility;
namespace GameRes.Formats.Ankh
{
[Export(typeof(ArchiveFormat))]
[ExportMetadata("Priority", -1)]
public class GrpOpener : ArchiveFormat
{
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 CanWrite { get { return false; } }
public GrpOpener ()
{
Extensions = new string[] { "grp", "bin", "dat", "vc" };
}
public override ArcFile TryOpen (ArcView file)
{
uint first_offset = file.View.ReadUInt32 (0);
if (first_offset < 8 || first_offset >= file.MaxOffset || 0 != (first_offset & 3))
return null;
int count = (int)(first_offset - 4) / 4;
if (!IsSaneCount (count))
return null;
var base_name = Path.GetFileNameWithoutExtension (file.Name);
uint index_offset = 0;
uint next_offset = first_offset;
var dir = new List<Entry> (count);
for (int i = 0; i < count && next_offset < file.MaxOffset; ++i)
{
var entry = new PackedEntry { Offset = next_offset };
index_offset += 4;
next_offset = file.View.ReadUInt32 (index_offset);
if (next_offset < entry.Offset)
return null;
entry.Size = (uint)(next_offset - entry.Offset);
entry.UnpackedSize = entry.Size;
if (entry.Size != 0)
{
if (!entry.CheckPlacement (file.MaxOffset))
return null;
entry.Name = string.Format ("{0}#{1:D4}", base_name, i);
dir.Add (entry);
}
}
if (0 == dir.Count)
return null;
DetectFileTypes (file, dir);
return new ArcFile (file, this, dir);
}
internal void DetectFileTypes (ArcView file, List<Entry> dir)
{
var header = new byte[16];
foreach (PackedEntry entry in dir)
{
if (entry.Size <= 8)
continue;
file.View.Read (entry.Offset, header, 0, 16);
if (header.AsciiEqual ("TPW"))
{
entry.IsPacked =header[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.ChangeType (ImageFormat.Bmp);
}
else if (header.AsciiEqual (4, "HDJ\0"))
{
if (header.AsciiEqual (12, "BM"))
entry.ChangeType (ImageFormat.Bmp);
else if (header.AsciiEqual (12, "MThd"))
entry.Name = Path.ChangeExtension (entry.Name, "mid");
entry.UnpackedSize = header.ToUInt32 (0);
entry.IsPacked = true;
}
else if (header.AsciiEqual (0, "zfd "))
{
entry.ChangeType (ImageFormat.Tga);
entry.UnpackedSize = header.ToUInt32 (4);
entry.IsPacked = true;
}
else if (header.AsciiEqual (4, "OggS"))
{
entry.ChangeType (OggAudio.Instance);
entry.Offset += 4;
entry.Size -= 4;
}
else if (entry.Size > 12 &&
(header.AsciiEqual (8, "RIFF") ||
((header[4] & 0xF) == 0xF && header.AsciiEqual (5, "RIFF"))))
{
entry.ChangeType (AudioFormat.Wav);
entry.UnpackedSize = header.ToUInt32 (0);
entry.IsPacked = true;
}
else
{
uint signature = header.ToUInt32 (0);
var res = AutoEntry.DetectFileType (signature);
if (res != null)
{
entry.ChangeType (res);
}
else if ((signature & 0xFFFF) == 0xFBFF)
{
entry.ChangeType (Mp3Format.Value);
}
else if (entry.Size > 0x16 && IsAudioEntry (file, entry))
{
entry.Type = "audio";
}
}
}
}
internal static ResourceInstance<AudioFormat> Mp3Format = new ResourceInstance<AudioFormat> ("MP3");
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)
{
var pent = entry as PackedEntry;
if (pent != null && pent.IsPacked && pent.Size > 8)
{
try
{
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 (arc.File.View.AsciiEqual (entry.Offset, "zfd "))
return OpenZfd (arc, pent);
if (entry.Size > 12)
{
byte type = arc.File.View.ReadByte (entry.Offset+4);
if ('W' == type
&& arc.File.View.AsciiEqual (entry.Offset+8, "RIFF"))
return OpenAudio (arc, entry);
if ((type & 0xF) == 0xF
&& arc.File.View.AsciiEqual (entry.Offset+5, "RIFF"))
{
var input = arc.File.CreateStream (entry.Offset+4, entry.Size-4);
return new LzssStream (input);
}
}
}
catch (Exception X)
{
System.Diagnostics.Trace.WriteLine (X.Message, "[GRP]");
}
}
return base.OpenEntry (arc, entry);
}
Stream OpenImage (ArcFile arc, PackedEntry entry)
{
using (var packed = arc.File.CreateStream (entry.Offset+8, entry.Size-8))
using (var reader = new GrpUnpacker (packed))
{
var unpacked = new byte[entry.UnpackedSize];
reader.UnpackHDJ (unpacked, 0);
return new BinMemoryStream (unpacked, entry.Name);
}
}
Stream OpenAudio (ArcFile arc, Entry entry)
{
int unpacked_size = arc.File.View.ReadInt32 (entry.Offset);
byte pack_type = arc.File.View.ReadByte (entry.Offset+5);
byte channels = arc.File.View.ReadByte (entry.Offset+6);
byte header_size = arc.File.View.ReadByte (entry.Offset+7);
if (unpacked_size <= 0 || header_size > unpacked_size
|| !('A' == pack_type || 'S' == pack_type))
return base.OpenEntry (arc, entry);
var unpacked = new byte[unpacked_size];
arc.File.View.Read (entry.Offset+8, unpacked, 0, header_size);
uint packed_size = entry.Size - 8 - header_size;
using (var packed = arc.File.CreateStream (entry.Offset+8+header_size, packed_size))
using (var reader = new GrpUnpacker (packed))
{
if ('A' == pack_type)
reader.UnpackA (unpacked, header_size, channels);
else
reader.UnpackS (unpacked, header_size, channels);
return new BinMemoryStream (unpacked, entry.Name);
}
}
Stream OpenTpw (ArcFile arc, PackedEntry entry)
{
using (var input = arc.File.CreateStream (entry.Offset, entry.Size))
{
var output = new byte[entry.UnpackedSize];
UnpackTpw (input, output);
return new BinMemoryStream (output, entry.Name);
}
}
Stream OpenZfd (ArcFile arc, PackedEntry entry)
{
var input = arc.File.CreateStream (entry.Offset+8, entry.Size-8);
return new ZLibStream (input, CompressionMode.Decompress);
}
internal static void UnpackTpw (IBinaryStream input, byte[] output)
{
input.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.ReadUInt8();
if (0 == ctl)
break;
int remaining = output.Length - dst;
int count;
if (ctl < 0x40)
{
count = Math.Min (ctl, remaining);
input.Read (output, dst, count);
dst += count;
}
else if (ctl <= 0x6F)
{
if (0x6F == ctl)
count = input.ReadUInt16();
else
count = (byte)(ctl - 0x3D);
count = Math.Min (count, remaining);
byte v = input.ReadUInt8();
while (count --> 0)
output[dst++] = v;
}
else if (ctl <= 0x9F)
{
if (ctl == 0x9F)
count = input.ReadUInt16();
else
count = (byte)(ctl - 0x6E);
dst += input.Read (output, dst, Math.Min (2, remaining));
--count;
if (count > 0 && remaining > 2)
{
count = Math.Min (count * 2, remaining - 2);
Binary.CopyOverlapped (output, dst-2, dst, count);
dst += count;
}
}
else if (ctl <= 0xBF)
{
if (ctl == 0xBF)
count = input.ReadUInt16();
else
count = (byte)(ctl - 0x9E);
dst += input.Read (output, dst, Math.Min (3, remaining));
--count;
if (count > 0 && remaining > 3)
{
count = Math.Min (count * 3, remaining - 3);
Binary.CopyOverlapped (output, dst-3, dst, count);
dst += count;
}
}
else
{
count = Math.Min ((ctl & 0x3F) + 3, remaining);
int offset = input.ReadUInt8();
offset = (offset & 0x3F) - offsets[offset >> 6];
Binary.CopyOverlapped (output, dst+offset, dst, count);
dst += count;
}
}
}
}
internal sealed class GrpUnpacker : IDisposable
{
IBinaryStream m_input;
uint m_bits;
int m_cached_bits;
public GrpUnpacker (IBinaryStream input)
{
m_input = input;
}
// Different games have slightly different formats using exact same headers,
// so have to invent some flawed format recognition here.
enum GrpVariant { Default, BoD };
static GrpVariant LastUsedMethod = GrpVariant.Default;
static GrpVariant GetOppositeVariant ()
{
return GrpVariant.Default == LastUsedMethod ? GrpVariant.BoD : GrpVariant.Default;
}
public void UnpackHDJ (byte[] output, int dst)
{
try
{
if (UnpackHDJVariant (output, dst, LastUsedMethod))
return;
}
catch { /* ignore unpack errors */ }
var method = GetOppositeVariant();
m_input.Position = 0;
if (!UnpackHDJVariant (output, dst, method))
throw new InvalidFormatException();
LastUsedMethod = method;
}
private bool UnpackHDJVariant (byte[] output, int dst, GrpVariant method)
{
ResetBits();
int word_count = 0;
int byte_count = 0;
uint next_byte = 0;
uint next_word = 0;
while (dst < output.Length)
{
if (GetNextBit() != 0)
{
int count = 0;
bool long_count = false;
int offset;
if (GetNextBit() != 0)
{
if (0 == word_count)
{
next_word = m_input.ReadUInt32();
word_count = 2;
}
count = (int)((next_word >> 13) & 7) + 3;
offset = (int)(next_word | 0xFFFFE000);
next_word >>= 16;
--word_count;
long_count = 10 == count;
}
else
{
if (method == GrpVariant.Default)
count = GetBits (2);
if (0 == byte_count)
{
next_byte = m_input.ReadUInt32();
byte_count = 4;
}
if (method != GrpVariant.Default)
count = GetBits (2);
count += 2;
long_count = 5 == count;
offset = (int)(next_byte | 0xFFFFFF00);
next_byte >>= 8;
--byte_count;
}
if (long_count)
{
int n = 0;
while (GetNextBit() != 0)
++n;
if (n != 0)
count += GetBits (n) + 1;
}
int src = dst + offset;
if (src < 0 || src >= dst || dst + count > output.Length)
return false;
Binary.CopyOverlapped (output, src, dst, count);
dst += count;
}
else
{
if (0 == byte_count)
{
next_byte = m_input.ReadUInt32();
byte_count = 4;
}
output[dst++] = (byte)next_byte;
next_byte >>= 8;
--byte_count;
}
}
return true;
}
public void UnpackS (byte[] output, int dst, int channels)
{
try
{
if (UnpackSVariant (output, dst, channels, LastUsedMethod))
return;
}
catch { /* ignore parse errors */ }
var method = GetOppositeVariant();
m_input.Position = 0;
if (UnpackSVariant (output, dst, channels, method))
LastUsedMethod = method;
}
bool UnpackSVariant (byte[] output, int dst, int channels, GrpVariant method)
{
if (GrpVariant.Default == method)
UnpackSv2 (output, dst, channels);
else
UnpackSv1 (output, dst, channels);
return m_input.PeekByte() == -1; // rather loose test, but whatever
}
void UnpackSv1 (byte[] output, int dst, int channels)
{
ResetBits();
short last_word = 0;
while (dst < output.Length)
{
int word;
if (GetNextBit() != 0)
{
if (GetNextBit() != 0)
word = GetBits (10) << 6;
else
word = 0;
}
else
{
int adjust = GetBits (5) << 6;
if (0 != (adjust & 0x400))
adjust = -(adjust & 0x3FF);
word = last_word + adjust;
}
last_word = (short)word;
LittleEndian.Pack (last_word, output, dst);
dst += 2;
}
}
void UnpackSv2 (byte[] output, int dst, int channels)
{
if (channels != 1)
m_input.Seek ((channels-1) * 4, SeekOrigin.Current);
int step = channels * 2;
for (int i = 0; i < channels; ++i)
{
ResetBits();
int pos = dst;
short last_word = 0;
while (pos < output.Length)
{
int word;
if (GetNextBit() != 0)
{
if (GetNextBit() != 0)
{
word = GetBits (10) << 6;
}
else
{
int repeat;
if (GetNextBit() != 0)
{
int bit_length = 0;
do
{
++bit_length;
}
while (GetNextBit() != 0);
repeat = GetBits (bit_length) + 4;
}
else
{
repeat = GetBits (2);
}
word = 0;
while (repeat --> 0)
{
output[pos] = 0;
output[pos+1] = 0;
pos += step;
}
}
}
else
{
int adjust = (short)(GetBits (5) << 11) >> 5;
word = last_word + adjust;
}
LittleEndian.Pack ((short)word, output, pos);
last_word = (short)word;
pos += step;
}
dst += 2;
}
}
public void UnpackA (byte[] output, int dst, int channels)
{
if (channels != 1)
m_input.Seek ((channels-1) * 4, SeekOrigin.Current);
int step = 2 * channels;
for (int i = 0; i < channels; ++i)
{
int pos = dst;
ResetBits();
while (pos < output.Length)
{
int word = GetBits (10) << 6;;
LittleEndian.Pack ((short)word, output, pos);
pos += step;
}
dst += 2;
}
}
void ResetBits ()
{
m_cached_bits = 0;
}
int GetNextBit ()
{
return GetBits (1);
}
int GetBits (int count)
{
if (0 == m_cached_bits)
{
m_bits = m_input.ReadUInt32();
m_cached_bits = 32;
}
uint val;
if (m_cached_bits < count)
{
uint next_bits = m_input.ReadUInt32();
val = (m_bits | (next_bits >> m_cached_bits)) >> (32 - count);
m_bits = next_bits << (count - m_cached_bits);
m_cached_bits = 32 - (count - m_cached_bits);
}
else
{
val = m_bits >> (32 - count);
m_bits <<= count;
m_cached_bits -= count;
}
return (int)val;
}
#region IDisposable Members
bool _disposed = false;
public void Dispose ()
{
if (!_disposed)
{
m_input.Dispose();
_disposed = true;
}
}
#endregion
}
}