diff --git a/Yura.Shared/Archive/ArchiveFile.cs b/Yura.Shared/Archive/ArchiveFile.cs
new file mode 100644
index 0000000..5de206d
--- /dev/null
+++ b/Yura.Shared/Archive/ArchiveFile.cs
@@ -0,0 +1,95 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+
+namespace Yura.Shared.Archive
+{
+ public abstract class ArchiveFile
+ {
+ protected ArchiveOptions Options { get; }
+
+ public ArchiveFile(ArchiveOptions options)
+ {
+ ArgumentNullException.ThrowIfNull(options);
+
+ Options = options;
+ Records = [];
+ }
+
+ ///
+ /// Gets the records in the archive
+ ///
+ public List Records { get; }
+
+ ///
+ /// Gets the path of the archive
+ ///
+ public string Name => Options.Path;
+
+ ///
+ /// Opens the archive
+ ///
+ public abstract void Open();
+
+ ///
+ /// Reads a file from the archive
+ ///
+ /// The record to read
+ /// The contents of the file
+ public abstract byte[] Read(ArchiveRecord record);
+
+ ///
+ /// Gets all files in the specified directory
+ ///
+ /// The path to the directory
+ /// The contents of the directory
+ public List GetFiles(string path)
+ {
+ var hierachy = Split(path);
+
+ // Find all records where the path is equal, removing the file name
+ return Records.Where(record => record.Name != null && Split(record.Name).SkipLast(1).SequenceEqual(hierachy)).ToList();
+ }
+
+ ///
+ /// Gets the file name of a archive part
+ ///
+ /// The part
+ /// The extension
+ /// The formatted file name
+ protected string GetFilePart(int part, string extension = "")
+ {
+ var path = Options.Path[..^extension.Length];
+
+ var name = Path.GetFileNameWithoutExtension(path);
+ var directory = Path.GetDirectoryName(path);
+
+ return Path.Combine(directory, name + "." + part.ToString("000") + extension);
+ }
+
+ ///
+ /// From an offset and the alignment get the physical file and offset
+ ///
+ /// The offset of the file
+ /// The file and offset
+ protected (string, long) GetFileAndOffset(long offset)
+ {
+ // Check whether the archive is split over multiple files
+ if (Path.GetExtension(Name) == ".000")
+ {
+ var part = offset / Options.Alignment;
+ var path = GetFilePart((int)part);
+
+ return (path, offset % Options.Alignment);
+ }
+
+ return (Name, offset);
+ }
+
+ private static string[] Split(string path)
+ {
+ return path.Split('\\', StringSplitOptions.RemoveEmptyEntries);
+ }
+ }
+}
diff --git a/Yura.Shared/Archive/ArchiveOptions.cs b/Yura.Shared/Archive/ArchiveOptions.cs
new file mode 100644
index 0000000..ab30203
--- /dev/null
+++ b/Yura.Shared/Archive/ArchiveOptions.cs
@@ -0,0 +1,33 @@
+using Yura.Shared.IO;
+using Yura.Shared.Util;
+
+namespace Yura.Shared.Archive
+{
+ public class ArchiveOptions
+ {
+ ///
+ /// Gets or sets the path to the archive
+ ///
+ public string Path { get; set; }
+
+ ///
+ /// Gets or sets the archive endianness
+ ///
+ public Endianness Endianness { get; set; }
+
+ ///
+ /// Gets or sets the archive platform
+ ///
+ public Platform Platform { get; set; }
+
+ ///
+ /// Gets or sets the archive alignment
+ ///
+ public int Alignment { get; set; }
+
+ ///
+ /// Gets or sets the file list
+ ///
+ public FileList? FileList { get; set; }
+ }
+}
diff --git a/Yura.Shared/Archive/ArchiveRecord.cs b/Yura.Shared/Archive/ArchiveRecord.cs
new file mode 100644
index 0000000..f990ef4
--- /dev/null
+++ b/Yura.Shared/Archive/ArchiveRecord.cs
@@ -0,0 +1,25 @@
+namespace Yura.Shared.Archive
+{
+ public class ArchiveRecord
+ {
+ ///
+ /// Gets or sets the hash of the file name
+ ///
+ public ulong Hash { get; internal set; }
+
+ ///
+ /// Gets or sets the file name
+ ///
+ public string? Name { get; internal set; }
+
+ ///
+ /// Gets or sets the file size
+ ///
+ public uint Size { get; internal set; }
+
+ ///
+ /// Gets the file specialisation mask
+ ///
+ public ulong Specialisation { get; internal set; }
+ }
+}
diff --git a/Yura.Shared/Archive/DefianceArchive.cs b/Yura.Shared/Archive/DefianceArchive.cs
new file mode 100644
index 0000000..39e4bde
--- /dev/null
+++ b/Yura.Shared/Archive/DefianceArchive.cs
@@ -0,0 +1,72 @@
+using System.IO;
+using Yura.Shared.IO;
+
+namespace Yura.Shared.Archive
+{
+ public class DefianceArchive : ArchiveFile
+ {
+ public DefianceArchive(ArchiveOptions options) : base(options)
+ {
+ }
+
+ public override void Open()
+ {
+ var stream = File.OpenRead(Options.Path);
+ var reader = new DataReader(stream, Options.Endianness);
+
+ // Read the number of files
+ var numFiles = reader.ReadUInt16();
+
+ stream.Position += 2;
+
+ // Read the file name hashes
+ var hashes = new uint[numFiles];
+
+ for (var i = 0; i < numFiles; i++)
+ {
+ hashes[i] = reader.ReadUInt32();
+ }
+
+ // Read the file records
+ for (var i = 0; i < numFiles; i++)
+ {
+ var record = new Record
+ {
+ Hash = hashes[i],
+
+ Size = reader.ReadUInt32(),
+ Offset = reader.ReadUInt32()
+ };
+
+ reader.Position += 4;
+
+ Records.Add(record);
+ }
+
+ stream.Close();
+ }
+
+ public override byte[] Read(ArchiveRecord record)
+ {
+ var file = record as Record;
+
+ var (path, offset) = GetFileAndOffset(file.Offset);
+
+ // Read the file
+ var stream = File.OpenRead(path);
+ var data = new byte[file.Size];
+
+ stream.Position = offset;
+ stream.ReadExactly(data);
+
+ stream.Close();
+
+ return data;
+ }
+
+ private class Record : ArchiveRecord
+ {
+ public uint Offset { get; set; }
+ }
+ }
+}
diff --git a/Yura.Shared/Archive/DeusExArchive.cs b/Yura.Shared/Archive/DeusExArchive.cs
new file mode 100644
index 0000000..5f87763
--- /dev/null
+++ b/Yura.Shared/Archive/DeusExArchive.cs
@@ -0,0 +1,89 @@
+using System;
+using System.IO;
+using Yura.Shared.IO;
+
+namespace Yura.Shared.Archive
+{
+ public class DeusExArchive : ArchiveFile
+ {
+ private uint _alignment;
+
+ public DeusExArchive(ArchiveOptions options) : base(options)
+ {
+ }
+
+ public override void Open()
+ {
+ var stream = File.OpenRead(Options.Path);
+ var reader = new DataReader(stream, Options.Endianness);
+
+ // Read the header
+ _alignment = reader.ReadUInt32();
+
+ // Skip over the config name
+ reader.Position += 64;
+
+ // Read the number of files
+ var numRecords = reader.ReadUInt32();
+
+ if (numRecords > 1_000_000)
+ {
+ throw new ArgumentException("Bigfile has more than a million files, did you select the right endianness?");
+ }
+
+ // Read the file name hashes
+ var hashes = new uint[numRecords];
+
+ for (var i = 0; i < numRecords; i++)
+ {
+ hashes[i] = reader.ReadUInt32();
+ }
+
+ // Read the file records
+ for (var i = 0; i < numRecords; i++)
+ {
+ var record = new Record
+ {
+ Hash = hashes[i],
+
+ Size = reader.ReadUInt32(),
+ Offset = reader.ReadUInt32(),
+ Specialisation = reader.ReadUInt32(),
+ CompressedSize = reader.ReadUInt32(),
+ };
+
+ Records.Add(record);
+ }
+
+ stream.Close();
+ }
+
+ public override byte[] Read(ArchiveRecord record)
+ {
+ var file = record as Record;
+
+ // Calculate the location of the file
+ var offset = (long)file.Offset << 11;
+ var part = offset / _alignment;
+
+ var path = GetFilePart((int)part);
+
+ // Read the file
+ var stream = File.OpenRead(path);
+ var data = new byte[file.Size];
+
+ stream.Position = offset % _alignment;
+ stream.ReadExactly(data);
+
+ stream.Close();
+
+ return data;
+ }
+
+ private class Record : ArchiveRecord
+ {
+ public uint Offset { get; set; }
+ public uint CompressedSize { get; set; }
+ }
+ }
+}
diff --git a/Yura.Shared/Archive/LegendArchive.cs b/Yura.Shared/Archive/LegendArchive.cs
new file mode 100644
index 0000000..38d265d
--- /dev/null
+++ b/Yura.Shared/Archive/LegendArchive.cs
@@ -0,0 +1,102 @@
+using System;
+using System.IO;
+using System.IO.Compression;
+using Yura.Shared.IO;
+
+namespace Yura.Shared.Archive
+{
+ public class LegendArchive : ArchiveFile
+ {
+ public LegendArchive(ArchiveOptions options) : base(options)
+ {
+ }
+
+ public override void Open()
+ {
+ var stream = File.OpenRead(Options.Path);
+ var reader = new DataReader(stream, Options.Endianness);
+
+ // Read the number of files
+ var numRecords = reader.ReadUInt32();
+
+ if (numRecords > 1_000_000)
+ {
+ throw new ArgumentException("Bigfile has more than a million files, did you select the right endianness?");
+ }
+
+ // Read the file name hashes
+ var hashes = new uint[numRecords];
+
+ for (var i = 0; i < numRecords; i++)
+ {
+ hashes[i] = reader.ReadUInt32();
+ }
+
+ // Read the file records
+ for (var i = 0; i < numRecords; i++)
+ {
+ var record = new Record
+ {
+ Hash = hashes[i],
+
+ Size = reader.ReadUInt32(),
+ Offset = reader.ReadUInt32(),
+ Specialisation = reader.ReadUInt32(),
+ CompressedSize = reader.ReadUInt32(),
+ };
+
+ Records.Add(record);
+ }
+
+ stream.Close();
+ }
+
+ public override byte[] Read(ArchiveRecord record)
+ {
+ var file = record as Record;
+
+ // Convert sectors to file position
+ var position = (long)file.Offset << 11;
+
+ var (path, offset) = GetFileAndOffset(position);
+
+ // Read the file
+ var stream = File.OpenRead(path);
+ var data = new byte[file.Size];
+
+ if (file.CompressedSize == 0)
+ {
+ stream.Position = offset;
+ stream.ReadExactly(data);
+ }
+ else
+ {
+ ReadCompressed(stream, offset, file.CompressedSize, data);
+ }
+
+ stream.Close();
+
+ return data;
+ }
+
+ private static void ReadCompressed(Stream stream, long offset, uint size, Span buffer)
+ {
+ // Read the compressed data
+ var data = new byte[size];
+
+ stream.Position = offset;
+ stream.ReadExactly(data);
+
+ // Decompress the data
+ var zlib = new ZLibStream(new MemoryStream(data), CompressionMode.Decompress);
+
+ zlib.ReadExactly(buffer);
+ }
+
+ private class Record : ArchiveRecord
+ {
+ public uint Offset { get; set; }
+ public uint CompressedSize { get; set; }
+ }
+ }
+}
diff --git a/Yura.Shared/Archive/TigerArchive.cs b/Yura.Shared/Archive/TigerArchive.cs
new file mode 100644
index 0000000..9ddf265
--- /dev/null
+++ b/Yura.Shared/Archive/TigerArchive.cs
@@ -0,0 +1,126 @@
+using System;
+using System.IO;
+using Yura.Shared.IO;
+using Yura.Shared.Util;
+
+namespace Yura.Shared.Archive
+{
+ public class TigerArchive : ArchiveFile
+ {
+ public TigerArchive(ArchiveOptions options) : base(options)
+ {
+ }
+
+ public override void Open()
+ {
+ var stream = File.OpenRead(Options.Path);
+ var reader = new DataReader(stream, Options.Endianness);
+
+ // Read the magic
+ var magic = reader.ReadUInt32();
+
+ if (magic != 0x53464154 && magic != 0x54414653)
+ {
+ throw new Exception("File is not a tiger archive file");
+ }
+
+ var version = reader.ReadUInt32();
+
+ // Check for version 3, 4 or 5
+ if (version < 3 || version > 5)
+ {
+ throw new NotImplementedException($"Tiger archive version {version} is not supported");
+ }
+
+ // Load the file list with the correct hashing algorithm
+ Options.FileList?.Load(version < 5 ? HashAlgorithm.Crc32 : HashAlgorithm.Fnv1a);
+
+ var numArchives = reader.ReadUInt32();
+ var numRecords = reader.ReadUInt32();
+
+ // Skip 4 bytes, or 8 in version 5 or later (unless Orbis)
+ reader.Position += version >= 5 && Options.Platform != Platform.Orbis ? 8 : 4;
+
+ // Skip over the config name
+ reader.Position += 32;
+
+ // Read the file records
+ for (var i = 0; i < numRecords; i++)
+ {
+ var record = new Record();
+
+ if (version < 5)
+ {
+ record.Hash = reader.ReadUInt32();
+ record.Specialisation = reader.ReadUInt32();
+ record.Size = reader.ReadUInt32();
+ }
+ else
+ {
+ record.Hash = reader.ReadUInt64();
+ record.Specialisation = reader.ReadUInt64();
+ record.Size = reader.ReadUInt32();
+ }
+
+ // Skip in TR2 and later
+ if (version >= 4)
+ {
+ reader.Position += 4;
+ }
+
+ // TRAS
+ if (version == 3)
+ {
+ var packedOffset = reader.ReadUInt32();
+
+ record.Index = packedOffset & 0xF;
+ record.Offset = packedOffset & 0xFFFFF800;
+ }
+ // TR2
+ else if (version == 4)
+ {
+ var packedOffset = reader.ReadUInt64();
+
+ record.Index = packedOffset & 0xFFFF;
+ record.Offset = (packedOffset >> 32) & 0xFFFFFFFF;
+ }
+ // TR11
+ else if (version == 5)
+ {
+ var packedOffset = reader.ReadUInt64();
+
+ record.Index = packedOffset & 0xFFFF;
+ record.Offset = (packedOffset >> 32) & 0xFFFFFFFF;
+ }
+
+ Records.Add(record);
+ }
+
+ stream.Close();
+ }
+
+ public override byte[] Read(ArchiveRecord record)
+ {
+ var file = record as Record;
+
+ var path = GetFilePart((int)file.Index, ".tiger");
+
+ // Read the file
+ var stream = File.OpenRead(path);
+ var data = new byte[file.Size];
+
+ stream.Position = (long)file.Offset;
+ stream.ReadExactly(data);
+
+ stream.Close();
+
+ return data;
+ }
+
+ private class Record : ArchiveRecord
+ {
+ public ulong Index { get; set; }
+ public ulong Offset { get; set; }
+ }
+ }
+}
diff --git a/Yura.Shared/IO/DataReader.cs b/Yura.Shared/IO/DataReader.cs
new file mode 100644
index 0000000..37e9ea4
--- /dev/null
+++ b/Yura.Shared/IO/DataReader.cs
@@ -0,0 +1,173 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+
+namespace Yura.Shared.IO
+{
+ ///
+ /// Reads data types from a binary stream
+ ///
+ public class DataReader
+ {
+ private readonly Stream _stream;
+ private readonly Endianness _endianness;
+
+ ///
+ /// Initializes a new data reader with the specified stream and endianness
+ ///
+ /// The input stream
+ /// The endianness of the data
+ public DataReader(Stream stream, Endianness endianness)
+ {
+ ArgumentNullException.ThrowIfNull(stream);
+
+ _stream = stream;
+ _endianness = endianness;
+ }
+
+ ///
+ /// Initializes a new data reader from a buffer
+ ///
+ /// The input buffer
+ /// The endianness of the data
+ public DataReader(byte[] data, Endianness endianness) : this(new MemoryStream(data), endianness)
+ {
+ }
+
+ ///
+ /// Gets the underlying stream
+ ///
+ public Stream BaseStream => _stream;
+
+ ///
+ /// Gets the endianness
+ ///
+ public Endianness Endianness => _endianness;
+
+ ///
+ /// Gets or sets the position within the stream
+ ///
+ public long Position
+ {
+ get => _stream.Position;
+ set => _stream.Position = value;
+ }
+
+ ///
+ /// Reads a number of bytes from the stream into the buffer
+ ///
+ /// The buffer to read into
+ /// The buffer
+ private ReadOnlySpan InternalRead(Span data)
+ {
+ _stream.ReadExactly(data);
+
+ // Reverse the buffer if the data is big endian
+ if (_endianness == Endianness.BigEndian)
+ {
+ data.Reverse();
+ }
+
+ return data;
+ }
+
+ ///
+ /// Reads a byte from the stream
+ ///
+ /// The read byte
+ public byte ReadByte()
+ {
+ var value = _stream.ReadByte();
+
+ if (value == -1)
+ {
+ throw new EndOfStreamException();
+ }
+
+ return (byte)value;
+ }
+
+ ///
+ /// Reads a signed 16-bit integer from the stream
+ ///
+ /// The read integer
+ public short ReadInt16()
+ {
+ return BitConverter.ToInt16(InternalRead(stackalloc byte[2]));
+ }
+
+ ///
+ /// Reads a signed 32-bit integer from the stream
+ ///
+ /// The read integer
+ public int ReadInt32()
+ {
+ return BitConverter.ToInt32(InternalRead(stackalloc byte[4]));
+ }
+
+ ///
+ /// Reads a signed 64-bit integer from the stream
+ ///
+ /// The read integer
+ public long ReadInt64()
+ {
+ return BitConverter.ToInt64(InternalRead(stackalloc byte[8]));
+ }
+
+ ///
+ /// Reads an unsigned 16-bit integer from the stream
+ ///
+ /// The read integer
+ public ushort ReadUInt16()
+ {
+ return BitConverter.ToUInt16(InternalRead(stackalloc byte[2]));
+ }
+
+ ///
+ /// Reads an unsigned 32-bit integer from the stream
+ ///
+ /// The read integer
+ public uint ReadUInt32()
+ {
+ return BitConverter.ToUInt32(InternalRead(stackalloc byte[4]));
+ }
+
+ ///
+ /// Reads an unsigned 64-bit integer from the stream
+ ///
+ /// The read integer
+ public ulong ReadUInt64()
+ {
+ return BitConverter.ToUInt64(InternalRead(stackalloc byte[8]));
+ }
+
+ ///
+ /// Reads a 32-bit floating point value from the stream
+ ///
+ /// The read value
+ public float ReadSingle()
+ {
+ return BitConverter.ToSingle(InternalRead(stackalloc byte[4]));
+ }
+
+ ///
+ /// Reads a null-terminated string from the stream
+ ///
+ /// The encoding of the string
+ /// The read string
+ public string ReadString(Encoding encoding)
+ {
+ List data = [];
+
+ byte value;
+ while ((value = ReadByte()) != 0)
+ {
+ data.Add(value);
+ }
+
+ return encoding.GetString(data.ToArray());
+ }
+ }
+}
diff --git a/Yura.Shared/IO/DataWriter.cs b/Yura.Shared/IO/DataWriter.cs
new file mode 100644
index 0000000..5db7264
--- /dev/null
+++ b/Yura.Shared/IO/DataWriter.cs
@@ -0,0 +1,125 @@
+using System;
+using System.IO;
+using System.Linq;
+
+namespace Yura.Shared.IO
+{
+ ///
+ /// Writes data types to a binary stream
+ ///
+ public class DataWriter
+ {
+ private readonly Stream _stream;
+ private readonly Endianness _endianness;
+
+ ///
+ /// Initializes a new data writer with the specified stream and endianness
+ ///
+ /// The output stream
+ /// The endianness of the data
+ public DataWriter(Stream stream, Endianness endianness)
+ {
+ ArgumentNullException.ThrowIfNull(stream);
+
+ _stream = stream;
+ _endianness = endianness;
+ }
+
+ ///
+ /// Gets the underlying stream
+ ///
+ public Stream BaseStream => _stream;
+
+ ///
+ /// Gets the endianness
+ ///
+ public Endianness Endianness => _endianness;
+
+ ///
+ /// Gets or sets the position within the stream
+ ///
+ public long Position
+ {
+ get => _stream.Position;
+ set => _stream.Position = value;
+ }
+
+ ///
+ /// Writes a number of bytes to the stream
+ ///
+ /// The data to write
+ private void InternalWrite(Span data)
+ {
+ // Reverse the buffer if the data is big endian
+ if (_endianness == Endianness.BigEndian)
+ {
+ data.Reverse();
+ }
+
+ _stream.Write(data);
+ }
+
+ ///
+ /// Writes a signed 16-bit integer to the stream
+ ///
+ /// The integer to write
+ public void Write(short value)
+ {
+ InternalWrite(BitConverter.GetBytes(value));
+ }
+
+ ///
+ /// Writes a signed 32-bit integer to the stream
+ ///
+ /// The integer to write
+ public void Write(int value)
+ {
+ InternalWrite(BitConverter.GetBytes(value));
+ }
+
+ ///
+ /// Writes a signed 64-bit integer to the stream
+ ///
+ /// The integer to write
+ public void Write(long value)
+ {
+ InternalWrite(BitConverter.GetBytes(value));
+ }
+
+ ///
+ /// Writes an unsigned 16-bit integer to the stream
+ ///
+ /// The integer to write
+ public void Write(ushort value)
+ {
+ InternalWrite(BitConverter.GetBytes(value));
+ }
+
+ ///
+ /// Writes an unsigned 32-bit integer to the stream
+ ///
+ /// The integer to write
+ public void Write(uint value)
+ {
+ InternalWrite(BitConverter.GetBytes(value));
+ }
+
+ ///
+ /// Writes an unsigned 64-bit integer to the stream
+ ///
+ /// The integer to write
+ public void Write(ulong value)
+ {
+ InternalWrite(BitConverter.GetBytes(value));
+ }
+
+ ///
+ /// Writes a 32-bit floating point value to the stream
+ ///
+ /// The value to write
+ public void Write(float value)
+ {
+ InternalWrite(BitConverter.GetBytes(value));
+ }
+ }
+}
diff --git a/Yura.Shared/IO/Endianness.cs b/Yura.Shared/IO/Endianness.cs
new file mode 100644
index 0000000..bf180d3
--- /dev/null
+++ b/Yura.Shared/IO/Endianness.cs
@@ -0,0 +1,8 @@
+namespace Yura.Shared.IO
+{
+ public enum Endianness
+ {
+ LittleEndian,
+ BigEndian
+ }
+}
diff --git a/Yura.Shared/Util/FileList.cs b/Yura.Shared/Util/FileList.cs
new file mode 100644
index 0000000..ca9178f
--- /dev/null
+++ b/Yura.Shared/Util/FileList.cs
@@ -0,0 +1,116 @@
+using System.Collections.Generic;
+using System.IO;
+using Yura.Shared.Archive;
+
+namespace Yura.Shared.Util
+{
+ public class FileList
+ {
+ private readonly Dictionary _values;
+ private readonly string _path;
+
+ public FileList(string path, bool load)
+ {
+ _values = [];
+ _path = path;
+
+ if (load)
+ {
+ Load(HashAlgorithm.Crc32);
+ }
+ }
+
+ ///
+ /// Loads the file list with the specified hashing algorithm
+ ///
+ /// The hashing algorithm
+ public void Load(HashAlgorithm algorithm)
+ {
+ foreach (var line in File.ReadLines(_path))
+ {
+ var hash = CalculateHash(line, algorithm);
+
+ _values.TryAdd(hash, line);
+ }
+ }
+
+ ///
+ /// Resolves a hash
+ ///
+ /// The hash to resolve
+ /// The resolved hash or null
+ public string? Resolve(ulong hash)
+ {
+ if (_values.TryGetValue(hash, out var value))
+ {
+ return value;
+ }
+
+ return null;
+ }
+
+ ///
+ /// Resolves the hashes of all records
+ ///
+ /// The records to resolve the hashes for
+ public void Resolve(List records)
+ {
+ foreach (var record in records)
+ {
+ record.Name = Resolve(record.Hash);
+ }
+ }
+
+ ///
+ /// Calculates the hash of the value with the provided algorithm
+ ///
+ /// The value to calculate the hash of
+ /// The hashing algorithm
+ /// The computed result
+ public static ulong CalculateHash(string value, HashAlgorithm algorithm)
+ {
+ value = value.ToLower();
+
+ return algorithm == HashAlgorithm.Crc32 ? CalculateHash32(value) : CalculateHash64(value);
+ }
+
+ private static uint CalculateHash32(string value)
+ {
+ uint hash = 0xffffffff;
+
+ foreach (var c in value)
+ {
+ hash ^= (uint)c << 24;
+
+ for (int i = 0; i < 8; i++)
+ {
+ if ((hash & 0x80000000) != 0)
+ hash = (hash << 1) ^ 0x4c11db7;
+ else
+ hash <<= 1;
+ }
+ }
+
+ return ~hash;
+ }
+
+ private static ulong CalculateHash64(string value)
+ {
+ ulong hash = 0xcbf29ce484222325;
+
+ foreach (var c in value)
+ {
+ hash ^= c;
+ hash *= 0x100000001b3;
+ }
+
+ return hash;
+ }
+ }
+
+ public enum HashAlgorithm
+ {
+ Crc32,
+ Fnv1a
+ }
+}
diff --git a/Yura.Shared/Util/Platform.cs b/Yura.Shared/Util/Platform.cs
new file mode 100644
index 0000000..2265d2b
--- /dev/null
+++ b/Yura.Shared/Util/Platform.cs
@@ -0,0 +1,23 @@
+namespace Yura.Shared.Util
+{
+ public enum Platform
+ {
+ Pc,
+
+ // PlayStation
+ Ps2,
+ Psp,
+ Ps3,
+ Orbis,
+
+ // Xbox
+ Xbox,
+ Xenon,
+ Durango,
+ Scarlett,
+
+ // Nintendo
+ Wii,
+ GameCube
+ }
+}
diff --git a/Yura.Shared/Yura.Shared.csproj b/Yura.Shared/Yura.Shared.csproj
new file mode 100644
index 0000000..8696845
--- /dev/null
+++ b/Yura.Shared/Yura.Shared.csproj
@@ -0,0 +1,10 @@
+
+
+
+ net8.0
+ enable
+
+ 1.7.0
+
+
+
diff --git a/Yura.Test/ReaderTests.cs b/Yura.Test/ReaderTests.cs
new file mode 100644
index 0000000..37fd3f0
--- /dev/null
+++ b/Yura.Test/ReaderTests.cs
@@ -0,0 +1,47 @@
+using System.IO;
+using System.Text;
+using Yura.Shared.IO;
+
+namespace Yura.Test
+{
+ internal class ReaderTests
+ {
+ [Test]
+ public void TestSeek()
+ {
+ var stream = new MemoryStream();
+ var reader = new DataReader(stream, Endianness.LittleEndian);
+
+ reader.Position = 8;
+
+ Assert.That(reader.Position, Is.EqualTo(8));
+ }
+
+ [Test]
+ public void TestLittleEndian()
+ {
+ var stream = new MemoryStream([0x7B, 0x00, 0x00, 0x00]);
+ var reader = new DataReader(stream, Endianness.LittleEndian);
+
+ Assert.That(reader.ReadUInt32(), Is.EqualTo(123));
+ }
+
+ [Test]
+ public void TestBigEndian()
+ {
+ var stream = new MemoryStream([0x00, 0x00, 0x00, 0x7B]);
+ var reader = new DataReader(stream, Endianness.BigEndian);
+
+ Assert.That(reader.ReadUInt32(), Is.EqualTo(123));
+ }
+
+ [Test]
+ public void TestString()
+ {
+ var stream = new MemoryStream([0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x00]);
+ var reader = new DataReader(stream, Endianness.BigEndian);
+
+ Assert.That(reader.ReadString(Encoding.ASCII), Is.EqualTo("Hello"));
+ }
+ }
+}
diff --git a/Yura.Test/WriterTests.cs b/Yura.Test/WriterTests.cs
new file mode 100644
index 0000000..6559c0a
--- /dev/null
+++ b/Yura.Test/WriterTests.cs
@@ -0,0 +1,41 @@
+using System.IO;
+using Yura.Shared.IO;
+
+namespace Yura.Test
+{
+ internal class WriterTests
+ {
+ [Test]
+ public void TestSeek()
+ {
+ var stream = new MemoryStream();
+ var writer = new DataWriter(stream, Endianness.LittleEndian);
+
+ writer.Position = 8;
+
+ Assert.That(writer.Position, Is.EqualTo(8));
+ }
+
+ [Test]
+ public void TestLittleEndian()
+ {
+ var stream = new MemoryStream();
+ var writer = new DataWriter(stream, Endianness.LittleEndian);
+
+ writer.Write(123);
+
+ Assert.That(stream.ToArray(), Is.EquivalentTo((byte[])[0x7B, 0x00, 0x00, 0x00]));
+ }
+
+ [Test]
+ public void TestBigEndian()
+ {
+ var stream = new MemoryStream();
+ var writer = new DataWriter(stream, Endianness.BigEndian);
+
+ writer.Write(123);
+
+ Assert.That(stream.ToArray(), Is.EquivalentTo((byte[])[0x00, 0x00, 0x00, 0x7B]));
+ }
+ }
+}
diff --git a/Yura.Test/Yura.Test.csproj b/Yura.Test/Yura.Test.csproj
new file mode 100644
index 0000000..a6f7620
--- /dev/null
+++ b/Yura.Test/Yura.Test.csproj
@@ -0,0 +1,28 @@
+
+
+
+ net8.0
+ enable
+
+ false
+ true
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Yura.sln b/Yura.sln
index 3945a6f..8280276 100644
--- a/Yura.sln
+++ b/Yura.sln
@@ -1,10 +1,14 @@
Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 16
-VisualStudioVersion = 16.0.31911.196
+# Visual Studio Version 17
+VisualStudioVersion = 17.12.35506.116
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Yura", "Yura\Yura.csproj", "{7D73C719-E756-4019-9C62-B318F1B61776}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Yura.Shared", "Yura.Shared\Yura.Shared.csproj", "{67F16917-AD1D-485A-B419-A3360766365D}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Yura.Test", "Yura.Test\Yura.Test.csproj", "{AD8EDAE9-3B48-4437-AAA3-AEAD7127FDA0}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -15,6 +19,14 @@ Global
{7D73C719-E756-4019-9C62-B318F1B61776}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7D73C719-E756-4019-9C62-B318F1B61776}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7D73C719-E756-4019-9C62-B318F1B61776}.Release|Any CPU.Build.0 = Release|Any CPU
+ {67F16917-AD1D-485A-B419-A3360766365D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {67F16917-AD1D-485A-B419-A3360766365D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {67F16917-AD1D-485A-B419-A3360766365D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {67F16917-AD1D-485A-B419-A3360766365D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {AD8EDAE9-3B48-4437-AAA3-AEAD7127FDA0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {AD8EDAE9-3B48-4437-AAA3-AEAD7127FDA0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {AD8EDAE9-3B48-4437-AAA3-AEAD7127FDA0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {AD8EDAE9-3B48-4437-AAA3-AEAD7127FDA0}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/Yura/App.xaml.cs b/Yura/App.xaml.cs
index ef2f3ae..da0ed0f 100644
--- a/Yura/App.xaml.cs
+++ b/Yura/App.xaml.cs
@@ -29,7 +29,7 @@ public App()
// add information about open bigfile
if (_window != null && _window.Bigfile != null)
{
- sentryEvent.SetExtra("bigfile", _window.Bigfile.Filename);
+ sentryEvent.SetExtra("bigfile", _window.Bigfile.Name);
sentryEvent.SetExtra("game", _window.Game);
}
diff --git a/Yura/Archive/ArchiveFile.cs b/Yura/Archive/ArchiveFile.cs
deleted file mode 100644
index 128eb2d..0000000
--- a/Yura/Archive/ArchiveFile.cs
+++ /dev/null
@@ -1,94 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-
-namespace Yura.Archive
-{
- public abstract class ArchiveFile
- {
- public ArchiveFile(string path)
- {
- Filename = path;
- }
-
- ///
- /// Gets the records in the archive
- ///
- public abstract IReadOnlyList Records { get; }
-
- ///
- /// Opens the archive
- ///
- public abstract void Open();
-
- ///
- /// Reads a file from the archive and returns the content
- ///
- /// The file record
- /// The content of the file
- public abstract byte[] Read(ArchiveRecord record);
-
- ///
- /// Gets the specialisation mask of a record
- ///
- /// The record to get the specialisation of
- /// The specialisation mask
- public abstract uint GetSpecialisationMask(ArchiveRecord record);
-
- ///
- /// Gets or sets the file list
- ///
- public FileList FileList { get; set; }
-
- ///
- /// Gets the underlying file
- ///
- public string Filename { get; private set; }
-
- ///
- /// Gets all records in a folder
- ///
- /// The folder path
- /// All records in the folder
- public List GetFolder(string path)
- {
- // if root return all unknown files too
- if (path == "\\")
- {
- return Records.Where(
- x => x.Name == null || x.Name.Split("\\", StringSplitOptions.RemoveEmptyEntries).Length == 1).ToList();
- }
-
- var hierarchy = path.Split("\\", StringSplitOptions.RemoveEmptyEntries);
-
- return Records.Where(x =>
- {
- if (x.Name == null) return false;
-
- var split = x.Name.Split("\\", StringSplitOptions.RemoveEmptyEntries);
-
- if (split.Length - 1 != hierarchy.Length) return false;
-
- // compare both paths
- for (int i = 0; i < split.Length - 1; i++)
- {
- if (split[i] != hierarchy[i])
- {
- return false;
- }
- }
-
- return true;
- }).ToList();
- }
-
- protected string FormatBigfile(string path, int part)
- {
- var name = Path.GetFileNameWithoutExtension(path);
- var directory = Path.GetDirectoryName(path);
-
- return Path.Combine(directory, name + "." + part.ToString("000"));
- }
- }
-}
diff --git a/Yura/Archive/ArchiveRecord.cs b/Yura/Archive/ArchiveRecord.cs
deleted file mode 100644
index 0805d61..0000000
--- a/Yura/Archive/ArchiveRecord.cs
+++ /dev/null
@@ -1,22 +0,0 @@
-namespace Yura.Archive
-{
- public class ArchiveRecord
- {
- ///
- /// Gets the filename hash
- ///
- public ulong Hash { get; set; }
-
- ///
- /// Gets the name of the file
- ///
-#nullable enable
- public string? Name { get; set; }
-#nullable restore
-
- ///
- /// Gets the size of the file
- ///
- public uint Size { get; set; }
- }
-}
diff --git a/Yura/Archive/DefianceArchive.cs b/Yura/Archive/DefianceArchive.cs
deleted file mode 100644
index ebfcd8d..0000000
--- a/Yura/Archive/DefianceArchive.cs
+++ /dev/null
@@ -1,117 +0,0 @@
-using System.Collections.Generic;
-using System.IO;
-using StreamReader = Yura.IO.StreamReader;
-
-namespace Yura.Archive
-{
- class DefianceArchive : ArchiveFile
- {
- private const int XboxAlignment = 0xFA00000;
-
- private string _file;
- private TextureFormat _platform;
- private bool _littleEndian;
-
- private List _files;
-
- public DefianceArchive(string path, TextureFormat platform, bool littleEndian = true)
- : base(path)
- {
- _file = path;
- _platform = platform;
- _littleEndian = littleEndian;
-
- _files = new List();
- }
-
- public override void Open()
- {
- var stream = File.OpenRead(_file);
- var reader = new StreamReader(stream, _littleEndian);
-
- // extract number of files
- var numFiles = reader.ReadUInt16();
- reader.BaseStream.Position += 2;
-
- var hashes = new uint[numFiles];
-
- // read all filename hashes
- for (var i = 0; i < numFiles; i++)
- {
- hashes[i] = reader.ReadUInt32();
- }
-
- // read all records
- for (var i = 0; i < numFiles; i++)
- {
- var file = new DefianceRecord
- {
- Hash = hashes[i]
- };
-
- file.Size = reader.ReadUInt32();
- file.Offset = reader.ReadUInt32();
-
- // check if hash exist in file list
- if (FileList != null && FileList.Files.TryGetValue(file.Hash, out string name))
- {
- file.Name = name;
- }
-
- _files.Add(file);
-
- reader.BaseStream.Position += 4;
- }
-
- stream.Close();
- }
-
- public override IReadOnlyList Records
- {
- get
- {
- return _files;
- }
- }
-
- public override byte[] Read(ArchiveRecord record)
- {
- var file = record as DefianceRecord;
-
- var filename = _file;
- var offset = file.Offset;
-
- if (_platform == TextureFormat.Xbox)
- {
- // xbox bigfiles in this game are spread over multiple physical files
- // calculate the bigfile this file is in
- var bigfile = offset / XboxAlignment;
- offset = offset % XboxAlignment;
-
- var name = Path.GetFileNameWithoutExtension(_file);
- filename = Path.GetDirectoryName(_file) + Path.DirectorySeparatorChar + name + "." + bigfile.ToString("000");
- }
-
- var stream = File.OpenRead(filename);
- var bytes = new byte[file.Size];
-
- stream.Position = offset;
- stream.Read(bytes, 0, (int)file.Size);
-
- stream.Close();
-
- return bytes;
- }
-
- public override uint GetSpecialisationMask(ArchiveRecord record)
- {
- // defiance does not use specialisation
- return 0;
- }
- }
-
- class DefianceRecord : ArchiveRecord
- {
- public uint Offset { get; set; }
- }
-}
diff --git a/Yura/Archive/DeusExArchive.cs b/Yura/Archive/DeusExArchive.cs
deleted file mode 100644
index 8196343..0000000
--- a/Yura/Archive/DeusExArchive.cs
+++ /dev/null
@@ -1,122 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using StreamReader = Yura.IO.StreamReader;
-
-namespace Yura.Archive
-{
- class DeusExArchive : ArchiveFile
- {
- private string _file;
- private uint _alignment;
- private bool _littleEndian;
-
- private List _files;
-
- public DeusExArchive(string path, bool littleEndian = true)
- : base(path)
- {
- _file = path;
- _littleEndian = littleEndian;
-
- _files = new List();
- }
-
- public override void Open()
- {
- var stream = File.OpenRead(_file);
- var reader = new StreamReader(stream, _littleEndian);
-
- _alignment = reader.ReadUInt32();
-
- // skip over config name
- reader.BaseStream.Position += 64;
-
- // same as legend
- var numRecords = reader.ReadUInt32();
-
- if (numRecords > 1_000_000)
- {
- throw new ArgumentException("Bigfile has more than a million files, did you select the wrong endianness?");
- }
-
- var hashes = new uint[numRecords];
-
- // read all filename hashes
- for (var i = 0; i < numRecords; i++)
- {
- hashes[i] = reader.ReadUInt32();
- }
-
- // read all records
- for (var i = 0; i < numRecords; i++)
- {
- var file = new DeusExRecord()
- {
- Hash = hashes[i]
- };
-
- file.Size = reader.ReadUInt32();
- file.Offset = reader.ReadUInt32();
- file.Specialisation = reader.ReadUInt32();
-
- // check if hash exist in file list
- if (FileList != null && FileList.Files.TryGetValue(file.Hash, out string name))
- {
- file.Name = name;
- }
-
- _files.Add(file);
-
- reader.BaseStream.Position += 4;
- }
-
- stream.Close();
- }
-
- public override IReadOnlyList Records
- {
- get
- {
- return _files;
- }
- }
-
- public override byte[] Read(ArchiveRecord record)
- {
- var file = record as DeusExRecord;
-
- // calculate in which bigfile the data is
- var offset = (long)file.Offset << 11;
- var bigfile = offset / _alignment;
-
- // get the right bigfile filename
- var name = Path.GetFileNameWithoutExtension(_file);
- var filename = Path.GetDirectoryName(_file) + Path.DirectorySeparatorChar + name + "." + bigfile.ToString("000");
-
- // read data
- var stream = File.OpenRead(filename);
- var bytes = new byte[file.Size];
-
- stream.Position = offset % _alignment;
- stream.Read(bytes, 0, (int)file.Size);
-
- stream.Close();
-
- return bytes;
- }
-
- public override uint GetSpecialisationMask(ArchiveRecord record)
- {
- var file = record as DeusExRecord;
-
- return file.Specialisation;
- }
- }
-
- class DeusExRecord : ArchiveRecord
- {
- public uint Offset { get; set; }
- public uint Specialisation { get; set; }
- }
-}
diff --git a/Yura/Archive/FileList.cs b/Yura/Archive/FileList.cs
deleted file mode 100644
index 7079b78..0000000
--- a/Yura/Archive/FileList.cs
+++ /dev/null
@@ -1,105 +0,0 @@
-using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
-using System.IO;
-
-namespace Yura.Archive
-{
- public class FileList
- {
- public Dictionary Files { get; private set; }
- public string Path { get; private set; }
-
- ///
- /// Constructs a new file list with the path as input file
- ///
- /// The input file
- /// Whether to already load the file, can be false if hashing algorithm is not yet known
- public FileList([NotNull] string path, bool load)
- {
- if (!File.Exists(path))
- {
- throw new FileNotFoundException("File list not found", path);
- }
-
- Path = path;
- Files = new Dictionary();
-
- if (load)
- {
- Load(HashingAlgorithm.Crc32);
- }
- }
-
- ///
- /// Loads the file list from disk
- ///
- public void Load(HashingAlgorithm algorithm)
- {
- foreach (var line in File.ReadAllLines(Path))
- {
- ulong hash = 0;
-
- if (algorithm == HashingAlgorithm.Crc32)
- {
- hash = CalculateHash32(line);
- }
- else if (algorithm == HashingAlgorithm.Fnv1a)
- {
- hash = CalculateHash64(line);
- }
-
- // either a hash collision or double file entry
- if (!Files.ContainsKey(hash))
- {
- Files.Add(hash, line);
- }
- }
- }
-
- // CRC32
- public uint CalculateHash32(string name)
- {
- // all paths are lowercase
- name = name.ToLower();
-
- uint hash = 0xFFFFFFFF;
-
- foreach(var rune in name)
- {
- hash ^= (uint)rune << 24;
-
- for(int i = 0; i < 8; i++)
- {
- if ((hash & 0x80000000) != 0)
- hash = (hash << 1) ^ 0x4C11DB7;
- else
- hash <<= 1;
- }
- }
-
- return ~hash;
- }
-
- // FNV1A
- public ulong CalculateHash64(string name)
- {
- name = name.ToLower();
-
- ulong hash = 0xcbf29ce484222325;
-
- foreach (var rune in name)
- {
- hash = hash ^ rune;
- hash *= 0x100000001b3;
- }
-
- return hash;
- }
- }
-
- public enum HashingAlgorithm
- {
- Crc32,
- Fnv1a
- }
-}
diff --git a/Yura/Archive/LegendArchive.cs b/Yura/Archive/LegendArchive.cs
deleted file mode 100644
index 1ba64bf..0000000
--- a/Yura/Archive/LegendArchive.cs
+++ /dev/null
@@ -1,157 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.IO.Compression;
-using StreamReader = Yura.IO.StreamReader;
-
-namespace Yura.Archive
-{
- class LegendArchive : ArchiveFile
- {
- private string _file;
- private int _alignment;
- private bool _littleEndian;
-
- private List _files;
-
- public LegendArchive(string path, int alignment, bool littleEndian = true)
- : base(path)
- {
- _file = path;
- _littleEndian = littleEndian;
- _alignment = alignment;
-
- _files = new List();
- }
-
- public override void Open()
- {
- var stream = File.OpenRead(_file);
- var reader = new StreamReader(stream, _littleEndian);
-
- // extract number of files
- var numRecords = reader.ReadUInt32();
-
- if (numRecords > 1_000_000)
- {
- throw new ArgumentException("Bigfile has more than a million files, did you select the wrong endianness?");
- }
-
- var hashes = new uint[numRecords];
-
- // read all filename hashes
- for (var i = 0; i < numRecords; i++)
- {
- hashes[i] = reader.ReadUInt32();
- }
-
- // read all records
- for (var i = 0; i < numRecords; i++)
- {
- var file = new LegendRecord()
- {
- Hash = hashes[i]
- };
-
- file.Size = reader.ReadUInt32();
- file.Offset = reader.ReadUInt32();
- file.Specialisation = reader.ReadUInt32();
- file.CompressedSize = reader.ReadUInt32();
-
- // check if hash exist in file list
- if (FileList != null && FileList.Files.TryGetValue(file.Hash, out string name))
- {
- file.Name = name;
- }
-
- _files.Add(file);
- }
-
- stream.Close();
- }
-
- public override IReadOnlyList Records
- {
- get
- {
- return _files;
- }
- }
-
- public override byte[] Read(ArchiveRecord record)
- {
- var file = record as LegendRecord;
-
- var offset = (long)file.Offset << 11;
- string path = null;
-
- // check whether the bigfile is split over multiple files
- if (_file.EndsWith(".000"))
- {
- // calculate which bigfile the file is in, and get the file offset
- var bigfile = (int)(offset / _alignment);
- offset = offset % _alignment;
-
- path = FormatBigfile(_file, bigfile);
- }
- else
- {
- path = _file;
- }
-
- // read the file
- byte[] data;
- if (file.CompressedSize == 0)
- {
- data = new byte[file.Size];
- Read(path, offset, file.Size, data);
-
- return data;
- }
- else
- {
- data = new byte[file.Size];
- var compressedData = new byte[file.CompressedSize];
-
- Read(path, offset, file.CompressedSize, compressedData);
- Decompress(compressedData, data);
- }
-
- return data;
- }
-
- private void Read(string path, long offset, uint size, byte[] data)
- {
- var stream = File.OpenRead(path);
-
- stream.Position = offset;
- stream.Read(data, 0, (int)size);
-
- stream.Close();
- }
-
- private void Decompress(byte[] compressedData, byte[] data)
- {
- var stream = new MemoryStream(compressedData);
- var decompressed = new MemoryStream(data);
-
- var zlib = new ZLibStream(stream, CompressionMode.Decompress);
-
- zlib.CopyTo(decompressed);
- }
-
- public override uint GetSpecialisationMask(ArchiveRecord record)
- {
- var file = record as LegendRecord;
-
- return file.Specialisation;
- }
- }
-
- class LegendRecord : ArchiveRecord
- {
- public uint Offset { get; set; }
- public uint Specialisation { get; set; }
- public uint CompressedSize { get; set; }
- }
-}
diff --git a/Yura/Archive/TigerArchive.cs b/Yura/Archive/TigerArchive.cs
deleted file mode 100644
index ef1a259..0000000
--- a/Yura/Archive/TigerArchive.cs
+++ /dev/null
@@ -1,164 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using StreamReader = Yura.IO.StreamReader;
-
-namespace Yura.Archive
-{
- class TigerArchive : ArchiveFile
- {
- private string _file;
- private TextureFormat _platform;
- private bool _littleEndian;
-
- private List _files;
-
- public TigerArchive(string path, TextureFormat platform, bool littleEndian = true)
- : base(path)
- {
- _file = path;
- _platform = platform;
- _littleEndian = littleEndian;
-
- _files = new List();
- }
-
- public override void Open()
- {
- var stream = File.OpenRead(_file);
- var reader = new StreamReader(stream, _littleEndian);
-
- var magic = reader.ReadUInt32();
-
- if (magic != 0x53464154 && magic != 0x54414653)
- {
- throw new Exception("File is not a tiger archive file");
- }
-
- var version = reader.ReadUInt32();
-
- // check if version 3, 4 or 5
- if (version < 3 || version > 5)
- {
- throw new Exception($"Tiger archive version {version} is not supported");
- }
-
- // load file list
- FileList?.Load(version < 5 ? HashingAlgorithm.Crc32 : HashingAlgorithm.Fnv1a);
-
- var numArchives = reader.ReadUInt32();
- var numRecords = reader.ReadUInt32();
-
- // skip 4 bytes, or 8 in version 5 or later
- // also dont skip on PS4 since some idiot at CD or Eidos decided this field does not exist on PS4
- reader.BaseStream.Position += version >= 5 && _platform != TextureFormat.Orbis ? 8 : 4;
-
- // skip over config name
- reader.BaseStream.Position += 32;
-
- // read all records
- for (var i = 0; i < numRecords; i++)
- {
- var file = new TigerRecord();
-
- if (version < 5)
- {
- file.Hash = reader.ReadUInt32();
- file.Specialisation = reader.ReadUInt32();
- file.Size = reader.ReadUInt32();
- }
- else
- {
- file.Hash = reader.ReadUInt64();
- file.Specialisation = reader.ReadUInt64();
- file.Size = reader.ReadUInt32();
- }
-
- // 2013
- if (version == 3)
- {
- var packedOffset = reader.ReadUInt32();
-
- file.Index = packedOffset & 0xF;
- file.Offset = packedOffset & 0xFFFFF800;
- }
- // rise/tr2
- else if (version == 4)
- {
- reader.BaseStream.Position += 4; // padding
-
- var packedOffset = reader.ReadUInt64();
-
- // not sure about this, need to check LOWORD/HIDWORD
- file.Index = packedOffset & 0xffff;
- file.Offset = (packedOffset >> 32) & 0xFFFFFFFF;
- }
- // shadow
- else if (version == 5)
- {
- reader.BaseStream.Position += 4;
-
- var packedOffset = reader.ReadUInt64();
- file.Index = packedOffset & 0xffff;
- file.Offset = (packedOffset >> 32) & 0xFFFFFFFF;
- }
-
- // check if hash exist in file list
- if (FileList != null && FileList.Files.TryGetValue(file.Hash, out string name))
- {
- file.Name = name;
- }
-
- _files.Add(file);
- }
-
- stream.Close();
- }
-
- public override IReadOnlyList Records
- {
- get
- {
- return _files;
- }
- }
-
- public override byte[] Read(ArchiveRecord record)
- {
- var file = record as TigerRecord;
-
- // get right bigfile
- var name = Path.GetFileNameWithoutExtension(_file);
- name = name.Substring(0, name.Length - 4);
-
- var filename = Path.GetDirectoryName(_file) + Path.DirectorySeparatorChar + name + "." + file.Index.ToString("000") + ".tiger";
-
- // read the file
- var stream = File.OpenRead(filename);
- var bytes = new byte[file.Size];
-
- stream.Position = (long)file.Offset;
- stream.Read(bytes, 0, (int)file.Size);
-
- stream.Close();
-
- return bytes;
- }
-
- public override uint GetSpecialisationMask(ArchiveRecord record)
- {
- var file = record as TigerRecord;
-
- // TODO this will lose 32 bits in Shadow
- return (uint)file.Specialisation;
- }
- }
-
- class TigerRecord : ArchiveRecord
- {
- public ulong Index { get; set; }
- public ulong Offset { get; set; }
-
- public ulong Specialisation { get; set; }
- }
-}
diff --git a/Yura/CommandLineOptions.cs b/Yura/CommandLineOptions.cs
index 93ca787..16d26ad 100644
--- a/Yura/CommandLineOptions.cs
+++ b/Yura/CommandLineOptions.cs
@@ -1,5 +1,7 @@
using System;
using System.IO;
+using Yura.Shared.IO;
+using Yura.Shared.Util;
namespace Yura
{
@@ -52,11 +54,11 @@ public Game Game
}
}
- public bool LittleEndian
+ public Endianness Endianness
{
get
{
- return GetOption("-endianness") != "big";
+ return GetOption("-endianness") != "big" ? Endianness.LittleEndian : Endianness.BigEndian;
}
}
@@ -89,16 +91,16 @@ public string FileList
}
}
- public TextureFormat TextureFormat
+ public Platform Platform
{
get
{
- if (Enum.TryParse(typeof(TextureFormat), GetOption("-platform"), true, out var game))
+ if (Enum.TryParse(typeof(Platform), GetOption("-platform"), true, out var game))
{
- return (TextureFormat)game;
+ return (Platform)game;
}
- return TextureFormat.Pc;
+ return Platform.Pc;
}
}
}
diff --git a/Yura/Formats/CMPRTexture.cs b/Yura/Formats/CMPRTexture.cs
index f4040d1..e5317a7 100644
--- a/Yura/Formats/CMPRTexture.cs
+++ b/Yura/Formats/CMPRTexture.cs
@@ -2,7 +2,7 @@
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
-using Yura.IO;
+using Yura.Shared.IO;
namespace Yura.Formats
{
@@ -29,7 +29,7 @@ protected override Freezable CreateInstanceCore()
// most of the code below for CMPR algo is copied from there
public override void CopyPixels(Int32Rect sourceRect, Array pixels, int stride, int _)
{
- var reader = new StreamReader(_buffer, false);
+ var reader = new DataReader(_buffer, Endianness.LittleEndian);
for (int y = 0; y < _height; y += 8)
{
diff --git a/Yura/Formats/DrmFile.cs b/Yura/Formats/DrmFile.cs
index 511186f..f3ac03a 100644
--- a/Yura/Formats/DrmFile.cs
+++ b/Yura/Formats/DrmFile.cs
@@ -1,9 +1,6 @@
using System;
using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-using Yura.IO;
+using Yura.Shared.IO;
namespace Yura.Formats
{
@@ -18,11 +15,11 @@ class DrmFile
/// The data of the file
/// Whether the file should be read as little endian
/// Whether to relocate relocations, currently unsupported
- public DrmFile(byte[] data, bool litteEndian, bool relocate = false)
+ public DrmFile(byte[] data, Endianness endianness, bool relocate = false)
{
_sections = new List();
- var reader = new StreamReader(data, litteEndian);
+ var reader = new DataReader(data, endianness);
// read file version
if (reader.ReadInt32() != 14)
diff --git a/Yura/Formats/LocaleFile.cs b/Yura/Formats/LocaleFile.cs
index e810628..7359292 100644
--- a/Yura/Formats/LocaleFile.cs
+++ b/Yura/Formats/LocaleFile.cs
@@ -1,5 +1,6 @@
using System.Collections.Generic;
-using StreamReader = Yura.IO.StreamReader;
+using System.Text;
+using Yura.Shared.IO;
namespace Yura.Formats
{
@@ -8,9 +9,9 @@ class LocaleFile
public Language Language { get; private set; }
public List Entries { get; private set; }
- public LocaleFile(byte[] data, bool litteEndian)
+ public LocaleFile(byte[] data, Endianness endianness)
{
- var reader = new StreamReader(data, litteEndian);
+ var reader = new DataReader(data, endianness);
Entries = new List();
Language = (Language)reader.ReadUInt32();
@@ -28,7 +29,7 @@ public LocaleFile(byte[] data, bool litteEndian)
reader.BaseStream.Position = offset;
// read the string
- var str = reader.ReadString();
+ var str = reader.ReadString(Encoding.UTF8);
Entries.Add(str);
reader.BaseStream.Position = cursor;
diff --git a/Yura/IFileSettings.cs b/Yura/IFileSettings.cs
index 0954813..4eb532a 100644
--- a/Yura/IFileSettings.cs
+++ b/Yura/IFileSettings.cs
@@ -1,11 +1,14 @@
-namespace Yura
+using Yura.Shared.IO;
+using Yura.Shared.Util;
+
+namespace Yura
{
public interface IFileSettings
{
public Game Game { get; }
- public bool LittleEndian { get; }
+ public Endianness Endianness { get; }
public int Alignment { get; }
public string FileList { get; }
- public TextureFormat TextureFormat { get; }
+ public Platform Platform { get; }
}
}
diff --git a/Yura/IO/StreamReader.cs b/Yura/IO/StreamReader.cs
deleted file mode 100644
index e1bcf43..0000000
--- a/Yura/IO/StreamReader.cs
+++ /dev/null
@@ -1,111 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Text;
-
-namespace Yura.IO
-{
- ///
- /// BinaryReader-like class with support for different endianness
- ///
- class StreamReader
- {
- private Stream _stream;
- private bool _littleEndian;
-
- ///
- /// Creates a new StreamReader from a stream
- ///
- /// The stream to read from
- /// Whether the data should be read little endian
- public StreamReader(Stream stream, bool littleEndian = true)
- {
- _stream = stream;
- _littleEndian = littleEndian;
- }
-
- ///
- /// Creates a new StreamReader from a buffer
- ///
- /// The buffer to read from
- /// Whether the data should be read little endian
- public StreamReader(byte[] buffer, bool littleEndian = true)
- {
- _stream = new MemoryStream(buffer);
- _littleEndian = littleEndian;
- }
-
- private byte[] Read(int bytes)
- {
- var data = new byte[bytes];
- _stream.Read(data, 0, bytes);
-
- // i guess this kinda assumes the platform is little endian
- if (!_littleEndian)
- {
- Array.Reverse(data);
- }
-
- return data;
- }
-
- public byte ReadByte()
- {
- return (byte)_stream.ReadByte();
- }
-
- public short ReadInt16()
- {
- var data = Read(2);
-
- return BitConverter.ToInt16(data);
- }
-
- public ushort ReadUInt16()
- {
- var data = Read(2);
-
- return BitConverter.ToUInt16(data);
- }
-
- public int ReadInt32()
- {
- var data = Read(4);
-
- return BitConverter.ToInt32(data);
- }
-
- public uint ReadUInt32()
- {
- var data = Read(4);
-
- return BitConverter.ToUInt32(data);
- }
-
- public ulong ReadUInt64()
- {
- var data = Read(8);
-
- return BitConverter.ToUInt64(data);
- }
-
- ///
- /// Reads a null-terminated string from the stream
- ///
- /// The readed string
- public string ReadString()
- {
- var chars = new List();
-
- while(_stream.ReadByte() != 0)
- {
- _stream.Position--;
- chars.Add((byte)_stream.ReadByte());
- }
-
- return Encoding.UTF8.GetString(chars.ToArray());
- }
-
- public Stream BaseStream => _stream;
- }
-}
diff --git a/Yura/MainWindow.xaml.cs b/Yura/MainWindow.xaml.cs
index 77cc852..750b14e 100644
--- a/Yura/MainWindow.xaml.cs
+++ b/Yura/MainWindow.xaml.cs
@@ -11,8 +11,10 @@
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media.Imaging;
-using Yura.Archive;
using Yura.Formats;
+using Yura.Shared.Archive;
+using Yura.Shared.IO;
+using Yura.Shared.Util;
namespace Yura
{
@@ -34,8 +36,8 @@ public partial class MainWindow : Window
// dictionary of ext,file type for names and icons
private Dictionary> _fileTypes;
- private bool _littleEndian;
- private TextureFormat _textureFormat;
+ private Endianness _endianness;
+ private Platform _platform;
private Game _currentGame;
// the current open bigfile
@@ -133,31 +135,39 @@ public void OpenBigfile(string bigfile, IFileSettings settings)
{
var list = (settings.FileList == null) ? null : new FileList(settings.FileList, settings.Game != Game.Tiger);
- _littleEndian = settings.LittleEndian;
- _textureFormat = settings.TextureFormat;
+ _endianness = settings.Endianness;
+ _platform = settings.Platform;
+ _currentGame = settings.Game;
+
+ // Open the archive with the following options
+ var options = new ArchiveOptions
+ {
+ Path = bigfile,
+ Endianness = settings.Endianness,
+ Platform = settings.Platform,
+ Alignment = settings.Alignment,
+ FileList = list
+ };
switch (settings.Game)
{
case Game.Legend:
- _bigfile = new LegendArchive(bigfile, settings.Alignment, _littleEndian);
+ _bigfile = new LegendArchive(options);
break;
case Game.DeusEx:
- _bigfile = new DeusExArchive(bigfile, _littleEndian);
+ _bigfile = new DeusExArchive(options);
break;
case Game.Defiance:
- _bigfile = new DefianceArchive(bigfile, settings.TextureFormat, _littleEndian);
+ _bigfile = new DefianceArchive(options);
break;
case Game.Tiger:
- _bigfile = new TigerArchive(bigfile, settings.TextureFormat, _littleEndian);
+ _bigfile = new TigerArchive(options);
break;
default:
MessageBox.Show(this, Properties.Resources.NoGameSelectedMessage, Properties.Resources.NoGameSelected, MessageBoxButton.OK, MessageBoxImage.Exclamation);
return;
}
- _currentGame = settings.Game;
- _bigfile.FileList = list;
-
PathBox.Text = Path.GetFileName(bigfile);
try
@@ -170,6 +180,9 @@ public void OpenBigfile(string bigfile, IFileSettings settings)
return;
}
+ // Resolve all file names
+ list?.Resolve(_bigfile.Records);
+
UpdateTree();
}
@@ -211,15 +224,15 @@ private void DirectoryView_SelectedItemChanged(object sender, RoutedPropertyChan
var folder = e.NewValue as DirectoryViewFolder;
// if path is null then get root
- SwitchDirectory(folder.Path == null ? "\\" : folder.Path + "\\");
+ SwitchDirectory(folder.Path);
}
public void SwitchDirectory(string path, string selectedFile = null)
{
- var files = _bigfile.GetFolder(path);
- var bigfile = Path.GetFileName(_bigfile.Filename);
+ var files = GetFiles(path);
+ var bigfile = Path.GetFileName(_bigfile.Name);
- if (path[0] == '\\')
+ if (path == null)
{
PathBox.Text = bigfile;
}
@@ -231,6 +244,16 @@ public void SwitchDirectory(string path, string selectedFile = null)
ShowFiles(files, selectedFile);
}
+ private List GetFiles(string path)
+ {
+ if (path == null)
+ {
+ return _bigfile.Records.Where(x => x.Name == null).ToList();
+ }
+
+ return _bigfile.GetFiles(path);
+ }
+
private void ShowFiles(List files, string selectedFile = null)
{
var filesview = new List();
@@ -285,7 +308,7 @@ private void ShowFiles(List files, string selectedFile = null)
private string GetSpecMask(ArchiveRecord record)
{
- var specMask = _bigfile.GetSpecialisationMask(record);
+ var specMask = (uint)record.Specialisation;
switch ((SpecMaskView) Properties.Settings.Default.SpecMaskView)
{
@@ -362,8 +385,8 @@ private void FileView_MouseDoubleClick(object sender, MouseButtonEventArgs e)
if (size > 4 && file[0] == 33 && file[1] == 'W' && file[2] == 'A' && file[3] == 'R')
{
var viewer = new TextureViewer();
- viewer.TextureFormat = _textureFormat;
- viewer.LittleEndian = _littleEndian;
+ viewer.Platform = _platform;
+ viewer.Endianness = _endianness;
viewer.Texture = file;
viewer.Title = item.Name;
@@ -376,7 +399,7 @@ private void FileView_MouseDoubleClick(object sender, MouseButtonEventArgs e)
if (item.Name == "locals.bin")
{
var viewer = new LocaleViewer();
- viewer.LittleEndian = _littleEndian;
+ viewer.Endianness = _endianness;
viewer.Data = file;
viewer.Show();
@@ -530,7 +553,7 @@ private void SearchCommand_Executed(object sender, ExecutedRoutedEventArgs e)
{
var searchWindow = new SearchWindow() { Owner = this };
searchWindow.Archive = _bigfile;
- searchWindow.LittleEndian = _littleEndian;
+ searchWindow.Endianness = _endianness;
searchWindow.Show();
}
@@ -546,7 +569,7 @@ private void CopyFileList_Click(object sender, RoutedEventArgs e)
foreach (var file in _bigfile.Records)
{
- output.AppendLine($"{file.Hash:X8}\t{file.Size}\t{_bigfile.GetSpecialisationMask(file):X}\t{file.Name}");
+ output.AppendLine($"{file.Hash:X8}\t{file.Size}\t{(uint)file.Specialisation:X}\t{file.Name}");
}
Clipboard.SetText(output.ToString());
diff --git a/Yura/OpenDialog.xaml b/Yura/OpenDialog.xaml
index 5a33669..4777217 100644
--- a/Yura/OpenDialog.xaml
+++ b/Yura/OpenDialog.xaml
@@ -53,21 +53,23 @@
PC
- PS2
- PSP
- PS3
- PS4
+ PlayStation 2
+ PlayStation Portable
+ PlayStation 3
+ PlayStation 4
Xbox
Xbox 360
Xbox One
Xbox Series X
Wii
+ GameCube
0x9600000
0x7FF00000
+ 0xFA00000
files, int id, SectionType ty
}
// read drm file
- var drm = new DrmFile(content, LittleEndian);
+ var drm = new DrmFile(content, Endianness);
// check sections
foreach (var section in drm.Sections)
diff --git a/Yura/Windows/TextureViewer.xaml.cs b/Yura/Windows/TextureViewer.xaml.cs
index db4011e..97fab17 100644
--- a/Yura/Windows/TextureViewer.xaml.cs
+++ b/Yura/Windows/TextureViewer.xaml.cs
@@ -2,7 +2,8 @@
using System.Windows.Media;
using System.Windows.Media.Imaging;
using Yura.Formats;
-using StreamReader = Yura.IO.StreamReader;
+using Yura.Shared.IO;
+using Yura.Shared.Util;
namespace Yura
{
@@ -16,7 +17,7 @@ public TextureViewer()
InitializeComponent();
}
- private void CreateImage(int width, int height, StreamReader reader)
+ private void CreateImage(int width, int height, DataReader reader)
{
int stride = width * 4 + (width % 4);
@@ -25,13 +26,13 @@ private void CreateImage(int width, int height, StreamReader reader)
BitmapSource image;
- switch (TextureFormat)
+ switch (Platform)
{
- case TextureFormat.Wii:
+ case Platform.Wii:
image = new CMPRTexture(width, height, textureData);
break;
- case TextureFormat.Ps3:
+ case Platform.Ps3:
image = new CMPRTexture(width, height, textureData);
break;
@@ -43,15 +44,15 @@ private void CreateImage(int width, int height, StreamReader reader)
TextureImage.Source = image;
}
- public bool LittleEndian { get; set; }
+ public Endianness Endianness { get; set; }
- public TextureFormat TextureFormat { get; set; }
+ public Platform Platform { get; set; }
public byte[] Texture
{
set
{
- var reader = new StreamReader(value, LittleEndian);
+ var reader = new DataReader(value, Endianness);
var magic = reader.ReadInt32();
var start = reader.ReadInt32();
diff --git a/Yura/Yura.csproj b/Yura/Yura.csproj
index 35af785..f598549 100644
--- a/Yura/Yura.csproj
+++ b/Yura/Yura.csproj
@@ -6,7 +6,7 @@
true
true
Yura.ico
- 1.6
+ 1.7.0
@@ -26,6 +26,10 @@
+
+
+
+