using System.Runtime.InteropServices; using XFEWarframe.Client; namespace XFEWarframe.Bridge; /// /// Extends config serialization and follows its C++ copy/move/destruction lifecycle. /// All engine calls occur synchronously on the engine's original calling thread. /// Native structs and slot indices retain their original size and values. /// internal static unsafe partial class ExperimentalAbilityRuntime { private const int OverrideOffset = 0x38, SuitOverrideOffset = 0x36C0; private sealed record Plan(nint Primary, byte Slot, nint[] Abilities) { public nint[][]? Banks { get; init; } } private static readonly object Gate = new(); private static readonly Dictionary Configs = []; private static readonly Dictionary Suits = []; private static readonly Dictionary Names = new[] { "XFEAbilityOverrides", "XFEAbilityBanks", "XFEInfestationKing", "0", "1", "2", "3" }.ToDictionary(x => x, Literal); private static nint serialize, copy, move, assign, moveAssign, destroy, buildEquippedItem, initialize, set, suitDestroy, apply, typeSerialize, validate, typeBase, rank; private static Action log = _ => { }; private static bool trace; private static int transferTraceCount, setTraceCount, initializeTraceCount, serializeTraceCount; [ThreadStatic] private static bool applying; [ThreadStatic] private static nint initializingSuit; public static bool Available { get; private set; } public static void Prepare(nint image, ClientConfiguration configuration, List hooks, Action writeLog) { log = writeLog; trace = configuration.EnableGameTestControl; if (!configuration.GameSha256.Equals(ExperimentalAbilityCompatibility.ExecutableSha256, StringComparison.OrdinalIgnoreCase)) { log("Multiple abilities unavailable: executable is outside the experimental ABI profile."); return; } foreach (var entry in ExperimentalAbilityCompatibility.Entries) if (!new ReadOnlySpan((void*)(image + entry.Rva), entry.Bytes.Length / 2).SequenceEqual(Convert.FromHexString(entry.Bytes))) { log("Multiple abilities unavailable: ABI check failed for " + entry.Name); return; } nint Address(string name) => image + ExperimentalAbilityCompatibility.Entries.Single(e => e.Name == name).Rva; nint Hook(string name, nint callback) { var hook = new NativeHook(Address(name), callback); hooks.Add(hook); return hook.Trampoline; } typeBase = image + ExperimentalAbilityCompatibility.TypeBaseRva; typeSerialize = Address("TypeSerialize"); validate = Address("AbilityValidate"); apply = Address("OverrideApply"); serialize = Hook("ConfigSerialize", (nint)(delegate* unmanaged)&Serialize); copy = Hook("ConfigCopy", (nint)(delegate* unmanaged)&Copy); move = Hook("ConfigMove", (nint)(delegate* unmanaged)&Move); assign = Hook("ConfigAssign", (nint)(delegate* unmanaged)&Assign); moveAssign = Hook("ConfigMoveAssign", (nint)(delegate* unmanaged)&MoveAssign); destroy = Hook("ConfigDestroy", (nint)(delegate* unmanaged)&Destroy); buildEquippedItem = Hook("BuildEquippedItem", (nint)(delegate* unmanaged)&BuildEquippedItem); initialize = Hook("SuitInitialize", (nint)(delegate* unmanaged)&Initialize); var rankHook = NativeHook.RedirectCall(Address("SuitBeforeRanks"), (nint)(delegate* unmanaged)&BeforeRanks); hooks.Add(rankHook); rank = rankHook.Trampoline; set = Hook("OverrideSet", (nint)(delegate* unmanaged)&Set); suitDestroy = Hook("SuitDestroy", (nint)(delegate* unmanaged)&SuitDestroy); PrepareBanks(Address, Hook, hooks); PrepareDependencies(image, Address, Hook); _ = AbilityHudCatalog.Entries; // Parse the embedded resource catalog before installing hooks. Available = true; log("Multiple abilities: experimental ABI bindings prepared (2026.08.19.11.06)."); } [UnmanagedCallersOnly] private static void Serialize(nint config, nint reader) { ((delegate* unmanaged)serialize)(config, reader); var mode = *(byte*)(reader + 10); var binary = *(byte*)(reader + 11) != 0; if (trace && *(nint*)(config + OverrideOffset) != 0 && Interlocked.Increment(ref serializeTraceCount) <= 80) log($"Multiple abilities: serialize config=0x{config:X}, mode={mode}, binary={binary}."); if (binary || mode is not (0 or 1)) return; try { SerializeKing(config, reader, mode); if (mode == 0) { Plan? saved; lock (Gate) Configs.TryGetValue(config, out saved); if (saved is null || saved.Primary != *(nint*)(config + OverrideOffset) || saved.Slot != *(byte*)(config + OverrideOffset + 8)) return; Name(reader, "XFEAbilityOverrides"); ((delegate* unmanaged)Virtual(reader, 0x158))(reader, 12); ((delegate* unmanaged)Virtual(reader, 0x58))(reader); try { for (var slot = 0; slot < 4; slot++) { var ability = saved.Abilities[slot]; if (ability == 0) continue; Name(reader, slot.ToString(System.Globalization.CultureInfo.InvariantCulture)); ((delegate* unmanaged)typeSerialize)(&ability, reader, typeBase); } } finally { ((delegate* unmanaged)Virtual(reader, 0x60))(reader); } if (saved.Banks is not null) WriteBanks(reader, saved.Banks); if (trace) log("Multiple abilities: wrote extended JSON configuration."); return; } lock (Gate) Configs.Remove(config); Name(reader, "XFEAbilityOverrides"); ((delegate* unmanaged)Virtual(reader, 0x158))(reader, 12); if (((delegate* unmanaged)Virtual(reader, 0x48))(reader) == 0) return; ((delegate* unmanaged)Virtual(reader, 0x58))(reader); var abilities = new nint[4]; try { for (var slot = 0; slot < 4; slot++) { Name(reader, slot.ToString(System.Globalization.CultureInfo.InvariantCulture)); nint ability = 0; ((delegate* unmanaged)typeSerialize)(&ability, reader, typeBase); if (ability != 0 && *(nint*)((delegate* unmanaged)validate)(&ability) == 0) throw new InvalidDataException("An experimental override is not a player ability."); abilities[slot] = ability; } } finally { ((delegate* unmanaged)Virtual(reader, 0x60))(reader); } var banks = ReadBanks(reader); var primary = *(nint*)(config + OverrideOffset); var index = *(byte*)(config + OverrideOffset + 8); if (index > 3 || primary == 0 || abilities[index] != primary || (banks is null && abilities.Count(a => a != 0) < 2)) return; if (banks is not null && Enumerable.Range(0, 4).Any(i => banks[i].FirstOrDefault() != abilities[i])) throw new InvalidDataException("Ability bank does not match its compatibility projection."); lock (Gate) { if (Configs.Count >= 65536) throw new InvalidDataException("Experimental config cache limit reached."); Configs[config] = new(primary, index, abilities) { Banks = banks }; } if (trace) log($"Multiple abilities: read configuration 0x{config:X} with {abilities.Count(a => a != 0)} replacements; primary=0x{primary:X}, slot={index}."); } catch (Exception ex) { log("Multiple abilities: configuration ignored (" + ex.GetType().Name + ")."); } } private static nint Virtual(nint instance, int offset) => *(nint*)(*(nint*)instance + offset); private static void Name(nint reader, string name) { ((delegate* unmanaged)Virtual(reader, 0x138))(reader, Names[name]); } // The native reader can retain the selected property's literal view. These five small // allocations intentionally live for the entire injected module lifetime, like native literals. private static nint Literal(string text) { var bytes = System.Text.Encoding.UTF8.GetBytes(text + '\0'); var memory = Marshal.AllocHGlobal(16 + bytes.Length); *(ulong*)memory = 0xC000000000000000; *(nint*)(memory + 8) = memory + 16; Marshal.Copy(bytes, 0, memory + 16, bytes.Length); return memory; } private static nint Transfer(nint original, nint destination, nint source, bool moving) { Plan? plan; lock (Gate) Configs.TryGetValue(source, out plan); bool king; lock (Gate) king = KingConfigs.Contains(source); var result = ((delegate* unmanaged)original)(destination, source); lock (Gate) { Configs.Remove(destination); if (moving && source != destination) Configs.Remove(source); KingConfigs.Remove(destination); if (moving && source != destination) KingConfigs.Remove(source); if (king) KingConfigs.Add(destination); if (plan is not null && Configs.Count < 65536 && *(nint*)(destination + OverrideOffset) == plan.Primary && *(byte*)(destination + OverrideOffset + 8) == plan.Slot) Configs[destination] = plan; } if (trace && plan is not null && Interlocked.Increment(ref transferTraceCount) <= 80) log($"Multiple abilities: {(moving ? "move" : "copy")} 0x{source:X} -> 0x{destination:X}; primary matches={*(nint*)(destination + OverrideOffset) == plan.Primary}."); return result; } [UnmanagedCallersOnly] private static nint Copy(nint d, nint s) => Transfer(copy, d, s, false); [UnmanagedCallersOnly] private static nint Move(nint d, nint s) => Transfer(move, d, s, true); [UnmanagedCallersOnly] private static nint Assign(nint d, nint s) => Transfer(assign, d, s, false); [UnmanagedCallersOnly] private static nint MoveAssign(nint d, nint s) => Transfer(moveAssign, d, s, true); [UnmanagedCallersOnly] private static void Destroy(nint config) { lock (Gate) { Configs.Remove(config); KingConfigs.Remove(config); } ((delegate* unmanaged)destroy)(config); } [UnmanagedCallersOnly] private static void SuitDestroy(nint suit) { try { ReleaseBanks(suit); RestoreDonorAnimations(suit, true); } catch (Exception ex) { log("Ability animation cleanup: " + ex.GetType().Name); } lock (Gate) { Suits.Remove(suit); BankStates.Remove(suit); KingSuits.Remove(suit); } ((delegate* unmanaged)suitDestroy)(suit); } // The engine flattens the selected MOD config and the independently selected cosmetic // config into a new config. That path copies AbilityOverride inline, bypassing every // config copy constructor. Match the source item ID and MOD index before flattening; // never infer a plan from the primary ability (A and C can share that ability). [UnmanagedCallersOnly] private static void BuildEquippedItem(nint profile, nint loadout, nint inventory, byte mode, byte slot, nint items, nint preset, nint equipped, byte force, byte option) { var selected = SelectedPlan(items, equipped, mode); bool king; lock (Gate) king = KingConfigs.Contains(SelectedConfig(items, equipped, mode)); ((delegate* unmanaged)buildEquippedItem) (profile, loadout, inventory, mode, slot, items, preset, equipped, force, option); var config = *(nint*)(equipped + 0x68); if (config == 0 || *(uint*)(equipped + 0x70) < 0x330) return; lock (Gate) { KingConfigs.Remove(config); if (king) KingConfigs.Add(config); Configs.Remove(config); if (selected is not null && Configs.Count < 65536 && *(nint*)(config + OverrideOffset) == selected.Primary && *(byte*)(config + OverrideOffset + 8) == selected.Slot) { Configs[config] = selected; if (trace) log($"Multiple abilities: merged MOD config {*(nuint*)(equipped + 0x130)} into equipped config 0x{config:X} ({selected.Abilities.Count(a => a != 0)} slots)."); } } } private static Plan? SelectedPlan(nint items, nint equipped, byte mode) { var config = SelectedConfig(items, equipped, mode); if (config == 0) return null; lock (Gate) return Configs.TryGetValue(config, out var plan) && plan.Primary == *(nint*)(config + OverrideOffset) && plan.Slot == *(byte*)(config + OverrideOffset + 8) ? plan : null; } private static nint SelectedConfig(nint items, nint equipped, byte mode) { // Conclave builds deliberately omit the native override too. if (mode == 3 || items == 0 || equipped == 0) return 0; var first = *(nint*)items; var bytes = *(uint*)(items + 8); if (first == 0 || bytes > 0x130 * 65536 || bytes % 0x130 != 0) return 0; var index = *(nuint*)(equipped + 0x130); for (nuint offset = 0; offset < bytes; offset += 0x130) { var item = first + (nint)offset; if (*(ulong*)(item + 0x20) != *(ulong*)(equipped + 0x20) || *(uint*)(item + 0x28) != *(uint*)(equipped + 0x28)) continue; var configs = *(nint*)(item + 0x68); var configBytes = *(uint*)(item + 0x70); if (configs == 0 || index >= configBytes / 0x330) return 0; return configs + (nint)(index * 0x330); } return 0; } [UnmanagedCallersOnly] private static void Set(nint suit, nint incoming) { ReleaseBanks(suit); RestoreDonorAnimations(suit, true); Plan? next; Plan? previous; lock (Gate) { KingSuits.Remove(suit); if (KingConfigs.Contains(incoming - OverrideOffset)) KingSuits.Add(suit); Configs.TryGetValue(incoming - OverrideOffset, out next); if (next is not null && (next.Primary != *(nint*)incoming || next.Slot != *(byte*)(incoming + 8))) next = null; Suits.TryGetValue(suit, out previous); Suits.Remove(suit); BankStates.Remove(suit); if (next is not null) Suits[suit] = next; } if (trace && Interlocked.Increment(ref setTraceCount) <= 80) { string candidates; lock (Gate) candidates = string.Join(",", Configs.Where(p => p.Value.Primary == *(nint*)incoming).Take(8).Select(p => $"0x{p.Key:X}:{p.Value.Abilities.Count(a => a != 0)}")); log($"Multiple abilities: set suit=0x{suit:X}, config=0x{incoming - OverrideOffset:X}, plan={next is not null}, instances={*(int*)(suit + 0x3450)}, candidates=[{candidates}]."); } if (!applying && previous is not null) RestoreDefaults(suit, previous); ((delegate* unmanaged)set)(suit, incoming); ApplyPlan(suit, next); } [UnmanagedCallersOnly] private static void Initialize(nint suit) { var previous = initializingSuit; if (trace && Interlocked.Increment(ref initializeTraceCount) <= 40) { bool hasPlan; lock (Gate) hasPlan = Suits.ContainsKey(suit); log($"Multiple abilities: initialize suit=0x{suit:X}, plan={hasPlan}, instances={*(int*)(suit + 0x3450)}."); } if (*(int*)(suit + 0x3450) == 0) initializingSuit = suit; try { ((delegate* unmanaged)initialize)(suit); } finally { initializingSuit = previous; } } [UnmanagedCallersOnly] private static int BeforeRanks(nint suit) { Plan? plan; lock (Gate) Suits.TryGetValue(suit, out plan); // Default instances already exist here; ranks, augments and scripts are // initialized next by the engine, using the final replaced instances. if (initializingSuit == suit) { initializingSuit = 0; ApplyPlan(suit, plan); } return ((delegate* unmanaged)rank)(suit); } private static void ApplyPlan(nint suit, Plan? plan) { if (applying || plan is null || *(int*)(suit + 0x3450) == 0) return; applying = true; try { for (byte slot = 0; slot < 4; slot++) if (plan.Abilities[slot] != 0 && slot != plan.Slot) ApplyOne(suit, plan.Abilities[slot], slot); if (trace) log("Multiple abilities: applied " + plan.Abilities.Count(a => a != 0) + " slots to one suit configuration."); } finally { applying = false; } } private static void RestoreDefaults(nint suit, Plan previous) { if (*(int*)(suit + 0x3450) == 0) return; var types = *(nint*)(suit + 0x3418); var bytes = *(uint*)(suit + 0x3420); if (types == 0 || bytes < 64 || bytes > 1024) return; for (byte slot = 0; slot < 4; slot++) { if (previous.Abilities[slot] == 0) continue; var reference = *(nint*)(types + slot * 16); if (reference != 0 && *(nint*)reference != 0) ApplyOne(suit, *(nint*)(types + slot * 16 + 8), slot); } } private static void ApplyOne(nint suit, nint ability, byte slot) { var primary = *(nint*)(suit + SuitOverrideOffset); var index = *(byte*)(suit + SuitOverrideOffset + 8); try { *(nint*)(suit + SuitOverrideOffset) = ability; *(byte*)(suit + SuitOverrideOffset + 8) = slot; ((delegate* unmanaged)apply)(suit); } finally { *(nint*)(suit + SuitOverrideOffset) = primary; *(byte*)(suit + SuitOverrideOffset + 8) = index; } } }