From 01e2f4dc71fe51772e596bd786512618c9a6bfbc Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Thu, 15 Jan 2026 15:03:33 -0500 Subject: [PATCH 1/4] add CompilerGeneratedObjectWrapper.cs and StateMachineWrapper.cs --- .../CompilerGeneratedObjectWrapper.cs | 83 ++++++++++++++ Reactor/Utilities/StateMachineWrapper.cs | 104 ++++++++++++++++++ 2 files changed, 187 insertions(+) create mode 100644 Reactor/Utilities/CompilerGeneratedObjectWrapper.cs create mode 100644 Reactor/Utilities/StateMachineWrapper.cs diff --git a/Reactor/Utilities/CompilerGeneratedObjectWrapper.cs b/Reactor/Utilities/CompilerGeneratedObjectWrapper.cs new file mode 100644 index 0000000..000166d --- /dev/null +++ b/Reactor/Utilities/CompilerGeneratedObjectWrapper.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using HarmonyLib; + +namespace Reactor.Utilities; + +/// +/// A safe wrapper around compiler generated classes like DisplayClass or IEnumerator state machines. +/// +public class CompilerGeneratedObjectWrapper +{ + /// + /// Gets a reference to the compiler generated object. + /// + public object GeneratedObject { get; } + + /// + /// Gets the type of the compiler generated object. + /// + protected Type GeneratedType { get; } + + /// + /// Gets the property info cache for faster lookups. + /// + protected Dictionary PropertyCache { get; } + + /// + /// Initializes a new instance of the class. + /// + /// An instance of the compiler generated object. + public CompilerGeneratedObjectWrapper(object generatedObject) + { + GeneratedObject = generatedObject; + GeneratedType = generatedObject.GetType(); + + PropertyCache = []; + } + + /// + /// Gets the value of a field in the compiler generated object. + /// + /// The name of the field to get. + /// The type of the field. + /// >The value of the field. + /// Thrown if the field does not exist. + public TField GetField(string fieldName) + { + if (!PropertyCache.TryGetValue(fieldName, out var propertyInfo)) + { + propertyInfo = AccessTools.Property(GeneratedType, fieldName); + if (propertyInfo == null) + { + throw new MissingMemberException($"Could not find field '{fieldName}' in type '{GeneratedType}'."); + } + PropertyCache[fieldName] = propertyInfo; + } + + return (TField) propertyInfo.GetValue(GeneratedObject)!; + } + + /// + /// Sets the value of a field in the compiler generated object. + /// + /// The name of the field to set. + /// The value to set. + /// The type of the field. + /// Thrown if the field does not exist. + public void SetField(string fieldName, TField value) + { + if (!PropertyCache.TryGetValue(fieldName, out var propertyInfo)) + { + propertyInfo = AccessTools.Property(GeneratedType, fieldName); + if (propertyInfo == null) + { + throw new MissingMemberException($"Could not find field '{fieldName}' in type '{GeneratedType}'."); + } + PropertyCache[fieldName] = propertyInfo; + } + + propertyInfo.SetValue(GeneratedObject, value); + } +} diff --git a/Reactor/Utilities/StateMachineWrapper.cs b/Reactor/Utilities/StateMachineWrapper.cs new file mode 100644 index 0000000..f67617b --- /dev/null +++ b/Reactor/Utilities/StateMachineWrapper.cs @@ -0,0 +1,104 @@ +using System; +using System.Linq; +using System.Reflection; +using HarmonyLib; + +namespace Reactor.Utilities; + +/// +/// A wrapper for state machine objects to access their parent instance and state. +/// +/// The type of the parent class that owns the state machine. +public class StateMachineWrapper : CompilerGeneratedObjectWrapper +{ + // normally it is fields, but IL2CPP turns them into properties + private readonly PropertyInfo _thisProperty; + private readonly PropertyInfo _stateProperty; + + private T? _parentInstance; + + /// + /// Gets the instance of the parent class that owns the state machine. + /// + public T Instance => _parentInstance ??= (T) _thisProperty.GetValue(GeneratedObject)!; + + /// + /// Gets or sets the current state of the state machine. + /// + /// The current state as an integer. + public int State + { + get => (int) _stateProperty.GetValue(GeneratedObject)!; + set => _stateProperty.SetValue(GeneratedObject, value); + } + + /// + /// Initializes a new instance of the class. + /// + /// The state machine instance to wrap. + public StateMachineWrapper(object stateMachine) : base(stateMachine) + { + _thisProperty = AccessTools.Property(GeneratedType, "__4__this"); + _stateProperty = AccessTools.Property(GeneratedType, "__1__state"); + + if (_thisProperty == null || _stateProperty == null) + { + throw new MissingMemberException($"Could not find required properties in type '{GeneratedType}'."); + } + } + + /// + /// Gets a parameter from the state machine by its name. + /// + /// The name of the parameter to retrieve. + /// The type of the parameter to retrieve. + /// >The value of the specified parameter. + /// Thrown if the specified parameter does not exist. + public TField GetParameter(string parameterName) + { + return GetField(parameterName); + } + + /// + /// Sets a parameter in the state machine by its name. + /// + /// The name of the parameter to set. + /// The value to set for the parameter. + /// The type of the parameter to set. + /// Thrown if the specified parameter does not exist. + public void SetParameter(string parameterName, TField value) + { + SetField(parameterName, value); + } + + /// + /// Attempts to retrieve the MoveNext method of a state machine for the specified method name in type T. + /// + /// The name of the method whose state machine MoveNext method is to be retrieved. + /// The type containing the state machine. + /// The MoveNext if found; otherwise, null. + public static MethodBase? GetStateMachineMoveNext(string methodName) + { + var typeName = typeof(T).FullName; + var showRoleStateMachine = + typeof(T) + .GetNestedTypes() + .FirstOrDefault(x => x.Name.Contains(methodName)); + + if (showRoleStateMachine == null) + { + Error($"Failed to find {methodName} state machine for {typeName}"); + return null; + } + + var moveNext = AccessTools.Method(showRoleStateMachine, "MoveNext"); + if (moveNext == null) + { + Error($"Failed to find MoveNext method for {typeName}.{methodName}"); + return null; + } + + Info($"Found {methodName}.MoveNext"); + return moveNext; + } +} From a5956eb59ce5e36c579475ffdc7042ccd97cd27e Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Thu, 15 Jan 2026 15:08:11 -0500 Subject: [PATCH 2/4] Use compiler generated wrappers for patching --- Reactor/GlobalUsings.cs | 1 - Reactor/Networking/Patches/ClientPatches.cs | 58 ++++++++++++----- .../Networking/Patches/ReactorConnection.cs | 23 ++++--- Reactor/Patches/Fixes/CoFindGamePatch.cs | 9 ++- .../Miscellaneous/CustomServersPatch.cs | 64 +++++++++++++------ 5 files changed, 110 insertions(+), 45 deletions(-) delete mode 100644 Reactor/GlobalUsings.cs diff --git a/Reactor/GlobalUsings.cs b/Reactor/GlobalUsings.cs deleted file mode 100644 index bb00b4a..0000000 --- a/Reactor/GlobalUsings.cs +++ /dev/null @@ -1 +0,0 @@ -global using AmongUsClient_CoFindGame = AmongUsClient._CoFindGame_d__54; diff --git a/Reactor/Networking/Patches/ClientPatches.cs b/Reactor/Networking/Patches/ClientPatches.cs index 333c440..1c8dbaf 100644 --- a/Reactor/Networking/Patches/ClientPatches.cs +++ b/Reactor/Networking/Patches/ClientPatches.cs @@ -1,15 +1,18 @@ using System; using System.Linq; +using System.Reflection; using AmongUs.Data; using AmongUs.InnerNet.GameDataMessages; using BepInEx.Unity.IL2CPP.Utils; using HarmonyLib; using Hazel; using Il2CppInterop.Runtime; +using Il2CppInterop.Runtime.InteropTypes; using Il2CppInterop.Runtime.InteropTypes.Arrays; using InnerNet; using Reactor.Networking.Extensions; using Reactor.Networking.Messages; +using Reactor.Utilities; using UnityEngine; using IEnumerator = System.Collections.IEnumerator; @@ -30,15 +33,22 @@ public static void Prefix(InnerNetClient __instance, ref DisconnectReasons reaso } } - [HarmonyPatch(typeof(InnerNetClient._HandleGameDataInner_d__165), nameof(InnerNetClient._HandleGameDataInner_d__165.MoveNext))] + [HarmonyPatch] public static class HandleGameDataInnerPatch { - public static bool Prefix(InnerNetClient._HandleGameDataInner_d__165 __instance, ref bool __result) + public static MethodBase TargetMethod() { - var innerNetClient = __instance.__4__this; - var reader = __instance.reader; + return StateMachineWrapper.GetStateMachineMoveNext(nameof(InnerNetClient.HandleGameDataInner))!; + } + + public static bool Prefix(Il2CppObjectBase __instance, ref bool __result) + { + var wrapper = new StateMachineWrapper(__instance); + + var innerNetClient = wrapper.Instance; + var reader = wrapper.GetParameter("reader"); - if (__instance.__1__state != 0) return true; + if (wrapper.State != 0) return true; if (reader.Tag == byte.MaxValue) { @@ -162,14 +172,22 @@ IEnumerator CoKick() } } - [HarmonyPatch(typeof(InnerNetClient._CoSendSceneChange_d__156), nameof(InnerNetClient._CoSendSceneChange_d__156.MoveNext))] + [HarmonyPatch] public static class CoSendSceneChangePatch { - public static bool Prefix(InnerNetClient._CoSendSceneChange_d__156 __instance, ref bool __result) + public static MethodBase TargetMethod() + { + return StateMachineWrapper.GetStateMachineMoveNext(nameof(InnerNetClient.CoSendSceneChange))!; + } + + public static bool Prefix(Il2CppObjectBase __instance, ref bool __result) { + var wrapper = new StateMachineWrapper(__instance); + if (ReactorConnection.Instance!.Syncer != Syncer.Host) return true; - var innerNetClient = __instance.__4__this; + var innerNetClient = wrapper.Instance; + var sceneName = wrapper.GetParameter("sceneName"); // Check for the conditions when the scene change message should be sent if (!innerNetClient.AmHost && @@ -184,7 +202,7 @@ public static bool Prefix(InnerNetClient._CoSendSceneChange_d__156 __instance, r writer.Write(innerNetClient.GameId); writer.StartMessage((byte) GameDataTypes.SceneChangeFlag); writer.WritePacked(innerNetClient.ClientId); - writer.Write(__instance.sceneName); + writer.Write(sceneName); // PATCH - Inject ReactorHandshakeC2S Debug("Injecting ReactorHandshakeC2S to CoSendSceneChange"); @@ -197,10 +215,10 @@ public static bool Prefix(InnerNetClient._CoSendSceneChange_d__156 __instance, r writer.Recycle(); // Create a new coroutine to let AmongUsClient handle scene changes too - innerNetClient.StartCoroutine(innerNetClient.CoOnPlayerChangedScene(clientData, __instance.sceneName)); + innerNetClient.StartCoroutine(innerNetClient.CoOnPlayerChangedScene(clientData, sceneName)); // Cancel this coroutine - __instance.__1__state = -1; + wrapper.State = -1; __result = false; return false; } @@ -210,16 +228,24 @@ public static bool Prefix(InnerNetClient._CoSendSceneChange_d__156 __instance, r } } - [HarmonyPatch(typeof(InnerNetClient._CoHandleSpawn_d__166), nameof(InnerNetClient._CoHandleSpawn_d__166.MoveNext))] + [HarmonyPatch] public static class CoHandleSpawnPatch { - public static void Postfix(InnerNetClient._CoHandleSpawn_d__166 __instance, bool __result) + public static MethodBase TargetMethod() + { + return StateMachineWrapper.GetStateMachineMoveNext(nameof(InnerNetClient.CoHandleSpawn))!; + } + + public static void Postfix(Il2CppObjectBase __instance, bool __result) { if (ReactorConnection.Instance!.Syncer != Syncer.Host) return; - if (!__result && !AmongUsClient.Instance.AmHost && __instance._ownerId_5__2 == AmongUsClient.Instance.ClientId) + var wrapper = new StateMachineWrapper(__instance); + var ownerId = wrapper.GetParameter("ownerId"); + + if (!__result && !AmongUsClient.Instance.AmHost && ownerId == AmongUsClient.Instance.ClientId) { - var reader = __instance.reader; + var reader = wrapper.GetParameter("reader"); if (reader.BytesRemaining >= ReactorHeader.Size && ReactorHeader.Read(reader)) { ModdedHandshakeS2C.Deserialize(reader, out var serverName, out var serverVersion, out _); @@ -228,7 +254,7 @@ public static void Postfix(InnerNetClient._CoHandleSpawn_d__166 __instance, bool else { Debug("Host is not modded"); - if (!Mod.Validate(ModList.Current, Array.Empty(), out var reason)) + if (!Mod.Validate(ModList.Current, [], out var reason)) { AmongUsClient.Instance.DisconnectWithReason(reason); } diff --git a/Reactor/Networking/Patches/ReactorConnection.cs b/Reactor/Networking/Patches/ReactorConnection.cs index ffbcd5e..5ec921e 100644 --- a/Reactor/Networking/Patches/ReactorConnection.cs +++ b/Reactor/Networking/Patches/ReactorConnection.cs @@ -1,5 +1,7 @@ +using System.Reflection; using HarmonyLib; using InnerNet; +using Reactor.Utilities; namespace Reactor.Networking.Patches; @@ -20,13 +22,16 @@ public class ReactorConnection /// public static ReactorConnection? Instance { get; private set; } + // CoConnect(string) was inlined, so we patch the MoveNext method instead. [HarmonyPatch] - private static class Patches + internal static class CoConnectPatch { - // CoConnect(string) was inlined, so we patch the MoveNext method instead. - [HarmonyPatch(typeof(InnerNetClient._CoConnect_d__65), nameof(InnerNetClient._CoConnect_d__65.MoveNext))] - [HarmonyPrefix] - public static void CoConnect() + public static MethodBase TargetMethod() + { + return StateMachineWrapper.GetStateMachineMoveNext(nameof(InnerNetClient.CoConnect))!; + } + + public static void Prefix() { if (Instance == null) { @@ -34,10 +39,12 @@ public static void CoConnect() Instance = new ReactorConnection(); } } + } - [HarmonyPatch(typeof(InnerNetClient), nameof(InnerNetClient.DisconnectInternal))] - [HarmonyPostfix] - public static void DisconnectInternalPostfix() + [HarmonyPatch(typeof(InnerNetClient), nameof(InnerNetClient.DisconnectInternal))] + internal static class InnerNetClientDisconnectPatch + { + public static void Postfix() { Debug("ReactorConnection disconnected"); Instance = null; diff --git a/Reactor/Patches/Fixes/CoFindGamePatch.cs b/Reactor/Patches/Fixes/CoFindGamePatch.cs index 46b97e4..58ed2af 100644 --- a/Reactor/Patches/Fixes/CoFindGamePatch.cs +++ b/Reactor/Patches/Fixes/CoFindGamePatch.cs @@ -1,13 +1,20 @@ +using System.Reflection; using HarmonyLib; +using Reactor.Utilities; namespace Reactor.Patches.Fixes; /// /// Fixes Game Lists not working on servers using legacy matchmaking. /// -[HarmonyPatch(typeof(AmongUsClient_CoFindGame), nameof(AmongUsClient_CoFindGame.MoveNext))] +[HarmonyPatch] internal static class CoFindGamePatch { + public static MethodBase TargetMethod() + { + return StateMachineWrapper.GetStateMachineMoveNext(nameof(AmongUsClient.CoFindGame))!; + } + public static void Prefix() { if (AmongUsClient.Instance.LastDisconnectReason == DisconnectReasons.Unknown) diff --git a/Reactor/Patches/Miscellaneous/CustomServersPatch.cs b/Reactor/Patches/Miscellaneous/CustomServersPatch.cs index 3e10387..0f328dc 100644 --- a/Reactor/Patches/Miscellaneous/CustomServersPatch.cs +++ b/Reactor/Patches/Miscellaneous/CustomServersPatch.cs @@ -1,10 +1,13 @@ using System; +using System.Collections.Generic; using System.Linq; +using System.Reflection; using HarmonyLib; +using Il2CppInterop.Runtime.InteropTypes; +using Reactor.Utilities; namespace Reactor.Patches.Miscellaneous; -[HarmonyPatch] internal static class CustomServersPatch { private static bool IsCurrentServerOfficial() @@ -16,32 +19,55 @@ private static bool IsCurrentServerOfficial() regionInfo.Servers.All(serverInfo => serverInfo.Ip.EndsWith(Domain, StringComparison.Ordinal)); } - [HarmonyPatch(typeof(AuthManager._CoConnect_d__4), nameof(AuthManager._CoConnect_d__4.MoveNext))] - [HarmonyPatch(typeof(AuthManager._CoWaitForNonce_d__6), nameof(AuthManager._CoWaitForNonce_d__6.MoveNext))] - [HarmonyPrefix] - public static bool DisableAuthServer(ref bool __result) + [HarmonyPatch] + public static class DisableAuthServerPatch { - if (IsCurrentServerOfficial()) + public static IEnumerable TargetMethods() => + [ + StateMachineWrapper.GetStateMachineMoveNext(nameof(AuthManager.CoConnect))!, + StateMachineWrapper.GetStateMachineMoveNext(nameof(AuthManager.CoWaitForNonce))! + ]; + + public static bool Prefix(ref bool __result) { - return true; - } + if (IsCurrentServerOfficial()) + { + return true; + } - __result = false; - return false; + __result = false; + return false; + } } - [HarmonyPatch(typeof(AmongUsClient._CoJoinOnlinePublicGame_d__49), nameof(AmongUsClient._CoJoinOnlinePublicGame_d__49.MoveNext))] - [HarmonyPrefix] - public static void EnableUdpMatchmaking(AmongUsClient._CoJoinOnlinePublicGame_d__49 __instance) + [HarmonyPatch] + public static class EnableUdpPatch { - // Skip to state 1 which just calls CoJoinOnlineGameDirect - if (__instance.__1__state == 0 && !ServerManager.Instance.IsHttp) + public static MethodBase TargetMethod() + { + return StateMachineWrapper.GetStateMachineMoveNext(nameof(AmongUsClient.CoJoinOnlinePublicGame))!; + } + + public static void Prefix(Il2CppObjectBase __instance) { - __instance.__1__state = 1; - __instance.__8__1 = new AmongUsClient.__c__DisplayClass49_0 + var stateMachine = new StateMachineWrapper(__instance); + + // Skip to state 1 which just calls CoJoinOnlineGameDirect + if (stateMachine.State == 0 && !ServerManager.Instance.IsHttp) { - matchmakerToken = string.Empty, - }; + stateMachine.State = 1; + var lambdaType = stateMachine.GetParameter("__8__1").GetType(); + var newDisplayClass = Activator.CreateInstance(lambdaType); + if (newDisplayClass == null) + { + throw new InvalidOperationException($"Could not create display class of type '{lambdaType}'."); + } + + var displayClass = new CompilerGeneratedObjectWrapper(newDisplayClass); + displayClass.SetField("matchmakerToken", string.Empty); + + stateMachine.SetParameter("__8__1", newDisplayClass); + } } } } From 71983a9acc5b03dc08a083acbe3cc56114f41f9c Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Thu, 15 Jan 2026 15:31:32 -0500 Subject: [PATCH 3/4] Fix CoHandleSpawnPatch --- Reactor/Networking/Patches/ClientPatches.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Reactor/Networking/Patches/ClientPatches.cs b/Reactor/Networking/Patches/ClientPatches.cs index 1c8dbaf..c03e805 100644 --- a/Reactor/Networking/Patches/ClientPatches.cs +++ b/Reactor/Networking/Patches/ClientPatches.cs @@ -241,7 +241,7 @@ public static void Postfix(Il2CppObjectBase __instance, bool __result) if (ReactorConnection.Instance!.Syncer != Syncer.Host) return; var wrapper = new StateMachineWrapper(__instance); - var ownerId = wrapper.GetParameter("ownerId"); + var ownerId = wrapper.GetParameter("_ownerId_5__2"); if (!__result && !AmongUsClient.Instance.AmHost && ownerId == AmongUsClient.Instance.ClientId) { From 44cb0661f144b7ad970067c3b3fdb96899f61151 Mon Sep 17 00:00:00 2001 From: XtraCube <72575280+XtraCube@users.noreply.github.com> Date: Wed, 21 Jan 2026 08:50:06 -0500 Subject: [PATCH 4/4] Use typed delegates for faster invoke --- .../CompilerGeneratedObjectWrapper.cs | 61 +++++++++++++++---- 1 file changed, 49 insertions(+), 12 deletions(-) diff --git a/Reactor/Utilities/CompilerGeneratedObjectWrapper.cs b/Reactor/Utilities/CompilerGeneratedObjectWrapper.cs index 000166d..99e3610 100644 --- a/Reactor/Utilities/CompilerGeneratedObjectWrapper.cs +++ b/Reactor/Utilities/CompilerGeneratedObjectWrapper.cs @@ -25,6 +25,16 @@ public class CompilerGeneratedObjectWrapper /// protected Dictionary PropertyCache { get; } + /// + /// Gets the getter cache for faster property access. + /// + protected Dictionary GetterCache { get; } + + /// + /// Gets the setter cache for faster property access. + /// + protected Dictionary SetterCache { get; } + /// /// Initializes a new instance of the class. /// @@ -35,6 +45,31 @@ public CompilerGeneratedObjectWrapper(object generatedObject) GeneratedType = generatedObject.GetType(); PropertyCache = []; + GetterCache = []; + SetterCache = []; + } + + public PropertyInfo CacheProperty(string fieldName) + { + var propertyInfo = AccessTools.Property(GeneratedType, fieldName) + ?? throw new MissingMemberException( + $"Could not find field '{fieldName}' in type '{GeneratedType}'."); + + if (propertyInfo.PropertyType != typeof(T)) + { + throw new InvalidCastException( + $"Field '{fieldName}' is of type '{propertyInfo.PropertyType}', not '{typeof(T)}'."); + } + + PropertyCache[fieldName] = propertyInfo; + + var funcType = typeof(Func); + GetterCache[fieldName] = propertyInfo.GetMethod!.CreateDelegate(funcType, GeneratedObject); + + var actionType = typeof(Action); + SetterCache[fieldName] = propertyInfo.SetMethod!.CreateDelegate(actionType, GeneratedObject); + + return propertyInfo; } /// @@ -48,12 +83,12 @@ public TField GetField(string fieldName) { if (!PropertyCache.TryGetValue(fieldName, out var propertyInfo)) { - propertyInfo = AccessTools.Property(GeneratedType, fieldName); - if (propertyInfo == null) - { - throw new MissingMemberException($"Could not find field '{fieldName}' in type '{GeneratedType}'."); - } - PropertyCache[fieldName] = propertyInfo; + propertyInfo = CacheProperty(fieldName); + } + + if (GetterCache.TryGetValue(fieldName, out var getter)) + { + return ((Func) getter)(); } return (TField) propertyInfo.GetValue(GeneratedObject)!; @@ -70,14 +105,16 @@ public void SetField(string fieldName, TField value) { if (!PropertyCache.TryGetValue(fieldName, out var propertyInfo)) { - propertyInfo = AccessTools.Property(GeneratedType, fieldName); - if (propertyInfo == null) - { - throw new MissingMemberException($"Could not find field '{fieldName}' in type '{GeneratedType}'."); - } - PropertyCache[fieldName] = propertyInfo; + propertyInfo = CacheProperty(fieldName); + } + + if (SetterCache.TryGetValue(fieldName, out var setter)) + { + ((Action) setter)(value); + return; } propertyInfo.SetValue(GeneratedObject, value); } } +