XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEEDToolBox

【WinUI】XFE精英危险工具箱

公开
关注 0 Fork 0 Star 0
UTF-8
using System.Buffers.Binary;
using System.IO.Compression;
using System.Numerics;
using System.Reflection;
using System.Reflection.PortableExecutable;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;

namespace XFEEDToolBox.Core.Rendering;

public sealed record EliteMeshDraw(int MetadataOffset, int IndexCount, int VertexCount, int IndexOffset, int VertexOffset);
public sealed record EliteModelReference(string ShipType, string ResourceName, string ResourceHash, string SourceSha256,
    int DeflateOffset, int InflatedBytes, int GpuOffset, int GpuBytes, int VertexBufferOffset, int VertexStride, EliteMeshDraw[] Meshes);
public sealed record EliteModelIndex(string GameSha256, int PublicKeyRva, int PublicKeyBytes, EliteModelReference[] Models);

/// <summary>Reads the installed game's display meshes without loading or changing its executable.</summary>
public sealed class EliteLocalModels
{
    public static EliteModelIndex Reference { get; } = ReadReference();
    private readonly string root;
    private readonly BigInteger modulus, exponent;
    private readonly int rsaBytes;
    public static string Normalize(string type) => new(type.ToLowerInvariant().Where(char.IsAsciiLetterOrDigit).ToArray());
    public EliteLocalModels(string gameExecutable)
    {
        root = Path.GetDirectoryName(Path.GetFullPath(gameExecutable))!;
        using var file = File.OpenRead(gameExecutable);
        if (!Convert.ToHexString(SHA256.HashData(file)).Equals(Reference.GameSha256, StringComparison.OrdinalIgnoreCase))
            throw new InvalidDataException("游戏版本已有变化,需要更新本地模型资源索引。");
        file.Position = 0;
        using var pe = new PEReader(file);
        var key = pe.GetSectionData(Reference.PublicKeyRva).GetContent(0, Reference.PublicKeyBytes).ToArray();
        using var rsa = RSA.Create(); rsa.ImportRSAPublicKey(key, out var consumed);
        if (consumed != key.Length) throw new InvalidDataException("游戏模型资源公钥格式不符。");
        var parameters = rsa.ExportParameters(false);
        modulus = new(parameters.Modulus!, isUnsigned: true, isBigEndian: true);
        exponent = new(parameters.Exponent!, isUnsigned: true, isBigEndian: true);
        rsaBytes = parameters.Modulus!.Length;
    }
    public ShipMesh? Read(string shipType)
    {
        var model = Reference.Models.FirstOrDefault(m => Normalize(m.ShipType) == Normalize(shipType));
        if (model is null) return null;
        var bucket = Convert.ToInt32(model.ResourceHash[..1], 16).ToString("x2");
        var path = Path.Combine(root, "Win64", bucket, model.ResourceHash);
        var info = new FileInfo(path);
        if (!info.Exists || info.Length is < 262 or > 128 * 1024 * 1024) throw new InvalidDataException("本地飞船模型资源缺失或超出大小限制。");
        var archive = File.ReadAllBytes(path);
        var hash = Convert.ToHexString(SHA256.HashData(archive));
        if (!hash.Equals(model.SourceSha256, StringComparison.OrdinalIgnoreCase)) throw new InvalidDataException("本地模型资源已有变化,需要更新模型索引。");
        var resource = DecodeArchive(archive, model.ResourceHash);
        if (!resource.AsSpan(0, 4).SequenceEqual("FRES"u8) || model.DeflateOffset < 144 || model.DeflateOffset >= resource.Length)
            throw new InvalidDataException("模型资源头无效。");
        using var compressed = new ZLibStream(new MemoryStream(resource, model.DeflateOffset, resource.Length - model.DeflateOffset, false), CompressionMode.Decompress);
        var data = new byte[model.InflatedBytes]; compressed.ReadExactly(data);
        if (compressed.ReadByte() != -1) throw new InvalidDataException("模型数据长度与索引不符。");
        if (model.GpuOffset < 0 || model.GpuBytes <= 0 || (long)model.GpuOffset + model.GpuBytes > data.Length) throw new InvalidDataException("模型网格数据越界。");
        var triangles = new List<MeshVertex>(model.Meshes.Sum(m => m.IndexCount));
        foreach (var draw in model.Meshes)
        {
            if (draw.IndexCount <= 0 || draw.IndexCount % 3 != 0 || draw.VertexCount is <= 0 or > 65536 ||
                ReadInt(data, draw.MetadataOffset) != draw.IndexCount || ReadInt(data, draw.MetadataOffset + 4) != draw.VertexCount)
                throw new InvalidDataException("船体绘制记录与资源索引不符。");
            var vertices = new MeshVertex[draw.VertexCount];
            var vertexStart = checked(model.GpuOffset + model.VertexBufferOffset + draw.VertexOffset);
            if (vertexStart < model.GpuOffset || (long)vertexStart + vertices.Length * model.VertexStride > (long)model.GpuOffset + model.GpuBytes) throw new InvalidDataException("船体顶点数据越界。");
            for (var i = 0; i < vertices.Length; i++)
            {
                var bytes = data.AsSpan(vertexStart + i * model.VertexStride, model.VertexStride);
                var normal = new Vector3(bytes[8] / 127f - 1, bytes[9] / 127f - 1, bytes[10] / 127f - 1);
                if (normal.LengthSquared() < .01f) normal = Vector3.UnitY; else normal = Vector3.Normalize(normal);
                vertices[i] = new(DecodePosition(bytes), normal);
            }
            var indexStart = checked(model.GpuOffset + draw.IndexOffset);
            if (indexStart < model.GpuOffset || (long)indexStart + draw.IndexCount * 2 > (long)model.GpuOffset + model.GpuBytes) throw new InvalidDataException("船体索引数据越界。");
            for (var i = 0; i < draw.IndexCount; i++)
            {
                var index = BinaryPrimitives.ReadUInt16LittleEndian(data.AsSpan(indexStart + i * 2, 2));
                if (index >= vertices.Length) throw new InvalidDataException("船体顶点索引越界。");
                triangles.Add(vertices[index]);
            }
        }
        return ShipMeshReader.Create(triangles.ToArray(), path + "#" + model.ResourceName, hash);
    }
    internal byte[] ReadDisplayResource(string resourceHash,string expectedHash)
    {
        if(resourceHash.Length!=40||!resourceHash.All(char.IsAsciiHexDigit)||expectedHash.Length!=64)throw new InvalidDataException("本地显示资源索引无效。");
        var path=Path.Combine(root,"Win64",Convert.ToInt32(resourceHash[..1],16).ToString("x2"),resourceHash);
        var file=new FileInfo(path);
        if(!file.Exists||file.Length is <262 or >4*1024*1024)throw new InvalidDataException("本地显示资源缺失或过大。");
        var data=File.ReadAllBytes(path);
        if(!Convert.ToHexString(SHA256.HashData(data)).Equals(expectedHash,StringComparison.OrdinalIgnoreCase))throw new InvalidDataException("本地显示资源版本不匹配。");
        return DecodeArchive(data,resourceHash);
    }
    // Matches the installed game's PACKEDVERTEXDATAA vertex shader. The scale exponent is per vertex.
    public static Vector3 DecodePosition(ReadOnlySpan<byte> vertex)
    {
        if (vertex.Length < 12) throw new InvalidDataException("船体压缩顶点不完整。");
        var packed = BinaryPrimitives.ReadUInt64LittleEndian(vertex);
        var power = vertex[11] & 127;
        if (power == 64)
        {
            var radius = Math.Pow(2, (packed >> 48) * (16.0 / 65535)) - 1;
            return new((float)((packed & 65535) * (2.0 / 65535) - 1) * (float)radius,
                (float)(((packed >> 16) & 65535) * (2.0 / 65535) - 1) * (float)radius,
                (float)(((packed >> 32) & 65535) * (2.0 / 65535) - 1) * (float)radius);
        }
        if (power > 19) throw new InvalidDataException("未知的船体坐标压缩参数。");
        var scale = 1.0 / ((1 << (20 - power)) - 1); var bias = 1 << power;
        return new((float)((packed & 0x1fffff) * scale - bias),
            (float)(((packed >> 21) & 0x1fffff) * scale - bias), (float)(((packed >> 42) & 0x1fffff) * scale - bias));
    }
    private byte[] DecodeArchive(byte[] archive, string resourceHash)
    {
        if (!archive.AsSpan(0, 6).SequenceEqual(new byte[] { 70, 82, 69, 65, 0, 0 })) throw new InvalidDataException("模型资源封装版本不符。");
        // FREA uses a public RSA signature block for its header; no account or license state is accessed.
        var signature = new BigInteger(archive.AsSpan(6, rsaBytes), isUnsigned: true, isBigEndian: true);
        var blockBytes = BigInteger.ModPow(signature, exponent, modulus).ToByteArray(isUnsigned: true, isBigEndian: true);
        var block = new byte[rsaBytes]; blockBytes.CopyTo(block.AsSpan(rsaBytes - blockBytes.Length));
        if (block[0] != 0 || block[1] != 1) throw new InvalidDataException("模型资源签名头无效。");
        var separator = 2;
        while (separator < block.Length && block[separator] == 255) separator++;
        if (separator < 10 || separator >= block.Length || block[separator] != 0) throw new InvalidDataException("模型资源签名填充无效。");
        var message = block.AsSpan(separator + 1);
        if (message.Length < 40) throw new InvalidDataException("模型资源签名块不完整。");
        var size = BinaryPrimitives.ReadInt32LittleEndian(message);
        if (size is < 144 or > 128 * 1024 * 1024) throw new InvalidDataException("模型资源声明的长度无效。");
        var mask = Encoding.ASCII.GetBytes(resourceHash.ToLowerInvariant());
        var key = message.Slice(4, 32).ToArray();
        for (var i = 0; i < key.Length; i++) key[i] ^= mask[i % mask.Length];
        using var aes = Aes.Create(); aes.Key = key;
        var tail = aes.DecryptEcb(archive.AsSpan(6 + rsaBytes), PaddingMode.None);
        var prefix = message[36..];
        if (size > prefix.Length + tail.Length) throw new InvalidDataException("模型资源长度不足。");
        var result = new byte[size]; prefix[..Math.Min(size, prefix.Length)].CopyTo(result);
        if (size > prefix.Length) tail.AsSpan(0, size - prefix.Length).CopyTo(result.AsSpan(prefix.Length));
        return result;
    }
    private static int ReadInt(byte[] data, int offset) => offset >= 0 && offset <= data.Length - 4 ? BinaryPrimitives.ReadInt32LittleEndian(data.AsSpan(offset, 4)) : -1;
    private static EliteModelIndex ReadReference()
    {
        using var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("XFEEDToolBox.Core.Rendering.local-model-reference.json")!;
        return JsonSerializer.Deserialize<EliteModelIndex>(stream, new JsonSerializerOptions { PropertyNameCaseInsensitive = true })!;
    }
}