//! \file ArcPSB.cs
//! \date Thu Mar 24 01:40:57 2016
//! \brief E-mote engine image container.
//
// 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;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.IO;
using System.Text;
using System.Windows.Media;
using GameRes.Utility;
namespace GameRes.Formats.Emote
{
internal class TexEntry : Entry
{
public string TexType;
public int Width;
public int Height;
public int TruncatedWidth;
public int TruncatedHeight;
}
[Serializable]
public class PsbScheme : ResourceScheme
{
public uint[] KnownKeys;
}
[Export(typeof(ArchiveFormat))]
public class PsbOpener : ArchiveFormat
{
public override string Tag { get { return "PSB/EMOTE"; } }
public override string Description { get { return "E-mote engine texture container"; } }
public override uint Signature { get { return 0x425350; } } // 'PSB'
public override bool IsHierarchic { get { return false; } }
public override bool CanWrite { get { return false; } }
static uint[] KnownKeys = new uint[] { 970396437u };
public PsbOpener ()
{
Extensions = new string[] { "psb" };
}
public override ArcFile TryOpen (ArcView file)
{
using (var input = file.CreateStream())
using (var reader = new PsbReader (input))
{
foreach (var key in KnownKeys)
{
if (reader.Parse (key))
{
var dir = reader.GetTextures();
if (null == dir || 0 == dir.Count)
return null;
else
return new ArcFile (file, this, dir);
}
if (!reader.IsEncrypted)
break;
}
return null;
}
}
public override Stream OpenEntry (ArcFile arc, Entry entry)
{
var tex = entry as TexEntry;
if (null == tex)
return base.OpenEntry (arc, entry);
byte[] header;
using (var mem = new MemoryStream())
using (var writer = new BinaryWriter (mem))
{
writer.Write ((uint)0x81C3D2D1); // 'PSB' ^ 0x81818181
writer.Write ((int)0);
writer.Write (tex.Width);
writer.Write (tex.Height);
writer.Write (tex.TruncatedWidth);
writer.Write (tex.TruncatedHeight);
writer.Write (tex.TexType);
writer.BaseStream.Position = 4;
writer.Write ((int)writer.BaseStream.Length);
header = mem.ToArray();
}
var input = arc.File.CreateStream (entry.Offset, entry.Size);
return new PrefixStream (header, input);
}
public override ResourceScheme Scheme
{
get { return new PsbScheme { KnownKeys = KnownKeys }; }
set { KnownKeys = ((PsbScheme)value).KnownKeys; }
}
}
///
/// PSB container deserialization.
///
internal sealed class PsbReader : IDisposable
{
IBinaryStream m_input;
public PsbReader (IBinaryStream input)
{
m_input = input;
}
public int Version { get { return m_version; } }
public bool IsEncrypted { get { return 0 != (m_flags & 3); } }
public int DataOffset { get { return m_chunk_data; } }
public T GetRootKey (string key)
{
int obj_offset;
if (!GetKey (key, m_root, out obj_offset))
return default(T);
return (T)GetObject (obj_offset);
}
int m_version;
int m_flags;
uint[] m_key = new uint[6];
Dictionary m_name_map;
public bool Parse (uint key)
{
m_key[0] = 0x075BCD15;
m_key[1] = 0x159A55E5;
m_key[2] = 0x1F123BB5;
m_key[3] = key;
m_key[4] = 0;
m_key[5] = 0;
if (!ReadHeader())
return false;
if (Version < 2)
throw new NotSupportedException ("Not supported PSB version");
m_name_map = ReadNames();
#if DEBUG
var dict = GetDict (m_root); // returns all metadata in a single dictionary
#endif
return true;
}
public List GetTextures ()
{
var source = GetRootKey ("source");
if (null == source || 0 == source.Count)
return null;
var dir = new List (source.Count);
foreach (DictionaryEntry item in source)
{
var item_value = item.Value as IDictionary;
if (null == item_value)
continue;
var texture = item_value["texture"] as IDictionary;
if (null == texture)
continue;
var pixel = texture["pixel"] as EmChunk;
if (null == pixel)
continue;
var entry = new TexEntry {
Name = item.Key.ToString(),
Type = "image",
Offset = DataOffset + pixel.Offset,
Size = (uint)pixel.Length,
TexType = texture["type"].ToString(),
Width = Convert.ToInt32 (texture["width"]),
Height = Convert.ToInt32 (texture["height"]),
TruncatedWidth = Convert.ToInt32 (texture["truncated_width"]),
TruncatedHeight = Convert.ToInt32 (texture["truncated_height"]),
};
dir.Add (entry);
}
return dir;
}
int m_names;
int m_strings;
int m_strings_data;
int m_chunk_offsets;
int m_chunk_lengths;
int m_chunk_data;
int m_root;
byte[] m_data;
bool ReadHeader ()
{
m_input.Position = 4;
m_version = m_input.ReadUInt16();
m_flags = m_input.ReadUInt16();
if (m_version < 3)
m_flags = 2;
var header = new byte[0x20];
m_input.Read (header, 0, header.Length);
if (0 != (m_flags & 1))
Decrypt (header, 0, 0x20);
m_names = LittleEndian.ToInt32 (header, 0x04);
m_strings = LittleEndian.ToInt32 (header, 0x08);
m_strings_data = LittleEndian.ToInt32 (header, 0x0C);
m_chunk_offsets = LittleEndian.ToInt32 (header, 0x10);
m_chunk_lengths = LittleEndian.ToInt32 (header, 0x14);
m_chunk_data = LittleEndian.ToInt32 (header, 0x18);
m_root = LittleEndian.ToInt32 (header, 0x1C);
int buffer_length = (int)m_input.Length;
if (!(m_names >= 0x28 && m_names < m_chunk_data
&& m_strings >= 0x28 && m_strings < m_chunk_data
&& m_strings_data >= 0x28 && m_strings_data < m_chunk_data
&& m_chunk_offsets >= 0x28 && m_chunk_offsets < m_chunk_data
&& m_chunk_lengths >= 0x28 && m_chunk_lengths < m_chunk_data
&& m_chunk_data >= 0x28 && m_chunk_data < buffer_length
&& m_root >= 0x28 && m_root < m_chunk_data))
return false;
if (null == m_data || m_data.Length < m_chunk_data)
m_data = new byte[m_chunk_data];
int data_pos = (int)m_input.Position;
m_input.Read (m_data, data_pos, m_chunk_data-data_pos);
if (0 != (m_flags & 2))
Decrypt (m_data, m_names, m_chunk_offsets-m_names);
// root object is a dictionary
return 0x21 == m_data[m_root];
}
bool GetKey (string name, int dict_offset, out int value_offset)
{
value_offset = 0;
int offset;
if (!GetOffset (name, out offset))
return false;
var keys = GetArray (++dict_offset);
if (0 == keys.Count)
return false;
int upper_bound = keys.Count;
int lower_bound = 0;
int key_index = 0;
while (lower_bound < upper_bound)
{
key_index = (upper_bound + lower_bound) >> 1;
int key = GetArrayElem (keys, key_index);
if (key == offset)
break;
if (key >= offset)
upper_bound = (upper_bound + lower_bound) >> 1;
else
lower_bound = key_index + 1;
}
if (lower_bound >= upper_bound)
return false;
var values = GetArray (dict_offset + keys.ArraySize);
int data_offset = GetArrayElem (values, key_index);
value_offset = dict_offset + keys.ArraySize + values.ArraySize + data_offset;
return true;
}
bool GetOffset (string name, out int offset)
{
// FIXME works for ASCII names only.
var nm1 = GetArray (m_names);
var nm2 = GetArray (m_names + nm1.ArraySize);
int i = 0;
for (int name_idx = 0; ; ++name_idx)
{
char symbol = name_idx < name.Length ? name[name_idx] : '\0';
int prev_i = i;
i = symbol + GetArrayElem (nm1, i);
if (i >= nm1.Count || GetArrayElem (nm2, i) != prev_i)
break;
if (name_idx >= name.Length)
{
offset = GetArrayElem (nm1, i);
return true;
}
}
offset = 0;
return false;
}
Dictionary ReadNames ()
{
// this implementation is utterly inefficient. FIXME
var lookup = new Dictionary();
var next_lookup = new Dictionary();
var dict = new Dictionary();
var nm1 = GetArray (m_names);
var nm2 = GetArray (m_names + nm1.ArraySize);
lookup[0] = new byte[0];
while (lookup.Count > 0)
{
foreach (var item in lookup)
{
int first = GetArrayElem (nm1, item.Key);
for (int i = 0; i < 256; ++i)
{
if (GetArrayElem (nm2, i + first) == item.Key)
{
if (0 == i)
dict[GetArrayElem (nm1, i + first)] = Encoding.UTF8.GetString (item.Value);
else
next_lookup[i+first] = ArrayAppend (item.Value, (byte)i);
}
}
}
var tmp = lookup;
lookup = next_lookup;
next_lookup = tmp;
next_lookup.Clear();
}
return dict;
}
static byte[] ArrayAppend (byte[] array, byte n)
{
var new_array = new byte[array.Length+1];
Buffer.BlockCopy (array, 0, new_array, 0, array.Length);
new_array[array.Length] = n;
return new_array;
}
EmArray GetArray (int offset)
{
int data_offset = m_data[offset] - 10;
var array = new EmArray {
Count = GetInteger (offset, 0xC),
ElemSize = m_data[offset + data_offset - 1] - 12,
DataOffset = offset + data_offset,
};
array.ArraySize = array.Count * array.ElemSize + data_offset;
return array;
}
int GetArrayElem (EmArray a1, int index)
{
int offset = index * a1.ElemSize;
switch (a1.ElemSize)
{
case 1:
return m_data[a1.DataOffset + offset];
case 2:
return LittleEndian.ToUInt16 (m_data, a1.DataOffset + offset);
case 3:
return LittleEndian.ToUInt16 (m_data, a1.DataOffset + offset) | m_data[a1.DataOffset + offset + 2] << 16;
case 4:
return LittleEndian.ToInt32 (m_data, a1.DataOffset + offset);
default:
throw new InvalidFormatException ("Invalid PSB array structure");
}
}
object GetObject (int offset)
{
switch (m_data[offset])
{
case 1: return null;
case 2: return true;
case 3: return false;
case 4:
case 5:
case 6:
case 7:
case 8: return GetInteger (offset, 4);
case 9:
case 0x0A:
case 0x0B:
case 0x0C: return GetLong (offset);
case 0x15:
case 0x16:
case 0x17:
case 0x18: return GetString (offset);
case 0x19:
case 0x1A:
case 0x1B:
case 0x1C: return GetChunk (offset);
case 0x1D:
case 0x1E: return GetFloat (offset);
case 0x1F: return GetDouble (offset);
case 0x20: return GetList (offset);
case 0x21: return GetDict (offset);
default:
throw new InvalidFormatException (string.Format ("Unknown serialized object type 0x{0:X2}", m_data[offset]));
}
}
int GetInteger (int offset, int base_type)
{
switch (m_data[offset] - base_type)
{
case 1: return m_data[offset+1];
case 2: return LittleEndian.ToUInt16 (m_data, offset+1);
case 3: return LittleEndian.ToUInt16 (m_data, offset+1) | m_data[offset+3] << 16;
case 4: return LittleEndian.ToInt32 (m_data, offset+1);
default: return 0;
}
}
float GetFloat (int offset)
{
if (0x1E == m_data[offset])
return BitConverter.ToSingle (m_data, offset+1); // FIXME endianness
else
return 0.0f;
}
double GetDouble (int offset)
{
if (0x1F == m_data[offset])
return BitConverter.ToDouble (m_data, offset+1); // FIXME endianness
else
return 0.0;
}
long GetLong (int offset)
{
switch (m_data[offset])
{
case 0x09: return LittleEndian.ToUInt32 (m_data, offset+1) | (long)(sbyte)m_data[offset+5] << 32;
case 0x0A: return LittleEndian.ToUInt32 (m_data, offset+1)
| (long)LittleEndian.ToInt16 (m_data, offset+5) << 32;
case 0x0B: return LittleEndian.ToUInt32 (m_data, offset+1)
| (long)LittleEndian.ToUInt16 (m_data, offset+5) << 32
| (long)(sbyte)m_data[offset+6] << 48;
case 0x0C: return LittleEndian.ToInt64 (m_data, offset+1);
default: return 0L;
}
}
string GetString (int obj_offset)
{
int index = GetInteger (obj_offset, 0x14);
var array = GetArray (m_strings);
int data_offset = m_strings_data + GetArrayElem (array, index);
return Binary.GetCString (m_data, data_offset, m_data.Length-data_offset, Encoding.UTF8);
}
IList GetList (int offset)
{
var array = GetArray (++offset);
var list = new ArrayList (array.Count);
for (int i = 0; i < array.Count; ++i)
{
int item_offset = offset + array.ArraySize + GetArrayElem (array, i);
var item = GetObject (item_offset);
list.Add (item);
}
return list;
}
IDictionary GetDict (int offset)
{
var keys = GetArray (++offset);
if (0 == keys.Count)
return new Dictionary();
var values = GetArray (offset + keys.ArraySize);
var dict = new Dictionary (keys.Count);
for (int i = 0; i < keys.Count; ++i)
{
int key = GetArrayElem (keys, i);
var value_offset = GetArrayElem (values, i);
string key_name = m_name_map[key];
dict[key_name] = GetObject (offset + value_offset + keys.ArraySize + values.ArraySize);
}
return dict;
}
EmChunk GetChunk (int offset)
{
var chunk_index = GetInteger (offset, 0x18);
var chunks = GetArray (m_chunk_offsets);
if (chunk_index >= chunks.Count)
throw new InvalidFormatException ("Invalid chunk index");
var lengths = GetArray (m_chunk_lengths);
return new EmChunk {
Offset = GetArrayElem (chunks, chunk_index),
Length = GetArrayElem (lengths, chunk_index),
};
}
void Decrypt (byte[] data, int offset, int length)
{
for (int i = 0; i < length; ++i)
{
if (0 == m_key[4])
{
var v5 = m_key[3];
var v6 = m_key[0] ^ (m_key[0] << 11);
m_key[0] = m_key[1];
m_key[1] = m_key[2];
var eax = v6 ^ v5 ^ ((v6 ^ (v5 >> 11)) >> 8);
m_key[2] = v5;
m_key[3] = eax;
m_key[4] = eax;
}
data[offset+i] ^= (byte)m_key[4];
m_key[4] >>= 8;
}
}
internal class EmArray
{
public int ArraySize;
public int Count;
public int ElemSize;
public int DataOffset;
}
internal class EmChunk
{
public int Offset;
public int Length;
}
#region IDisposable Members
public void Dispose ()
{
}
#endregion
}
internal class PsbTexMetaData : ImageMetaData
{
public string TexType;
public int FullWidth;
public int FullHeight;
public int DataOffset;
}
///
/// Artificial format representing PSB texture.
///
[Export(typeof(ImageFormat))]
internal class PsbTextureFormat : ImageFormat
{
public override string Tag { get { return "PSB/TEXTURE"; } }
public override string Description { get { return "PSB texture format"; } }
public override uint Signature { get { return 0x81C3D2D1; } } // 'PSB' ^ 0x81818181
public PsbTextureFormat ()
{
Extensions = new string[0];
}
public override ImageMetaData ReadMetaData (IBinaryStream stream)
{
stream.Position = 4;
// need BinaryReader because of ReadString
using (var reader = new BinaryReader (stream.AsStream, Encoding.UTF8, true))
{
var info = new PsbTexMetaData { BPP = 32 };
info.DataOffset = reader.ReadInt32();
info.FullWidth = reader.ReadInt32();
info.FullHeight = reader.ReadInt32();
info.Width = reader.ReadUInt32();
info.Height = reader.ReadUInt32();
info.TexType = reader.ReadString();
return info;
}
}
public override ImageData Read (IBinaryStream stream, ImageMetaData info)
{
var meta = (PsbTexMetaData)info;
var pixels = new byte[meta.Width * meta.Height * 4];
if ("RGBA8" == meta.TexType)
ReadRgba8 (stream.AsStream, meta, pixels);
else if ("RGBA4444" == meta.TexType)
ReadRgba4444 (stream.AsStream, meta, pixels);
else
throw new NotImplementedException (string.Format ("PSB texture format '{0}' not implemented", meta.TexType));
return ImageData.Create (info, PixelFormats.Bgra32, null, pixels);
}
void ReadRgba8 (Stream input, PsbTexMetaData meta, byte[] output)
{
int dst_stride = (int)meta.Width * 4;
long next_row = meta.DataOffset;
int src_stride = meta.FullWidth * 4;
int dst = 0;
for (uint i = 0; i < meta.Height; ++i)
{
input.Position = next_row;
input.Read (output, dst, dst_stride);
dst += dst_stride;
next_row += src_stride;
}
}
void ReadRgba4444 (Stream input, PsbTexMetaData meta, byte[] output)
{
int dst_stride = (int)meta.Width * 4;
int src_stride = meta.FullWidth * 2;
int dst = 0;
var row = new byte[src_stride];
input.Position = meta.DataOffset;
for (uint i = 0; i < meta.Height; ++i)
{
input.Read (row, 0, src_stride);
int src = 0;
for (int x = 0; x < dst_stride; x += 4)
{
uint p = LittleEndian.ToUInt16 (row, src);
src += 2;
output[dst++] = (byte)((p & 0x000Fu) * 0xFFu / 0x000Fu);
output[dst++] = (byte)((p & 0x00F0u) * 0xFFu / 0x00F0u);
output[dst++] = (byte)((p & 0x0F00u) * 0xFFu / 0x0F00u);
output[dst++] = (byte)((p & 0xF000u) * 0xFFu / 0xF000u);
}
}
}
public override void Write (Stream file, ImageData image)
{
throw new NotSupportedException ("PsbTextureFormat.Write not supported");
}
}
}