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; /// /// 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. /// internal static unsafe class GameTestRuntime { private sealed record Request(string Operation, JsonObject Body, CancellationToken Cancellation, TaskCompletionSource Completion); private static readonly ConcurrentQueue Pending = new(); private static nint image, update, login, callbackTable; private static Action log = _ => { }; private static string loginResult = "not-requested"; private static int tickThread; private static nint exclusiveInputSuit; private static long exclusiveInputUntil; private static readonly Queue 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 hooks, Action 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)&Update); update = hook.Trampoline; hooks.Add(hook); if (!config.EnableGameTestControl) return; Check(module + 0x1A2C7A0, "4C8BDC49895B0855565741544155415641574881EC80000000"); hooks.Add(NativeHook.Observe(module + 0x1A2C7A0, (nint)(delegate* unmanaged)&ObserveSound)); Check(module + 0x1568020, "48895C240848896C24104889742420574883EC60498B0041"); hooks.Add(NativeHook.Observe(module + 0x1568020, (nint)(delegate* unmanaged)&ObserveEntitySound)); Check(module + 0x10F77F0, "40534881ECA0000000488B05408479014833C44889842490"); hooks.Add(NativeHook.Observe(module + 0x10F77F0, (nint)(delegate* unmanaged)&ObserveNativeSound)); callbackTable = (nint)NativeMemory.AllocZeroed(48); var table = (nint*)callbackTable; table[0] = (nint)(delegate* unmanaged)&CallbackDestroy; table[1] = (nint)(delegate* unmanaged)&CallbackValid; table[2] = (nint)(delegate* unmanaged)&CallbackTarget; table[3] = (nint)(delegate* unmanaged)&CallbackFunction; table[4] = (nint)(delegate* unmanaged)&LoginCompleted; table[5] = (nint)(delegate* unmanaged)&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((void*)address, expected.Length).SequenceEqual(expected)) throw new InvalidDataException("Game test ABI signature changed."); } public static Task 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(TaskCreationOptions.RunContinuationsAsynchronously); Pending.Enqueue(new(operation, data, cancellation, completion)); return completion.Task.WaitAsync(cancellation); } [UnmanagedCallersOnly] private static void Update(nint manager) { ((delegate* unmanaged)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(); var pressed = request.Body["pressed"]!.GetValue(); 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() ? Environment.TickCount64 + 10000 : 0; } injectingAbilityInput = true; try { // Enter the normal game skill input handler, including passive/input scripts. return new JsonObject { ["handled"] = ((delegate* unmanaged)(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()); var hashBytes = Encoding.ASCII.GetBytes(body["passwordHash"]!.GetValue()); Span callback = stackalloc nint[3]; callback.Clear(); callback[0] = callbackTable; try { email.Assign(emailBytes); hash.Assign(hashBytes); loginResult = "pending"; fixed (nint* callbackPointer = callback) ((delegate* unmanaged)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)&LoginCompleted; [UnmanagedCallersOnly] private static void CallbackCopy(nint source, nint destination) => new ReadOnlySpan((void*)source, 24).CopyTo(new Span((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 + "."); } }