435 lines
15 KiB
C#
435 lines
15 KiB
C#
using CriWareFormats.Common;
|
|
using System;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Text;
|
|
|
|
namespace CriWareFormats
|
|
{
|
|
[Flags]
|
|
public enum ColumnFlag : byte
|
|
{
|
|
Name = 0x10,
|
|
Default = 0x20,
|
|
Row = 0x40,
|
|
Undefined = 0x80
|
|
}
|
|
|
|
public enum ColumnType : byte
|
|
{
|
|
Byte = 0x00,
|
|
SByte = 0x01,
|
|
UInt16 = 0x02,
|
|
Int16 = 0x03,
|
|
UInt32 = 0x04,
|
|
Int32 = 0x05,
|
|
UInt64 = 0x06,
|
|
Int64 = 0x07,
|
|
Float = 0x08,
|
|
Double = 0x09,
|
|
String = 0x0A,
|
|
VLData = 0x0B,
|
|
UInt128 = 0x0C,
|
|
Undefined = 0xFF
|
|
}
|
|
|
|
public struct Column
|
|
{
|
|
public ColumnFlag Flag;
|
|
public ColumnType Type;
|
|
public string Name;
|
|
public uint Offset;
|
|
|
|
public override string ToString()
|
|
{
|
|
return Name;
|
|
}
|
|
}
|
|
|
|
public struct VLData
|
|
{
|
|
public uint Offset;
|
|
public uint Size;
|
|
|
|
public override string ToString()
|
|
{
|
|
return $"Offset: {Offset}, Length: {Size}";
|
|
}
|
|
}
|
|
|
|
public struct Result
|
|
{
|
|
public ColumnType Type;
|
|
public object Value;
|
|
}
|
|
|
|
public sealed class UtfTable
|
|
{
|
|
private readonly BinaryReaderEndian binaryReader;
|
|
private readonly uint tableOffset;
|
|
|
|
private readonly uint tableSize;
|
|
private readonly ushort version;
|
|
private readonly ushort rowsOffset;
|
|
private readonly uint stringsOffset;
|
|
private readonly uint dataOffset;
|
|
private readonly uint nameOffset;
|
|
|
|
private readonly ushort columns;
|
|
private readonly ushort rowWidth;
|
|
private readonly uint rows;
|
|
|
|
private readonly byte[] schemaBuffer;
|
|
private readonly Column[] schema;
|
|
|
|
private readonly uint schemaOffset;
|
|
private readonly uint schemaSize;
|
|
private readonly uint rowsSize;
|
|
private readonly uint dataSize;
|
|
private readonly uint stringsSize;
|
|
|
|
private readonly byte[] stringTable;
|
|
private readonly string tableName;
|
|
|
|
public UtfTable(Stream utfTableStream, out int utfTableRows, out string utfTableRowName) :
|
|
this(utfTableStream, 0, out utfTableRows, out utfTableRowName)
|
|
{ }
|
|
|
|
public UtfTable(Stream utfTableStream, uint offset) :
|
|
this(utfTableStream, offset, out int _, out string _)
|
|
{ }
|
|
|
|
public UtfTable(Stream utfTableStream, uint offset, out int utfTableRows, out string rowName)
|
|
{
|
|
binaryReader = new BinaryReaderEndian(utfTableStream);
|
|
tableOffset = offset;
|
|
|
|
binaryReader.BaseStream.Position = offset;
|
|
|
|
if (!binaryReader.ReadChars(4).SequenceEqual("@UTF".ToCharArray()))
|
|
throw new InvalidDataException("Incorrect magic.");
|
|
tableSize = binaryReader.ReadUInt32BE() + 0x08;
|
|
version = binaryReader.ReadUInt16BE();
|
|
rowsOffset = (ushort)(binaryReader.ReadUInt16BE() + 0x08);
|
|
stringsOffset = binaryReader.ReadUInt32BE() + 0x08;
|
|
dataOffset = binaryReader.ReadUInt32BE() + 0x08;
|
|
nameOffset = binaryReader.ReadUInt32BE();
|
|
columns = binaryReader.ReadUInt16BE();
|
|
rowWidth = binaryReader.ReadUInt16BE();
|
|
rows = binaryReader.ReadUInt32BE();
|
|
|
|
schemaOffset = 0x20;
|
|
schemaSize = rowsOffset - schemaOffset;
|
|
rowsSize = stringsOffset - rowsOffset;
|
|
stringsSize = dataOffset - stringsOffset;
|
|
dataSize = tableSize - dataOffset;
|
|
|
|
if (version != 0x00 && version != 0x01)
|
|
throw new InvalidDataException("Unknown @UTF version.");
|
|
if (tableOffset + tableSize > utfTableStream.Length)
|
|
throw new InvalidDataException("Table size exceeds bounds of file.");
|
|
if (rowsOffset > tableSize || stringsOffset > tableSize || dataOffset > tableSize)
|
|
throw new InvalidDataException("Offsets out of bounds.");
|
|
if (stringsSize <= 0 || nameOffset > stringsSize)
|
|
throw new InvalidDataException("Invalid string table size.");
|
|
if (Columns <= 0)
|
|
throw new InvalidDataException("Table has no columns.");
|
|
|
|
schemaBuffer = new byte[schemaSize];
|
|
binaryReader.BaseStream.Position = tableOffset + schemaOffset;
|
|
if (binaryReader.Read(schemaBuffer, 0, (int)schemaSize) != schemaSize)
|
|
throw new InvalidDataException("Failed to read schema.");
|
|
|
|
stringTable = new byte[stringsSize];
|
|
binaryReader.BaseStream.Position = tableOffset + stringsOffset;
|
|
if (binaryReader.Read(stringTable, 0, (int)stringsSize) != stringsSize)
|
|
throw new InvalidDataException("Failed to read string table.");
|
|
|
|
uint columnOffset = 0;
|
|
uint schemaPos = 0;
|
|
|
|
tableName = GetStringFromTable(nameOffset);
|
|
|
|
schema = new Column[columns];
|
|
|
|
BinaryReaderEndian bytesReader = new BinaryReaderEndian(new MemoryStream(schemaBuffer) { Position = 0 });
|
|
for (int i = 0; i < columns; i++)
|
|
{
|
|
bytesReader.BaseStream.Position = schemaPos;
|
|
|
|
byte info = bytesReader.ReadByte();
|
|
uint nameOffset = bytesReader.ReadUInt32BE();
|
|
|
|
if (nameOffset > stringsSize)
|
|
throw new InvalidDataException("String offset out of bounds.");
|
|
schemaPos += 0x1 + 0x4;
|
|
|
|
bytesReader.BaseStream.Position = schemaPos;
|
|
|
|
schema[i] = new Column()
|
|
{
|
|
Flag = (ColumnFlag)(info & 0xF0),
|
|
Type = (ColumnType)(info & 0x0F),
|
|
Name = "",
|
|
Offset = 0
|
|
};
|
|
|
|
if (schema[i].Flag == 0 ||
|
|
!schema[i].Flag.HasFlag(ColumnFlag.Name) ||
|
|
schema[i].Flag.HasFlag(ColumnFlag.Default) && schema[i].Flag.HasFlag(ColumnFlag.Row) ||
|
|
schema[i].Flag.HasFlag(ColumnFlag.Undefined))
|
|
throw new InvalidDataException("Unknown column flag combo found.");
|
|
|
|
uint valueSize;
|
|
switch (schema[i].Type)
|
|
{
|
|
case ColumnType.Byte:
|
|
case ColumnType.SByte:
|
|
valueSize = 0x1;
|
|
break;
|
|
case ColumnType.UInt16:
|
|
case ColumnType.Int16:
|
|
valueSize = 0x2;
|
|
break;
|
|
case ColumnType.UInt32:
|
|
case ColumnType.Int32:
|
|
case ColumnType.Float:
|
|
case ColumnType.String:
|
|
valueSize = 0x4;
|
|
break;
|
|
case ColumnType.UInt64:
|
|
case ColumnType.Int64:
|
|
case ColumnType.VLData:
|
|
valueSize = 0x8;
|
|
break;
|
|
default:
|
|
throw new InvalidDataException("Invalid column type.");
|
|
};
|
|
|
|
if (schema[i].Flag.HasFlag(ColumnFlag.Name))
|
|
schema[i].Name = GetStringFromTable(nameOffset);
|
|
|
|
if (schema[i].Flag.HasFlag(ColumnFlag.Default))
|
|
{
|
|
schema[i].Offset = schemaPos;
|
|
schemaPos += valueSize;
|
|
|
|
bytesReader.BaseStream.Position = schemaPos;
|
|
|
|
}
|
|
|
|
if (schema[i].Flag.HasFlag(ColumnFlag.Row))
|
|
{
|
|
schema[i].Offset = columnOffset;
|
|
columnOffset += valueSize;
|
|
}
|
|
}
|
|
|
|
utfTableRows = (int)rows;
|
|
rowName = GetStringFromTable(nameOffset);
|
|
|
|
bytesReader.Dispose();
|
|
}
|
|
|
|
public ushort Columns => columns;
|
|
|
|
public uint Rows => rows;
|
|
|
|
public Column[] Schema => schema;
|
|
|
|
public string TableName => tableName;
|
|
|
|
public int GetColumn(string columnName)
|
|
{
|
|
for (int i = 0; i < columns; i++)
|
|
{
|
|
Column column = schema[i];
|
|
|
|
if (column.Name is null || !column.Name.Equals(columnName))
|
|
continue;
|
|
|
|
return i;
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
private bool Query(int row, int column, out Result result)
|
|
{
|
|
result = new Result();
|
|
|
|
if (row >= rows || row < 0)
|
|
//throw new ArgumentOutOfRangeException(nameof(row));
|
|
return false;
|
|
if (column >= columns || column < 0)
|
|
//throw new ArgumentOutOfRangeException(nameof(column));
|
|
return false;
|
|
|
|
Column col = schema[column];
|
|
uint dataOffset = 0;
|
|
BinaryReaderEndian bytesReader = null;
|
|
|
|
result.Type = col.Type;
|
|
|
|
if (col.Flag.HasFlag(ColumnFlag.Default))
|
|
{
|
|
if (schemaBuffer != null)
|
|
{
|
|
bytesReader = new BinaryReaderEndian(new MemoryStream(schemaBuffer));
|
|
bytesReader.BaseStream.Position = col.Offset;
|
|
}
|
|
else
|
|
{
|
|
dataOffset = tableOffset + schemaOffset + col.Offset;
|
|
}
|
|
}
|
|
else if (col.Flag.HasFlag(ColumnFlag.Row))
|
|
{
|
|
dataOffset = (uint)(tableOffset + rowsOffset + row * rowWidth + col.Offset);
|
|
}
|
|
else
|
|
throw new InvalidDataException("Invalid flag.");
|
|
|
|
binaryReader.BaseStream.Position = dataOffset;
|
|
|
|
switch (col.Type)
|
|
{
|
|
case ColumnType.Byte:
|
|
result.Value = (bytesReader != null) ? bytesReader.ReadByte() : binaryReader.ReadByte();
|
|
break;
|
|
case ColumnType.SByte:
|
|
result.Value = (bytesReader != null) ? bytesReader.ReadSByte() : binaryReader.ReadSByte();
|
|
break;
|
|
case ColumnType.UInt16:
|
|
result.Value = (bytesReader != null) ? bytesReader.ReadUInt16BE() : binaryReader.ReadUInt16BE();
|
|
break;
|
|
case ColumnType.Int16:
|
|
result.Value = (bytesReader != null) ? bytesReader.ReadInt16BE() : binaryReader.ReadInt16BE();
|
|
break;
|
|
case ColumnType.UInt32:
|
|
result.Value = (bytesReader != null) ? bytesReader.ReadUInt32BE() : binaryReader.ReadUInt32BE();
|
|
break;
|
|
case ColumnType.Int32:
|
|
result.Value = (bytesReader != null) ? bytesReader.ReadInt32BE() : binaryReader.ReadInt32BE();
|
|
break;
|
|
case ColumnType.UInt64:
|
|
result.Value = (bytesReader != null) ? bytesReader.ReadUInt64BE() : binaryReader.ReadUInt64BE();
|
|
break;
|
|
case ColumnType.Int64:
|
|
result.Value = (bytesReader != null) ? bytesReader.ReadInt64BE() : binaryReader.ReadInt64BE();
|
|
break;
|
|
case ColumnType.Float:
|
|
result.Value = (bytesReader != null) ? bytesReader.ReadSingleBE() : binaryReader.ReadSingleBE();
|
|
break;
|
|
//case ColumnType.Double:
|
|
// break;
|
|
case ColumnType.String:
|
|
uint nameOffset = (bytesReader != null) ? bytesReader.ReadUInt32BE() : binaryReader.ReadUInt32BE();
|
|
if (nameOffset > stringsSize)
|
|
throw new InvalidDataException("Name offset out of bounds.");
|
|
result.Value = GetStringFromTable(nameOffset);
|
|
break;
|
|
case ColumnType.VLData:
|
|
if ((bytesReader != null))
|
|
{
|
|
result.Value = new VLData()
|
|
{
|
|
Offset = bytesReader.ReadUInt32BE(),
|
|
Size = bytesReader.ReadUInt32BE()
|
|
};
|
|
}
|
|
else
|
|
{
|
|
result.Value = new VLData()
|
|
{
|
|
Offset = binaryReader.ReadUInt32BE(),
|
|
Size = binaryReader.ReadUInt32BE()
|
|
};
|
|
}
|
|
break;
|
|
//case ColumnType.UInt128:
|
|
// break;
|
|
default:
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public bool Query<T>(int row, int column, out T value)
|
|
{
|
|
bool valid = Query(row, column, out Result result);
|
|
|
|
bool enumParseResult = Enum.TryParse<ColumnType>(typeof(T).Name, out ColumnType type);
|
|
|
|
if (!valid || !enumParseResult || result.Type != type)
|
|
{
|
|
value = default;
|
|
return false;
|
|
}
|
|
|
|
value = (T)result.Value;
|
|
|
|
if (value is VLData vlData)
|
|
{
|
|
vlData.Offset += tableOffset + dataOffset;
|
|
value = (T)(object)vlData;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public bool Query<T>(int row, string columnName, out T value) =>
|
|
Query(row, GetColumn(columnName), out value);
|
|
|
|
public bool Query(int row, int column, out uint offset, out uint size)
|
|
{
|
|
if (!Query(row, column, out VLData data))
|
|
{
|
|
offset = 0;
|
|
size = 0;
|
|
|
|
return false;
|
|
}
|
|
|
|
offset = data.Offset;
|
|
size = data.Size;
|
|
|
|
return true;
|
|
}
|
|
|
|
public bool Query(int row, string columnName, out uint offset, out uint size)
|
|
=> Query(row, GetColumn(columnName), out offset, out size);
|
|
|
|
private string GetStringFromTable(uint offset)
|
|
{
|
|
if (offset > stringsSize)
|
|
throw new InvalidDataException("Invalid string offset.");
|
|
|
|
StringBuilder stringBuilder = new StringBuilder();
|
|
|
|
int i = 0;
|
|
while (i < stringsSize)
|
|
{
|
|
stringBuilder.Append((char)stringTable[offset + i++]);
|
|
if (stringTable[offset + i] == '\0') break;
|
|
}
|
|
|
|
return stringBuilder.ToString();
|
|
}
|
|
|
|
public UtfTable OpenSubtable(string tableName)
|
|
{
|
|
if (!Query(0, tableName, out VLData tableValueData))
|
|
throw new ArgumentException("Subtable does not exist.");
|
|
|
|
binaryReader.BaseStream.Position = tableValueData.Offset;
|
|
|
|
return new UtfTable(
|
|
binaryReader.BaseStream,
|
|
tableValueData.Offset,
|
|
out int _,
|
|
out string _);
|
|
}
|
|
}
|
|
} |