diff --git a/ArcFormats/ArcFormats.csproj b/ArcFormats/ArcFormats.csproj index 71930817..dde9ceff 100644 --- a/ArcFormats/ArcFormats.csproj +++ b/ArcFormats/ArcFormats.csproj @@ -279,6 +279,7 @@ + diff --git a/ArcFormats/NekoNyan/ArcDAT.cs b/ArcFormats/NekoNyan/ArcDAT.cs new file mode 100644 index 00000000..78d8127f --- /dev/null +++ b/ArcFormats/NekoNyan/ArcDAT.cs @@ -0,0 +1,419 @@ +//! \file ArcDAT.cs +//! \date 2026-9-13 +//! \brief NekoNyan Unity Engine Resource Archive +// +// Copyright (C) 2026 by morkt +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +// IN THE SOFTWARE. +// + +using System; +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.IO; +using System.Runtime.InteropServices; +using System.Text; +using CommunityToolkit.HighPerformance; + +namespace GameRes.Formats.NekoNyan +{ + internal class NekoNyanDATArchiveV1 : ArcFile + { + public readonly ArchiveCryptoBase Crypto; + public NekoNyanDATArchiveV1(ArcView arc, ArchiveFormat impl, ICollection dir, ArchiveCryptoBase crypto) + :base (arc, impl, dir) + { + this.Crypto = crypto; + } + } + + internal class NekoNyanDATEntryV1 : Entry + { + public uint Key { get; set; } + + public bool CheckValidRange(long maxOffset) + { + return this.Offset <= maxOffset && this.Size <= maxOffset && this.Offset <= maxOffset - this.Size; + } + } + + [Export(typeof(ArchiveFormat))] + public class NekoNyanDATOpenerV1 : ArchiveFormat + { + public override string Tag => "DAT/NekoNyan"; + public override string Description => "NekoNyan Unity Engine Resource Archive"; + public override uint Signature => 0u; + public override bool IsHierarchic => true; + public override bool CanWrite => false; + + public NekoNyanDATOpenerV1() + { + Extensions = new string[] { "dat" }; + } + + public override ArcFile TryOpen(ArcView file) + { + ArchiveCryptoBase crypto = QueryTitle(file.Name); + if (crypto is null) + { + return null; + } + if (!crypto.Load(file, out List entries)) + { + return null; + } + + List dir = new List(entries.Count); + foreach(ArchiveCryptoBase.FileEntry fe in entries) + { + NekoNyanDATEntryV1 nnde = Create(fe.FileName); + nnde.Offset = fe.Offset; + nnde.Size = fe.Size; + nnde.Key = fe.Key; + + if (!nnde.CheckValidRange(file.MaxOffset)) + { + return null; + } + + dir.Add(nnde); + } + + return new NekoNyanDATArchiveV1(file, this, dir, crypto); + } + + public override Stream OpenEntry(ArcFile arc, Entry entry) + { + Stream stream = base.OpenEntry(arc, entry); + if(!(arc is NekoNyanDATArchiveV1 nnarc) || !(entry is NekoNyanDATEntryV1 e)) + { + return stream; + } + + byte[] data = new byte[stream.Length]; + stream.Read(data, 0, data.Length); + + nnarc.Crypto.DecryptData(data, e.Key); + + return new MemoryStream(data, false); + } + + private static ArchiveCryptoBase QueryTitle(string path) + { + string dir = Path.GetDirectoryName(path); + while (!string.IsNullOrEmpty(dir) && Directory.GetFiles(dir, "*.exe", SearchOption.TopDirectoryOnly).Length == 0) + { + dir = Path.GetDirectoryName(dir); + } + if (string.IsNullOrEmpty(dir)) + { + return null; + } + + string up = Path.Combine(dir, "UnityPlayer.dll"); + string uch32 = Path.Combine(dir, "UnityCrashHandler32.exe"); + string uch64 = Path.Combine(dir, "UnityCrashHandler64.exe"); + if (!File.Exists(up) || !(File.Exists(uch32) || File.Exists(uch64))) + { + return null; + } + + return ArchiveCryptoBase.CreateFactory(dir); + } + } + + internal abstract class ArchiveCryptoBase + { + public struct FileEntry + { + public string FileName; + public uint Offset; + public uint Size; + public uint Key; + } + + private static readonly Dictionary smTitles = new Dictionary() + { + { "Aokana.exe", new ArchiveCryptoV10() }, + { "AokanaEXTRA1.exe", new ArchiveCryptoV10() }, + { "Kinkoi.exe", new ArchiveCryptoV10() }, + { "AokanaEXTRA2.exe", new ArchiveCryptoV11() }, + { "Clover Days Plus.exe", new ArchiveCryptoV12()}, + { "KoiChoco.exe", new ArchiveCryptoV13() }, + }; + + public static ArchiveCryptoBase CreateFactory(string root) + { + foreach(KeyValuePair pair in ArchiveCryptoBase.smTitles) + { + string path = Path.Combine(root, pair.Key); + if (File.Exists(path)) + { + return pair.Value; + } + } + return null; + } + + public bool Load(ArcView file, out List entries) + { + using (ArcViewStream stream = file.CreateStream()) + { + return this.Initialize(stream, out entries); + } + } + + public void DecryptData(byte[] data, uint key) + { + this.Decrypt(data, key); + } + + protected abstract bool Initialize(Stream stream, out List entries); + + protected abstract List ParseFileEntry(ReadOnlySpan rawEntryData, ReadOnlySpan rawFileNamesData, int fileCount); + + protected abstract void KeyGenerator(Span tablePtr, uint key); + + protected abstract void Decrypt(Span data, uint key); + } + + internal class ArchiveCryptoV10 : ArchiveCryptoBase + { + protected override bool Initialize(Stream stream, out List entries) + { + entries = null; + + byte[] rawPkgInfo = new byte[1024]; + if (stream.Read(rawPkgInfo, 0, rawPkgInfo.Length) != rawPkgInfo.Length) + { + return false; + } + + int fileCount = 0; + uint rawFileEntryKey = 0u; + uint rawFileNamesKey = 0u; + { + Span rawPkgInfoPack4 = MemoryMarshal.Cast(rawPkgInfo); + for (int i = 4; i < 255; ++i) + { + fileCount += rawPkgInfoPack4[i]; + } + rawFileEntryKey = (uint)rawPkgInfoPack4[53]; + rawFileNamesKey = (uint)rawPkgInfoPack4[23]; + } + + byte[] rawEntryData = new byte[16 * fileCount]; + if (stream.Read(rawEntryData, 0, rawEntryData.Length) != rawEntryData.Length) + { + return false; + } + this.Decrypt(rawEntryData, rawFileEntryKey); + + byte[] rawFileNamesData = new byte[BitConverter.ToInt32(rawEntryData, 12) - (1024 + rawEntryData.Length)]; + if (stream.Read(rawFileNamesData, 0, rawFileNamesData.Length) != rawFileNamesData.Length) + { + return false; + } + this.Decrypt(rawFileNamesData, rawFileNamesKey); + + entries = this.ParseFileEntry(rawEntryData, rawFileNamesData, fileCount); + return true; + } + + protected override List ParseFileEntry(ReadOnlySpan rawEntryData, ReadOnlySpan rawFileNamesData, int fileCount) + { + List entries = new List(fileCount); + + ReadOnlySpan rawEntryDataPack4 = MemoryMarshal.Cast(rawEntryData); + for (int i = 0; i < fileCount; ++i) + { + int pos = 4 * i; + FileEntry entry = new FileEntry() + { + Size = rawEntryDataPack4[pos + 0], + Key = rawEntryDataPack4[pos + 2], + Offset = rawEntryDataPack4[pos + 3] + }; + + int fileNameOffset = (int)rawEntryDataPack4[pos + 1]; + int fileNameLen = rawFileNamesData.Slice(fileNameOffset).IndexOf((byte)0x00); + + entry.FileName = Encoding.UTF8.GetString(rawFileNamesData.Slice(fileNameOffset, fileNameLen).ToArray()).ToLower(); + + entries.Add(entry); + } + + return entries; + } + + protected override void KeyGenerator(Span tablePtr, uint key) + { + uint k1 = key * 0x00001CDF + 0x0000A74C; + uint k2 = k1 << 0x11 ^ k1; + + for (int i = 0; i < 256; ++i) + { + k1 = k1 - key + k2; + k2 = k1 + 0x38; + k1 *= k2 & 0xEF; + tablePtr[i] = (byte)k1; + k1 >>= 1; + } + } + + protected override void Decrypt(Span data, uint key) + { + Span table = stackalloc byte[256]; + this.KeyGenerator(table, key); + for (int i = 0; i < data.Length; ++i) + { + byte temp = data[i]; + temp ^= table[i % 253]; + temp += 0x03; + temp += table[i % 89]; + temp ^= 0x99; + data[i] = temp; + } + } + } + + internal class ArchiveCryptoV11 : ArchiveCryptoV10 + { + protected override bool Initialize(Stream stream, out List entries) + { + entries = null; + + byte[] rawPkgInfo = new byte[1024]; + if (stream.Read(rawPkgInfo, 0, rawPkgInfo.Length) != rawPkgInfo.Length) + { + return false; + } + + int fileCount = 0; + uint rawFileEntryKey = 0u; + uint rawFileNamesKey = 0u; + { + Span rawPkgInfoPack4 = MemoryMarshal.Cast(rawPkgInfo); + for (int i = 3; i < 255; ++i) + { + fileCount += rawPkgInfoPack4[i]; + } + rawFileEntryKey = (uint)rawPkgInfoPack4[53]; + rawFileNamesKey = (uint)rawPkgInfoPack4[23]; + } + + byte[] rawEntryData = new byte[16 * fileCount]; + if (stream.Read(rawEntryData, 0, rawEntryData.Length) != rawEntryData.Length) + { + return false; + } + this.Decrypt(rawEntryData, rawFileEntryKey); + + byte[] rawFileNamesData = new byte[BitConverter.ToInt32(rawEntryData, 12) - (1024 + rawEntryData.Length)]; + if (stream.Read(rawFileNamesData, 0, rawFileNamesData.Length) != rawFileNamesData.Length) + { + return false; + } + this.Decrypt(rawFileNamesData, rawFileNamesKey); + + entries = this.ParseFileEntry(rawEntryData, rawFileNamesData, fileCount); + return true; + } + + protected override void KeyGenerator(Span tablePtr, uint key) + { + uint k1 = key * 0x0000131C + 0x0000A740; + uint k2 = k1 << 0x07 ^ k1; + + for (int i = 0; i < 256; ++i) + { + k1 = k1 - key + k2; + k2 = k1 + 0x9C; + k1 *= k2 & 0xCE; + tablePtr[i] = (byte)k1; + k1 >>= 3; + } + } + + protected override void Decrypt(Span data, uint key) + { + Span table = stackalloc byte[256]; + this.KeyGenerator(table, key); + for (int i = 0; i < data.Length; ++i) + { + byte temp = data[i]; + temp ^= table[i % 179]; + temp += 0x03; + temp += table[i % 89]; + temp ^= 0x77; + data[i] = temp; + } + } + } + + internal class ArchiveCryptoV12 : ArchiveCryptoV10 + { + protected override void Decrypt(Span data, uint key) + { + Span table = stackalloc byte[256]; + this.KeyGenerator(table, key); + for (int i = 0; i < data.Length; ++i) + { + byte temp = data[i]; + temp ^= table[i % 253]; + temp += table[i % 59]; + temp ^= 0x99; + data[i] = temp; + } + } + } + + internal class ArchiveCryptoV13 : ArchiveCryptoV11 + { + protected override void KeyGenerator(Span tablePtr, uint key) + { + uint k1 = key * 0x00001704u + 0x0000A140u; + uint k2 = k1 << 0x07 ^ k1; + + for (int i = 0; i < 256; ++i) + { + k1 = k1 - key + k2; + k2 = k1 + 0x155u; + k1 *= k2 & 0xDCu; + tablePtr[i] = (byte)k1; + k1 >>= 2; + } + } + + protected override void Decrypt(Span data, uint key) + { + Span table = stackalloc byte[256]; + this.KeyGenerator(table, key); + for (int i = 0; i < data.Length; ++i) + { + byte temp = data[i]; + temp ^= table[i % 235]; + temp += 0x1F; + temp += table[i % 87]; + temp ^= 0xA5; + data[i] = temp; + } + } + } +}