- AssetBrowser Optimizations.

- Filter options added to `AssetBrowser`.
- New game entry.
- Togglable Model Preview.
- bug fixes.
This commit is contained in:
Razmoth
2023-05-18 20:45:30 +04:00
parent 006a87938c
commit 3d1799b9df
15 changed files with 477 additions and 71 deletions

View File

@@ -1,5 +1,7 @@
using MessagePack;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
namespace AssetStudio
@@ -26,10 +28,22 @@ namespace AssetStudio
[Key(4)]
public ClassIDType Type { get; set; }
public bool Matches(Regex regex) => regex.IsMatch(Name)
|| regex.IsMatch(Container)
|| regex.IsMatch(Source)
|| regex.IsMatch(PathID.ToString())
|| regex.IsMatch(Type.ToString());
public bool Matches(Dictionary<string, Regex> filters)
{
var matches = new List<bool>();
foreach(var filter in filters)
{
matches.Add(filter.Key switch
{
string value when value.Equals(nameof(Name), StringComparison.OrdinalIgnoreCase) => filter.Value.IsMatch(Name),
string value when value.Equals(nameof(Container), StringComparison.OrdinalIgnoreCase) => filter.Value.IsMatch(Container),
string value when value.Equals(nameof(Source), StringComparison.OrdinalIgnoreCase) => filter.Value.IsMatch(Source),
string value when value.Equals(nameof(PathID), StringComparison.OrdinalIgnoreCase) => filter.Value.IsMatch(PathID.ToString()),
string value when value.Equals(nameof (Type), StringComparison.OrdinalIgnoreCase) => filter.Value.IsMatch(Type.ToString()),
_ => throw new NotImplementedException()
});
}
return matches.Count(x => x == true) == filters.Count;
}
}
}

View File

@@ -437,6 +437,10 @@ namespace AssetStudio
{
OPFPUtils.Decrypt(compressedBytesSpan, reader.FullPath);
}
if (Game.Type.IsNetEase() && i == 0)
{
NetEaseUtils.Decrypt(compressedBytesSpan);
}
var uncompressedSize = (int)blockInfo.uncompressedSize;
var uncompressedBytes = BigArrayPool<byte>.Shared.Rent(uncompressedSize);
var uncompressedBytesSpan = uncompressedBytes.AsSpan(0, uncompressedSize);

View File

@@ -1,5 +1,7 @@
using System;
using AssetStudio;
using System;
using System.Collections.Generic;
using System.Diagnostics.Metrics;
using System.Linq;
using System.Text;
@@ -34,6 +36,54 @@ namespace AssetStudio
m_Name = reader.ReadAlignedString();
}
public bool HasModel() => m_Transform != null && m_Transform.m_Father.IsNull && m_Transform.m_Children.Length > 0;
public bool HasModel() => HasMesh(m_Transform, new List<bool>());
private static bool HasMesh(Transform m_Transform, List<bool> meshes)
{
m_Transform.m_GameObject.TryGet(out var m_GameObject);
if (m_GameObject.m_MeshRenderer != null)
{
var mesh = GetMesh(m_GameObject.m_MeshRenderer);
meshes.Add(mesh != null);
}
if (m_GameObject.m_SkinnedMeshRenderer != null)
{
var mesh = GetMesh(m_GameObject.m_SkinnedMeshRenderer);
meshes.Add(mesh != null);
}
foreach (var pptr in m_Transform.m_Children)
{
if (pptr.TryGet(out var child))
meshes.Add(HasMesh(child, meshes));
}
return meshes.Any(x => x == true);
}
private static Mesh GetMesh(Renderer meshR)
{
if (meshR is SkinnedMeshRenderer sMesh)
{
if (sMesh.m_Mesh.TryGet(out var m_Mesh))
{
return m_Mesh;
}
}
else
{
meshR.m_GameObject.TryGet(out var m_GameObject);
if (m_GameObject.m_MeshFilter != null)
{
if (m_GameObject.m_MeshFilter.m_Mesh.TryGet(out var m_Mesh))
{
return m_Mesh;
}
}
}
return null;
}
}
}

View File

@@ -0,0 +1,243 @@
using System;
using System.Buffers.Binary;
using System.Runtime.InteropServices;
using System.Text;
namespace AssetStudio
{
//Special thanks to LukeFZ#4035.
public static class NetEaseUtils
{
private static readonly byte[] Signature = new byte[] { 0xEE, 0xDD };
public static void Decrypt(Span<byte> bytes)
{
var (encryptedOffset, encryptedSize) = ReadHeader(bytes);
var encrypted = bytes.Slice(encryptedOffset, encryptedSize);
var encryptedInts = MemoryMarshal.Cast<byte, int>(encrypted);
var seedInts = new int[] { encryptedInts[3], encryptedInts[1], encryptedInts[4], encrypted.Length, encryptedInts[2] };
var seedBytes = MemoryMarshal.AsBytes<int>(seedInts).ToArray();
var seed = (int)CRC.CalculateDigest(seedBytes, 0, (uint)seedBytes.Length);
var keyPart0 = seed ^ (encryptedInts[7] + 0x1981);
var keyPart1 = seed ^ (encrypted.Length + 0x2013);
var keyPart2 = seed ^ (encryptedInts[5] + 0x1985);
var keyPart3 = seed ^ (encryptedInts[6] + 0x2018);
for (int i = 0; i < 0x20; i++)
{
encrypted[i] ^= 0xA6;
}
var block = encrypted[0x20..];
var keyVector = new int[] { keyPart2, keyPart0, keyPart1, keyPart3 };
var keysVector = new int[] { 0x571, keyPart3, 0x892, 0x750, keyPart2, keyPart0, 0x746, keyPart1, 0x568 };
if (block.Length >= 0x80)
{
var dataBlock = block[0x80..];
var keyBlock = block[..0x80].ToArray();
var keyBlockInts = MemoryMarshal.Cast<byte, int>(keyBlock);
RC4(block[..0x80], seed);
RC4(keyBlock, keyPart1);
var blockCount = dataBlock.Length / 0x80;
for (int i = 0; i < blockCount; i++)
{
var blockOffset = i * 0x80;
var type = (byte)keysVector[i % keysVector.Length] % keyVector.Length;
var dataBlockInts = MemoryMarshal.Cast<byte, int>(dataBlock.Slice(blockOffset, 0x80));
for (int j = 0; j < 0x20; j++)
{
dataBlockInts[j] ^= keyBlockInts[j] ^ type switch
{
0 => keysVector[j % keysVector.Length] ^ (0x20 - j),
1 => keyVector[(byte)keyBlockInts[j] % keyVector.Length],
2 => keyVector[(byte)keyBlockInts[j] % keyVector.Length] ^ j,
3 => keyVector[(byte)keysVector[j % keysVector.Length] % keyVector.Length] ^ j,
_ => throw new NotImplementedException()
};
}
}
var remainingCount = dataBlock.Length % 0x80;
if (remainingCount > 0)
{
var remaining = encrypted[^remainingCount..];
for (int i = 0; i < remainingCount; i++)
{
remaining[i] ^= (byte)(keyBlock[i] ^ ((uint)keysVector[(uint)keyVector[i % keyVector.Length] % keysVector.Length] % 0xFF) ^ i);
}
}
}
else
{
RC4(block, seed);
}
}
private static (int, int) ReadHeader(Span<byte> bytes)
{
var index = bytes.Search(Signature, 0);
if (index == -1 || index >= 0x40)
{
throw new Exception("Header not found !!");
}
var info = bytes[index..];
ReadVersion(info);
ReadEncryptedSize(info, bytes.Length, out var encryptedSize);
var headerOffset = 0;
ReadHeaderOffset(info, 8, ref headerOffset);
ReadHeaderOffset(info, 9, ref headerOffset);
var headerSize = 0x30;
var encryptedOffset = 0x30;
if (headerOffset == index || headerOffset == 0)
{
if (index >= 0x20)
{
headerSize = 0x40;
encryptedOffset = 0x40;
}
}
else
{
if (headerOffset >= 0x20)
{
headerSize = 0x40;
encryptedOffset = 0x40;
}
if (headerOffset > index)
{
encryptedOffset += index - headerOffset;
}
}
encryptedSize -= headerSize;
return (encryptedOffset, encryptedSize);
}
private static void ReadVersion(Span<byte> bytes)
{
var version = BinaryPrimitives.ReadUInt16LittleEndian(bytes[2..]);
if (version < 0x2017 || version > 0x2025)
{
throw new Exception("Unsupported version");
}
var versionString = version.ToString("X4");
Encoding.UTF8.GetBytes(versionString, bytes);
}
private static void ReadEncryptedSize(Span<byte> bytes, int size, out int encryptedSize)
{
var (vectorCount, bytesCount) = (bytes[4], bytes[6]);
encryptedSize = size > 0x1000 ? 0x1000 : size;
if (vectorCount != 0x2E && bytesCount != 0x2E)
{
encryptedSize = bytesCount + 0x10 * vectorCount;
if (vectorCount == 0xAA && bytesCount == 0xBB)
{
encryptedSize = 0x1000;
}
bytes[4] = bytes[6] = 0x2E;
}
}
private static void ReadHeaderOffset(Span<byte> bytes, int index, ref int headerOffset)
{
if (bytes[index + 1] == 0x31 && bytes[index] != 0x66)
{
headerOffset = bytes[index];
bytes[index] = 0x66;
}
}
public class CRC
{
private static readonly uint[] Table;
static CRC()
{
Table = new uint[256];
const uint kPoly = 0x9823D6E;
for (uint i = 0; i < 256; i++)
{
uint r = i;
for (int j = 0; j < 8; j++)
{
if ((r & 1) != 0)
r ^= kPoly;
r >>= 1;
}
Table[i] = r;
}
}
uint _value = 0xFFFFFFFF;
public void Update(byte[] data, uint offset, uint size)
{
for (uint i = 0; i < size; i++)
_value = (Table[(byte)_value ^ data[offset + i]] ^ (_value >> 8)) + 0x10;
}
public uint GetDigest() { return ~_value - 0x7D29C488; }
public static uint CalculateDigest(byte[] data, uint offset, uint size)
{
var crc = new CRC();
crc.Update(data, offset, size);
return crc.GetDigest();
}
}
public static void RC4(Span<byte> data, int key) => RC4(data, BitConverter.GetBytes(key));
public static void RC4(Span<byte> data, byte[] key)
{
int[] S = new int[0x100];
for (int _ = 0; _ < 0x100; _++)
{
S[_] = _;
}
int[] T = new int[0x100];
if (key.Length == 0x100)
{
Buffer.BlockCopy(key, 0, T, 0, key.Length);
}
else
{
for (int _ = 0; _ < 0x100; _++)
{
T[_] = key[_ % key.Length];
}
}
int i = 0;
int j = 0;
for (i = 0; i < 0x100; i++)
{
j = (j + S[i] + T[i]) % 0x100;
(S[j], S[i]) = (S[i], S[j]);
}
i = j = 0;
for (int iteration = 0; iteration < data.Length; iteration++)
{
i = (i + 1) % 0x100;
j = (j + S[i]) % 0x100;
(S[j], S[i]) = (S[i], S[j]);
var K = (uint)S[(S[j] + S[i]) % 0x100];
var k = (byte)(K << 6) | (K >> 2);
data[iteration] ^= (byte)(k + 0x3A);
}
}
}
}

View File

@@ -1,4 +1,5 @@
using System.Text;
using System;
using System.Text;
namespace AssetStudio
{
@@ -17,8 +18,8 @@ namespace AssetStudio
}
return buffer;
}
public static int Search(this byte[] src, string value, int offset = 0) => Search(src, Encoding.UTF8.GetBytes(value), offset);
public static int Search(this byte[] src, byte[] pattern, int offset)
public static int Search(this byte[] src, string value, int offset = 0) => Search(src.AsSpan(), Encoding.UTF8.GetBytes(value), offset);
public static int Search(this Span<byte> src, byte[] pattern, int offset)
{
int maxFirstCharSlot = src.Length - pattern.Length + 1;
for (int i = offset; i < maxFirstCharSlot; i++)

View File

@@ -31,6 +31,7 @@ namespace AssetStudio
Games.Add(index++, new Game(GameType.FantasyOfWind));
Games.Add(index++, new Game(GameType.ShiningNikki));
Games.Add(index++, new Game(GameType.HelixWaltz2));
Games.Add(index++, new Game(GameType.NetEase));
}
public static Game GetGame(GameType gameType) => GetGame((int)gameType);
public static Game GetGame(int index)
@@ -132,7 +133,8 @@ namespace AssetStudio
AlchemyStars,
FantasyOfWind,
ShiningNikki,
HelixWaltz2
HelixWaltz2,
NetEase
}
public static class GameTypes
@@ -152,6 +154,7 @@ namespace AssetStudio
public static bool IsTOT(this GameType type) => type == GameType.TOT;
public static bool IsNaraka(this GameType type) => type == GameType.Naraka;
public static bool IsOPFP(this GameType type) => type == GameType.OPFP;
public static bool IsNetEase(this GameType type) => type == GameType.NetEase;
public static bool IsGIGroup(this GameType type) => type switch
{
GameType.GI or GameType.GI_Pack or GameType.GI_CB1 or GameType.GI_CB2 or GameType.GI_CB3 or GameType.GI_CB3Pre => true,