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

XFEWarframe

【WinUI】基于Warframe游戏本体的公开服务器以及安装器

公开
关注 0 Fork 0 Star 0
UTF-8
using System.Collections.Concurrent;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json.Nodes;
using XFEWarframe.Client;
using XFEWarframe.Client.Runtime;

namespace XFEWarframe.Bridge;

/// <summary>
/// Opt-in, loopback-only engine test adapter. Commands run on the game's profile update
/// thread, never on an HTTP worker. No keyboard, window messages or authentication UI.
/// </summary>
internal static unsafe class GameTestRuntime
{
    private sealed record Request(string Operation, JsonObject Body, CancellationToken Cancellation, TaskCompletionSource<string> Completion);
    private static readonly ConcurrentQueue<Request> Pending = new();
    private static nint image, update, login, callbackTable;
    private static Action<string> log = _ => { };
    private static string loginResult = "not-requested";
    private static int tickThread;
    private static nint exclusiveInputSuit;
    private static long exclusiveInputUntil;
    private static readonly Queue<JsonObject> SoundEvents = [];
    private static readonly object SoundGate = new();
    private static long soundSequence;
    private static long soundCalls;
    [ThreadStatic] private static bool injectingAbilityInput;
    public static bool Available { get; private set; }
    internal static bool AcceptAbilityInput(nint suit) => !Available || injectingAbilityInput ||
        suit != exclusiveInputSuit || Environment.TickCount64 >= exclusiveInputUntil;

    public static void Prepare(nint module, ClientConfiguration config, List<NativeHook> hooks, Action<string> writeLog)
    {
        if (!config.EnableGameTestControl && !ExperimentalAbilityRuntime.Available) return;
        if (config.EnableGameTestControl && (!new Uri(config.ServerUrl).IsLoopback || !new Uri(config.GameServerUrl).IsLoopback ||
            !config.GameSha256.Equals(ExperimentalAbilityCompatibility.ExecutableSha256, StringComparison.OrdinalIgnoreCase))
            )
            throw new InvalidDataException("Game test control requires the audited build and a loopback server.");
        Check(module + 0x8DFD50, "48895C2408574883EC20488B01488BD933FFFF90E8000000");
        Check(module + 0xB1D570, "40555657415541564157488DAC2438FBFFFF4881ECC80500");
        Check(module + 0x144CF80, "4053415541574883EC50450FB6E8448BFA488BD9E8A71F6C");
        image = module; login = module + 0xB1D570; log = writeLog;
        GameTestLua.Prepare(module);
        var hook = new NativeHook(module + 0x8DFD50, (nint)(delegate* unmanaged<nint, void>)&Update);
        update = hook.Trampoline; hooks.Add(hook);
        if (!config.EnableGameTestControl) return;
        Check(module + 0x1A2C7A0, "4C8BDC49895B0855565741544155415641574881EC80000000");
        hooks.Add(NativeHook.Observe(module + 0x1A2C7A0, (nint)(delegate* unmanaged<nint, void>)&ObserveSound));
        Check(module + 0x1568020, "48895C240848896C24104889742420574883EC60498B0041");
        hooks.Add(NativeHook.Observe(module + 0x1568020, (nint)(delegate* unmanaged<nint, nint, nint, void>)&ObserveEntitySound));
        Check(module + 0x10F77F0, "40534881ECA0000000488B05408479014833C44889842490");
        hooks.Add(NativeHook.Observe(module + 0x10F77F0, (nint)(delegate* unmanaged<nint, nint, nint, nint, void>)&ObserveNativeSound));
        callbackTable = (nint)NativeMemory.AllocZeroed(48);
        var table = (nint*)callbackTable;
        table[0] = (nint)(delegate* unmanaged<nint, void>)&CallbackDestroy;
        table[1] = (nint)(delegate* unmanaged<nint, byte>)&CallbackValid;
        table[2] = (nint)(delegate* unmanaged<nint, nint>)&CallbackTarget;
        table[3] = (nint)(delegate* unmanaged<nint, nint>)&CallbackFunction;
        table[4] = (nint)(delegate* unmanaged<nint, byte, Bootstrap.GameString*, void>)&LoginCompleted;
        table[5] = (nint)(delegate* unmanaged<nint, nint, void>)&CallbackCopy;
        Available = true;
        log("Game test control: engine-thread adapter enabled for this launch.");
    }

    [UnmanagedCallersOnly] private static void ObserveSound(nint state)
    {
        try
        {
            Interlocked.Increment(ref soundCalls);
            var arguments = *(nint*)(state + 0x10);
            if (*(nint*)(state + 8) < arguments + 32 || *(int*)(arguments + 28) != 9) return;
            var reference = *(nint*)(*(nint*)(arguments + 16) + 0x18);
            var resource = reference == 0 ? 0 : *(nint*)reference;
            if (resource == 0) return;
            var type = ExperimentalAbilityRuntime.DescribeResource(*(nint*)(resource + 8));
            if (type is null) return;
            RecordSound(type, "lua");
        }
        catch { /* Observation never changes playback. */ }
    }

    [UnmanagedCallersOnly] private static void ObserveEntitySound(nint result, nint actor, nint reference)
    {
        try
        {
            var resource = reference == 0 || *(nint*)reference == 0 ? 0 : **(nint**)reference;
            if (resource != 0 && ExperimentalAbilityRuntime.DescribeResource(*(nint*)(resource + 8)) is { } type)
                RecordSound(type, "entity");
        }
        catch { /* Observation never changes playback. */ }
    }

    private static void RecordSound(string type, string source)
    {
        lock (SoundGate)
        {
            SoundEvents.Enqueue(new JsonObject { ["sequence"] = ++soundSequence, ["resource"] = type, ["source"] = source });
            while (SoundEvents.Count > 256) SoundEvents.Dequeue();
        }
    }

    [UnmanagedCallersOnly] private static void ObserveNativeSound(nint region, nint result, nint reference, nint actor)
    {
        try
        {
            var resource = reference == 0 || *(nint*)reference == 0 ? 0 : **(nint**)reference;
            if (resource != 0 && ExperimentalAbilityRuntime.DescribeResource(*(nint*)(resource + 8)) is { } type)
                RecordSound(type, "native");
        }
        catch { /* Observation never changes playback. */ }
    }

    internal static JsonObject SoundSnapshot()
    {
        lock (SoundGate) return new() { ["calls"] = Interlocked.Read(ref soundCalls),
            ["events"] = new JsonArray(SoundEvents.Select(e => (JsonNode)e.DeepClone()).ToArray()) };
    }

    private static void Check(nint address, string bytes)
    {
        var expected = Convert.FromHexString(bytes);
        if (!new ReadOnlySpan<byte>((void*)address, expected.Length).SequenceEqual(expected))
            throw new InvalidDataException("Game test ABI signature changed.");
    }

    public static Task<string> Dispatch(string operation, byte[] body, CancellationToken cancellation)
    {
        cancellation.ThrowIfCancellationRequested();
        if (Pending.Count >= 16) throw new InvalidOperationException("Game command queue is full.");
        var data = GameTestProtocol.Parse(operation, body);
        var completion = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
        Pending.Enqueue(new(operation, data, cancellation, completion));
        return completion.Task.WaitAsync(cancellation);
    }

    [UnmanagedCallersOnly] private static void Update(nint manager)
    {
        ((delegate* unmanaged<nint, void>)update)(manager);
        ExperimentalAbilityRuntime.UpdateHud();
        ExperimentalAbilityRuntime.UpdateKing();
        GameBranding.UpdateLoginUi(*(int*)(manager + 0x168) == 4);
        if (Pending.IsEmpty) return;
        if (tickThread == 0) tickThread = Environment.CurrentManagedThreadId;
        if (tickThread != Environment.CurrentManagedThreadId) return;
        if (!Pending.TryDequeue(out var request)) return;
        try
        {
            request.Cancellation.ThrowIfCancellationRequested();
            if (*(nint*)manager != image + 0x214BA70) throw new InvalidOperationException("Unexpected game profile manager.");
            if (request.Operation == "POST lua")
            {
                request.Completion.TrySetResult(GameTestLua.Execute(request.Body).ToJsonString());
                return;
            }
            if (request.Operation == "GET ability-state")
            {
                request.Completion.TrySetResult(GameTestLua.WithLocalSuit(ExperimentalAbilityRuntime.Snapshot).ToJsonString());
                return;
            }
            if (request.Operation == "POST ability-input")
            {
                var slot = request.Body["slot"]!.GetValue<int>();
                var pressed = request.Body["pressed"]!.GetValue<bool>();
                var result = GameTestLua.WithLocalSuit(suit =>
                {
                    if (request.Body["exclusive"] is { } exclusive)
                    {
                        exclusiveInputSuit = suit;
                        // A crashed test runner must never leave the user's controls disabled.
                        exclusiveInputUntil = exclusive.GetValue<bool>() ? Environment.TickCount64 + 10000 : 0;
                    }
                    injectingAbilityInput = true;
                    try
                    {
                        // Enter the normal game skill input handler, including passive/input scripts.
                        return new JsonObject { ["handled"] = ((delegate* unmanaged<nint, int, byte, byte>)(image + 0x144CF80))
                            (suit, 0x91 + slot, pressed ? (byte)1 : (byte)0) != 0 };
                    }
                    finally { injectingAbilityInput = false; }
                });
                request.Completion.TrySetResult(result.ToJsonString());
                return;
            }
            if (request.Operation == "POST login") Login(manager, request.Body);
            request.Completion.TrySetResult(new JsonObject
            {
                ["profileState"] = *(int*)(manager + 0x168),
                ["profileCount"] = *(uint*)(manager + 0x238) / 8,
                ["loginResult"] = loginResult,
                ["thread"] = tickThread
            }.ToJsonString());
        }
        catch (Exception ex) { request.Completion.TrySetException(ex); }
        finally { request.Body.Clear(); }
    }

    private static void Login(nint manager, JsonObject body)
    {
        var state = *(int*)(manager + 0x168);
        if (state is 1 or 4) throw new InvalidOperationException("The game is already logging in or logged in.");
        var email = Bootstrap.GameString.Empty; var hash = Bootstrap.GameString.Empty;
        var emailBytes = Encoding.UTF8.GetBytes(body["email"]!.GetValue<string>());
        var hashBytes = Encoding.ASCII.GetBytes(body["passwordHash"]!.GetValue<string>());
        Span<nint> callback = stackalloc nint[3]; callback.Clear(); callback[0] = callbackTable;
        try
        {
            email.Assign(emailBytes);
            hash.Assign(hashBytes);
            loginResult = "pending";
            fixed (nint* callbackPointer = callback)
                ((delegate* unmanaged<nint, Bootstrap.GameString*, Bootstrap.GameString*, nint*, byte, void>)login)
                    (manager, &email, &hash, callbackPointer, 1);
            log("Game test control: native login requested.");
        }
        finally
        {
            email.ClearAndFree(); hash.ClearAndFree();
            CryptographicOperations.ZeroMemory(emailBytes); CryptographicOperations.ZeroMemory(hashBytes);
        }
    }

    [UnmanagedCallersOnly] private static void CallbackDestroy(nint self) => *(nint*)self = 0;
    [UnmanagedCallersOnly] private static byte CallbackValid(nint self) => 1;
    [UnmanagedCallersOnly] private static nint CallbackTarget(nint self) => 0;
    [UnmanagedCallersOnly] private static nint CallbackFunction(nint self) => (nint)(delegate* unmanaged<nint, byte, Bootstrap.GameString*, void>)&LoginCompleted;
    [UnmanagedCallersOnly] private static void CallbackCopy(nint source, nint destination) => new ReadOnlySpan<byte>((void*)source, 24).CopyTo(new Span<byte>((void*)destination, 24));
    [UnmanagedCallersOnly] private static void LoginCompleted(nint self, byte success, Bootstrap.GameString* message)
    {
        loginResult = success != 0 ? "succeeded" : "failed";
        // The game's message may contain account data; never include it in logs or status.
        log("Game test control: native login " + loginResult + ".");
    }
}