diff --git a/ArcFormats/ArcFormats.csproj b/ArcFormats/ArcFormats.csproj index e5b88d69..44b528d9 100644 --- a/ArcFormats/ArcFormats.csproj +++ b/ArcFormats/ArcFormats.csproj @@ -134,6 +134,7 @@ + @@ -208,6 +209,7 @@ + @@ -1343,7 +1345,6 @@ - perl "$(SolutionDir)inc-revision.pl" "$(ProjectPath)" $(ConfigurationName) diff --git a/ArcFormats/ArrayPoolGuard.cs b/ArcFormats/ArrayPoolGuard.cs new file mode 100644 index 00000000..b9ae0086 --- /dev/null +++ b/ArcFormats/ArrayPoolGuard.cs @@ -0,0 +1,67 @@ +using System; +using System.Buffers; +using System.Threading; + +namespace GameRes.Utility +{ + internal struct ArrayPoolGuard : IDisposable + { + private readonly ArrayPool m_pool; + private T[] m_array; + + public T[] Array + { + get + { + if (m_array == null) + throw new ObjectDisposedException(nameof(ArrayPoolGuard)); + return m_array; + } + } + + public ArrayPoolGuard(ArrayPool pool, int minimumLength) + { + m_pool = pool; + m_array = pool.Rent(minimumLength); + if (m_array == null) + throw new InvalidOperationException("ArrayPool.Rent returned null"); + } + + public static implicit operator T[](ArrayPoolGuard guard) => guard.Array; + + public void Dispose() + { + var oldSavedArray = Interlocked.CompareExchange(ref m_array, null, m_array); + if (oldSavedArray == null) return; + m_pool.Return(oldSavedArray); + m_array = null; + } + + #region Array Properties + + public T this[int index] + { + get => Array[index]; + set => Array[index] = value; + } + + public int Length => Array.Length; + + #endregion + } + + internal static class ArrayPoolExtension + { + /// + /// Used in conjunction with the using statement to borrow arrays from the pool and automatically return it at the end of using + /// + /// The array pool rent from + /// The minimum size of the array + /// Array element type + /// An struct,it will return the array to the pool when disposed being invoked + public static ArrayPoolGuard RentSafe(this ArrayPool pool, int min_length) + { + return new ArrayPoolGuard(pool, min_length); + } + } +} \ No newline at end of file diff --git a/ArcFormats/NekoNyan/SpriteArcDAT.cs b/ArcFormats/NekoNyan/SpriteArcDAT.cs new file mode 100644 index 00000000..6a58426a --- /dev/null +++ b/ArcFormats/NekoNyan/SpriteArcDAT.cs @@ -0,0 +1,273 @@ +using System; +using System.Buffers; +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Text; + +using GameRes.Utility; + +namespace GameRes.Formats.NekoNyan +{ + public class SpriteArcEntry : PackedEntry + { + public SpriteDecryptParams DecryptParams { get; set; } + public uint Key { get; set; } + } + + [Serializable] + public class SpriteScheme : ResourceScheme + { + public Dictionary KnownGame = new Dictionary(); + } + + [Serializable] + [SuppressMessage("ReSharper", "UnassignedField.Global")] + public class SpriteDecryptParams + { + public long FileCountBeginByte; + public uint GenKeyInitMul; + public uint GenKeyInitAdd; + public int GenKeyInitShift; + public uint GenKeyRoundAdd; + public uint GenKeyRoundAnd; + public int GenKeyRoundShift; + + public long DecryMod1; + public byte DecryAdd; + public long DecryMod2; + public byte DecryXor; + } + + [Export(typeof(ArchiveFormat))] + public class SpriteArcDat : ArchiveFormat + { + private SpriteScheme m_scheme = new SpriteScheme(); + + public override string Tag { get; } = "DAT/NEKONYAN/SPRITE"; + public override string Description { get; } = "NEKONYAN/SPRITE resource archive"; + public override uint Signature { get; } = 0x00000000; + public override bool IsHierarchic { get; } = true; + + public override ResourceScheme Scheme + { + get => m_scheme; + set => m_scheme = (SpriteScheme)value; + } + + public override ArcFile TryOpen(ArcView view) + { + const int header_size = 1024; + if (view.MaxOffset < header_size) + return null; // file too small + + if (!TryIdentifyGame(view, out var game)) + return null; // not a known game + + int file_count = 0; + for (long i = game.FileCountBeginByte; i < header_size - 4; i += 4) + file_count += view.View.ReadInt32(i); + + if (file_count == 0) + return new ArcFile(view, this, Array.Empty()); + + var entries = new List(); + uint seed1 = view.View.ReadUInt32(0xD4); + uint seed2 = view.View.ReadUInt32(0x5C); + + // table of contents is encrypted, need to decrypt it first + int toc_size = 16 * file_count; + if (toc_size > view.MaxOffset - header_size) + return null; // file too small + + using (var toc_buffer = ArrayPool.Shared.RentSafe(toc_size)) + { + if (view.View.Read(header_size, toc_buffer, 0, (uint)toc_size) != toc_size) + return null; // file too small + + SpriteDecryptionUtils.Decrypt(new Span(toc_buffer, 0, toc_size), seed1, game); + + int content_offset = BitConverter.ToInt32(toc_buffer, 12); + int const_size = content_offset - (header_size + toc_size); + + if (content_offset > view.MaxOffset) + return null; // file too small + + using (var const_buffer = ArrayPool.Shared.RentSafe(const_size)) + { + if (view.View.Read(header_size + toc_size, const_buffer, 0, (uint)const_size) != const_size) + return null; // file too small + + SpriteDecryptionUtils.Decrypt(new Span(const_buffer, 0, const_size), seed2, game); + + for (int i = 0; i < file_count; i++) + { + int entry_offset = 16 * i; + uint size = BitConverter.ToUInt32(toc_buffer, entry_offset); + int const_addr = BitConverter.ToInt32(toc_buffer, entry_offset + 4); + uint key = BitConverter.ToUInt32(toc_buffer, entry_offset + 8); + uint data_addr = BitConverter.ToUInt32(toc_buffer, entry_offset + 12); + + int cnt = 0; + for (; const_addr + cnt < const_size && const_buffer[const_addr + cnt] != 0; cnt++) { } + + string name = Encoding.ASCII.GetString(const_buffer, const_addr, cnt); + entries.Add(new SpriteArcEntry + { + DecryptParams = game, + Name = name, + Offset = data_addr, + Size = size, + Key = key, + Type = FormatCatalog.Instance.GetTypeFromName(name) + }); + } + } + } + + return new ArcFile(view, this, entries); + } + + public override Stream OpenEntry(ArcFile arc, Entry entry) + { + if (!(entry is SpriteArcEntry sprite_entry)) + return arc.File.CreateStream (entry.Offset, entry.Size); + + return new SpriteDecryptionStream(arc.File.CreateStream(entry.Offset, entry.Size), sprite_entry.Key, sprite_entry.DecryptParams); + } + + private bool TryIdentifyGame(ArcView view, out SpriteDecryptParams decrypt_params) + { + decrypt_params = null; + string info_path = VFS.CombinePath(VFS.GetDirectoryName(view.Name), "app.info"); + if (!File.Exists(info_path)) + return false; + + using (var sr = new StreamReader(info_path, Encoding.UTF8)) + { + string company = sr.ReadLine(); + string product = sr.ReadLine(); + + if (string.IsNullOrEmpty(company) || string.IsNullOrEmpty(product)) + return false; + + return m_scheme.KnownGame.TryGetValue($"{company}/{product}", out decrypt_params); + } + } + } + + public static class SpriteDecryptionUtils + { + public const int KeyTableSize = 256; + + public static void Decrypt(Span data, uint key, SpriteDecryptParams decry_params, long base_index = 0) + { + Span key_table = stackalloc byte[KeyTableSize]; + GenerateKeyTable(key_table, key, decry_params); + Decrypt(data, key_table, decry_params, base_index); + } + + public static void Decrypt(Span data, Span key_table, SpriteDecryptParams decry_params, long base_index = 0) + { + if (key_table.Length != KeyTableSize) + ThrowInvalidKeyTable(); + + for (int i = 0; i < data.Length; i++) + { + long key_index = base_index + i; + byte current_byte = data[i]; + current_byte ^= key_table[(int)(key_index % decry_params.DecryMod1)]; + current_byte += decry_params.DecryAdd; + current_byte += key_table[(int)(key_index % decry_params.DecryMod2)]; + current_byte ^= decry_params.DecryXor; + data[i] = current_byte; + } + } + + public static void GenerateKeyTable(Span key_table, uint seed, in SpriteDecryptParams decry_params) + { + if (key_table.Length != KeyTableSize) + ThrowInvalidKeyTable(); + + uint state1 = seed * decry_params.GenKeyInitMul + decry_params.GenKeyInitAdd; + uint state2 = (state1 << decry_params.GenKeyInitShift) ^ state1; + for (int i = 0; i < KeyTableSize; i++) + { + state1 -= seed; + state1 += state2; + state2 = state1 + decry_params.GenKeyRoundAdd; + state1 *= state2 & decry_params.GenKeyRoundAnd; + key_table[i] = (byte)state1; + state1 >>= decry_params.GenKeyRoundShift; + } + } + + private static void ThrowInvalidKeyTable() + { + throw new ArgumentException($"Key table must be exactly {KeyTableSize} bytes long."); + } + } + + public class SpriteDecryptionStream : Stream + { + private readonly Stream m_base_stream; + private readonly byte[] m_key_table; + private readonly SpriteDecryptParams m_decry_params; + + public SpriteDecryptionStream(Stream encrypted_source, uint decryption_key, SpriteDecryptParams decry_params) + { + m_base_stream = encrypted_source; + m_decry_params = decry_params; + + m_key_table = new byte[SpriteDecryptionUtils.KeyTableSize]; + SpriteDecryptionUtils.GenerateKeyTable(m_key_table, decryption_key, decry_params); + } + + public override void Flush() + { + m_base_stream.Flush(); + } + + public override long Seek(long offset, SeekOrigin origin) + { + return m_base_stream.Seek(offset, origin); + } + + public override void SetLength(long value) + { + m_base_stream.SetLength(value); + } + + public override int Read(byte[] buffer, int offset, int count) + { + long init_position = m_base_stream.Position; + int bytes_read = m_base_stream.Read(buffer, offset, count); + + if (bytes_read == 0) + return 0; + + var span = new Span(buffer, offset, bytes_read); + SpriteDecryptionUtils.Decrypt(span, m_key_table, m_decry_params, (int)init_position); + + return bytes_read; + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotImplementedException("SpriteDecryptionStream does not support writing yet."); + } + + public override bool CanRead => m_base_stream.CanRead; + public override bool CanSeek => m_base_stream.CanSeek; + public override bool CanWrite => false; // Unimplemented for writing + + public override long Length => m_base_stream.Length; + + public override long Position + { + get => m_base_stream.Position; + set => m_base_stream.Position = value; + } + } +} \ No newline at end of file diff --git a/ArcFormats/Resources/Formats.dat b/ArcFormats/Resources/Formats.dat index ae3a873d..75788782 100644 Binary files a/ArcFormats/Resources/Formats.dat and b/ArcFormats/Resources/Formats.dat differ