diff --git a/AmongUs.props b/AmongUs.props index 926417c..5c84ddf 100644 --- a/AmongUs.props +++ b/AmongUs.props @@ -1,11 +1,20 @@ - + 2020.12.9s - NuclearPowered/Mappings:0.1.0-alpha.2 + $(DefineConstants);STEAM + + + + 2020.11.17i + $(DefineConstants);ITCH + + + + NuclearPowered/Mappings:0.1.0 - + diff --git a/Reactor.Debugger/Reactor.Debugger.csproj b/Reactor.Debugger/Reactor.Debugger.csproj index bc0b229..6d8ccc8 100644 --- a/Reactor.Debugger/Reactor.Debugger.csproj +++ b/Reactor.Debugger/Reactor.Debugger.csproj @@ -1,11 +1,11 @@ netstandard2.1 + latest 0.1.0 Debugging mod for Reactor NuclearPowered - latest diff --git a/Reactor.Example/ExamplePlugin.cs b/Reactor.Example/ExamplePlugin.cs new file mode 100644 index 0000000..1408598 --- /dev/null +++ b/Reactor.Example/ExamplePlugin.cs @@ -0,0 +1,46 @@ +using System; +using BepInEx; +using BepInEx.IL2CPP; +using Reactor.Extensions; +using UnhollowerBaseLib.Attributes; +using UnityEngine; + +namespace Reactor.Example +{ + [BepInPlugin(Id)] + [BepInProcess("Among Us.exe")] + [BepInDependency(ReactorPlugin.Id)] + public class ExamplePlugin : BasePlugin + { + public const string Id = "gg.reactor.Example"; + + public override void Load() + { + RegisterInIl2CppAttribute.Register(); + RegisterCustomRpcAttribute.Register(this); + + var gameObject = new GameObject(nameof(ReactorPlugin)).DontDestroy(); + gameObject.AddComponent().Plugin = this; + } + + [RegisterInIl2Cpp] + public class ExampleComponent : MonoBehaviour + { + [HideFromIl2Cpp] + public ExamplePlugin Plugin { get; internal set; } + + public ExampleComponent(IntPtr ptr) : base(ptr) + { + } + + private void Update() + { + if (Input.GetKeyDown(KeyCode.F3)) + { + Plugin.Log.LogWarning("Sending example rpc"); + PlayerControl.LocalPlayer.Send(new ExampleRpc.Data("Cześć :)")); + } + } + } + } +} diff --git a/Reactor.Example/ExampleRpc.cs b/Reactor.Example/ExampleRpc.cs new file mode 100644 index 0000000..027d8f9 --- /dev/null +++ b/Reactor.Example/ExampleRpc.cs @@ -0,0 +1,39 @@ +using Hazel; + +namespace Reactor.Example +{ + [RegisterCustomRpc] + public class ExampleRpc : CustomRpc + { + public ExampleRpc(ExamplePlugin plugin) : base(plugin) + { + } + + public readonly struct Data + { + public readonly string Message; + + public Data(string message) + { + Message = message; + } + } + + public override RpcLocalHandling LocalHandling => RpcLocalHandling.Before; + + public override void Write(MessageWriter writer, Data data) + { + writer.Write(data.Message); + } + + public override Data Read(MessageReader reader) + { + return new Data(reader.ReadString()); + } + + public override void Handle(PlayerControl innerNetObject, Data data) + { + Plugin.Log.LogWarning($"{innerNetObject.Data.PlayerId} sent \"{data.Message}\""); + } + } +} diff --git a/Reactor.Example/Reactor.Example.csproj b/Reactor.Example/Reactor.Example.csproj new file mode 100644 index 0000000..ddd980b --- /dev/null +++ b/Reactor.Example/Reactor.Example.csproj @@ -0,0 +1,16 @@ + + + netstandard2.1 + latest + + 0.1.0 + Example mod using Reactor + NuclearPowered + + + + + + + + diff --git a/Reactor.sln b/Reactor.sln index 4465468..3cada7a 100644 --- a/Reactor.sln +++ b/Reactor.sln @@ -7,6 +7,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Build", "build\Build.csproj EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{E2F4DAFA-E98C-4A7D-BA47-AAB0271A769D}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Reactor.Example", "Reactor.Example\Reactor.Example.csproj", "{26A83466-26FE-4383-A35C-B8629902F909}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -22,9 +24,11 @@ Global {68589B4D-9EF5-40DD-8D6D-10977ED07259}.Release|Any CPU.ActiveCfg = Release|Any CPU {68589B4D-9EF5-40DD-8D6D-10977ED07259}.Release|Any CPU.Build.0 = Release|Any CPU {58AC4924-AC3D-45EA-80D7-E9F860FD1DD2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {58AC4924-AC3D-45EA-80D7-E9F860FD1DD2}.Debug|Any CPU.Build.0 = Debug|Any CPU {58AC4924-AC3D-45EA-80D7-E9F860FD1DD2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {58AC4924-AC3D-45EA-80D7-E9F860FD1DD2}.Release|Any CPU.Build.0 = Release|Any CPU + {26A83466-26FE-4383-A35C-B8629902F909}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {26A83466-26FE-4383-A35C-B8629902F909}.Debug|Any CPU.Build.0 = Debug|Any CPU + {26A83466-26FE-4383-A35C-B8629902F909}.Release|Any CPU.ActiveCfg = Release|Any CPU + {26A83466-26FE-4383-A35C-B8629902F909}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {58AC4924-AC3D-45EA-80D7-E9F860FD1DD2} = {E2F4DAFA-E98C-4A7D-BA47-AAB0271A769D} diff --git a/Reactor/CustomRpc.cs b/Reactor/CustomRpc.cs new file mode 100644 index 0000000..4958561 --- /dev/null +++ b/Reactor/CustomRpc.cs @@ -0,0 +1,223 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using BepInEx; +using BepInEx.IL2CPP; +using HarmonyLib; +using Hazel; +using Reactor.Net; + +namespace Reactor +{ + public enum RpcLocalHandling + { + None, + Before, + After + } + + public abstract class UnsafeCustomRpc + { + public int? Id { get; internal set; } + + public BasePlugin UnsafePlugin { get; } + public string PluginId { get; } + + public abstract Type InnerNetObjectType { get; } + + public abstract RpcLocalHandling LocalHandling { get; } + + protected UnsafeCustomRpc(BasePlugin plugin) + { + UnsafePlugin = plugin; + PluginId = MetadataHelper.GetMetadata(plugin).GUID; + } + + public abstract void UnsafeWrite(MessageWriter writer, object data); + public abstract object UnsafeRead(MessageReader reader); + public abstract void UnsafeHandle(InnerNetObject innerNetObject, object data); + } + + public abstract class CustomRpc : UnsafeCustomRpc where TPlugin : BasePlugin where TInnerNetObject : InnerNetObject + { + protected CustomRpc(TPlugin plugin) : base(plugin) + { + } + + public TPlugin Plugin => (TPlugin) UnsafePlugin; + public override Type InnerNetObjectType => typeof(TInnerNetObject); + + public abstract void Write(MessageWriter writer, TData data); + public abstract TData Read(MessageReader reader); + public abstract void Handle(TInnerNetObject innerNetObject, TData data); + + public override void UnsafeWrite(MessageWriter writer, object data) + { + Write(writer, (TData) data); + } + + public override object UnsafeRead(MessageReader reader) + { + return Read(reader); + } + + public override void UnsafeHandle(InnerNetObject innerNetObject, object data) + { + Handle((TInnerNetObject) innerNetObject, (TData) data); + } + } + + public class CustomRpcManager + { + public const byte CallId = byte.MaxValue; + + private readonly List _list = new List(); + private readonly Dictionary> _map = new Dictionary>(); + + public IReadOnlyList List => _list.AsReadOnly(); + + public ILookup Map => + _map.SelectMany(pair => pair.Value, (pair, value) => new { pair.Key, Value = value }) + .ToLookup(pair => pair.Key, pair => pair.Value); + + public Dictionary PluginIdMap { get; private set; } + public Dictionary PluginIdMapReversed { get; private set; } + + public CustomRpcManager() + { + foreach (var type in HandleRpcPatch.InnerNetObjectTypes) + { + _map[type] = new List(); + } + } + + public void ReloadPluginIdMap() + { + PluginIdMap = new Dictionary(); + PluginIdMapReversed = new Dictionary(); + + var i = -1; + + foreach (var mod in ModList.GetCurrent().OrderBy(x => x.Id)) + { + if (mod.Side == PluginSide.Both) + { + PluginIdMap[mod.Id] = i; + PluginIdMapReversed[i] = mod.Id; + i--; + } + } + } + + public UnsafeCustomRpc Register(UnsafeCustomRpc customRpc) + { + customRpc.Id = List.Count(x => x.UnsafePlugin == customRpc.UnsafePlugin); + _list.Add(customRpc); + _map[customRpc.InnerNetObjectType].Add(customRpc); + + return customRpc; + } + + public void Send(InnerNetObject netObject, object data, bool immediately = false) + { + UnsafeSend(netObject, List.Single(x => x.GetType() == typeof(TCustomRpc)), data, immediately); + } + + public void UnsafeSend(InnerNetObject netObject, UnsafeCustomRpc customRpc, object data, bool immediately = false) + { + if (netObject == null) throw new ArgumentNullException(nameof(netObject)); + if (customRpc == null) throw new ArgumentNullException(nameof(customRpc)); + if (data == null) throw new ArgumentNullException(nameof(data)); + + if (customRpc.Id == null) + { + throw new InvalidOperationException("Can't send unregistered CustomRpc"); + } + + if (customRpc.LocalHandling == RpcLocalHandling.Before) + { + customRpc.UnsafeHandle(netObject, data); + } + + var writer = immediately switch + { + false => AmongUsClient.Instance.StartRpc(netObject.NetId, CallId, SendOption.Reliable), + true => AmongUsClient.Instance.StartRpcImmediately(netObject.NetId, CallId, SendOption.Reliable, -1) + }; + + if (PluginIdMap.TryGetValue(customRpc.PluginId, out var pluginId)) + { + writer.WritePacked(pluginId); + } + else + { + writer.Write(customRpc.PluginId); + } + + writer.WritePacked(customRpc.Id!.Value); + customRpc.UnsafeWrite(writer, data); + + if (immediately) + { + AmongUsClient.Instance.FinishRpcImmediately(writer); + } + else + { + writer.EndMessage(); + } + + if (customRpc.LocalHandling == RpcLocalHandling.After) + { + customRpc.UnsafeHandle(netObject, data); + } + } + + [HarmonyPatch] + private static class HandleRpcPatch + { + internal static List InnerNetObjectTypes { get; } = typeof(InnerNetObject).Assembly.GetTypes().Where(x => x.IsSubclassOf(typeof(InnerNetObject)) && x != typeof(LobbyBehaviour)).ToList(); + + public static IEnumerable TargetMethods() + { + return InnerNetObjectTypes.Select(x => x.GetMethod(nameof(InnerNetObject.HandleRpc), AccessTools.all)); + } + + private static string ReadString(MessageReader reader, int len) + { + if (reader.BytesRemaining < len) throw new InvalidDataException($"Read length is longer than message length: {len} of {reader.BytesRemaining}"); + + var output = Encoding.UTF8.GetString(reader.Buffer, reader.readHead, len); + reader.Position += len; + return output; + } + + public static bool Prefix(InnerNetObject __instance, [HarmonyArgument(0)] byte callId, [HarmonyArgument(1)] MessageReader reader) + { + if (callId == CallId) + { + var manager = PluginSingleton.Instance.CustomRpcManager; + var customRpcs = manager.Map[__instance.GetType()]; + + var lengthOrShortId = reader.ReadPackedInt32(); + + var pluginId = lengthOrShortId < 0 + ? manager.PluginIdMapReversed[lengthOrShortId] + : ReadString(reader, lengthOrShortId); + + var id = reader.ReadPackedInt32(); + + var customRpc = customRpcs.Single(x => x.PluginId == pluginId && x.Id == id); + + customRpc.UnsafeHandle(__instance, customRpc.UnsafeRead(reader)); + + return false; + } + + return true; + } + } + } +} diff --git a/Reactor/Extensions/Extensions.cs b/Reactor/Extensions/Extensions.cs index be0b839..2011fed 100644 --- a/Reactor/Extensions/Extensions.cs +++ b/Reactor/Extensions/Extensions.cs @@ -129,5 +129,10 @@ namespace Reactor.Extensions { return type.GetMethods(AccessTools.all).Where(x => x.ReturnType == returnType && x.GetParameters().Select(x => x.ParameterType).SequenceEqual(parameterTypes)); } + + public static void Send(this InnerNetObject netObject, object data, bool immediately = false) + { + PluginSingleton.Instance.CustomRpcManager.Send(netObject, data, immediately); + } } } diff --git a/Reactor/Net/Mod.cs b/Reactor/Net/Mod.cs new file mode 100644 index 0000000..f6870dd --- /dev/null +++ b/Reactor/Net/Mod.cs @@ -0,0 +1,21 @@ +namespace Reactor.Net +{ + public readonly struct Mod + { + public readonly string Id; + public readonly string Version; + public readonly PluginSide Side; + + public Mod(string id, string version, PluginSide side) + { + Id = id; + Version = version; + Side = side; + } + + public override string ToString() + { + return $"{Id} ({Version})"; + } + } +} diff --git a/Reactor/Net/ModList.cs b/Reactor/Net/ModList.cs new file mode 100644 index 0000000..ebcaafd --- /dev/null +++ b/Reactor/Net/ModList.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using BepInEx.IL2CPP; +using Hazel; + +namespace Reactor.Net +{ + public static class ModList + { + public static void Serialize(MessageWriter writer, ISet mods) + { + writer.WritePacked(mods.Count); + + foreach (var mod in mods) + { + writer.Write(mod.Id); + writer.Write(mod.Version); + writer.Write((byte) mod.Side); + } + } + + public static ISet GetCurrent() + { + return IL2CPPChainloader.Instance.Plugins + .Select(plugin => new Mod( + plugin.Key, + plugin.Value.Metadata.Version.ToString(), + plugin.Value.Instance.GetType().GetCustomAttribute()?.Side ?? PluginSide.Both + )) + .ToHashSet(); + } + } +} diff --git a/Reactor/Net/ModdedHandshakeC2S.cs b/Reactor/Net/ModdedHandshakeC2S.cs new file mode 100644 index 0000000..d071ed3 --- /dev/null +++ b/Reactor/Net/ModdedHandshakeC2S.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using Hazel; +using UnhollowerBaseLib; + +namespace Reactor.Net +{ + public static class ModdedHandshakeC2S + { + public static void Serialize(MessageWriter writer, Il2CppStructArray clientVersion, string name, ISet mods = null) + { + writer.Write(clientVersion); + writer.Write(name); + + if (mods != null) + { + ModList.Serialize(writer, mods); + } + } + } +} diff --git a/Reactor/Net/ModdedHandshakeS2C.cs b/Reactor/Net/ModdedHandshakeS2C.cs new file mode 100644 index 0000000..63b9d9b --- /dev/null +++ b/Reactor/Net/ModdedHandshakeS2C.cs @@ -0,0 +1,14 @@ +using Hazel; + +namespace Reactor.Net +{ + public static class ModdedHandshakeS2C + { + public static void Serialize(MessageWriter writer, string serverBrand) + { + writer.StartMessage(byte.MaxValue); + writer.Write(serverBrand); + writer.EndMessage(); + } + } +} diff --git a/Reactor/Patches/ModdedHandshake.cs b/Reactor/Patches/ModdedHandshake.cs deleted file mode 100644 index 2094daf..0000000 --- a/Reactor/Patches/ModdedHandshake.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System.Linq; -using System.Reflection; -using BepInEx.IL2CPP; -using HarmonyLib; -using Hazel; -using UnhollowerBaseLib; - -namespace Reactor.Patches -{ - internal static class ModdedHandshake - { - // TODO add back when vent issue is fixed - // [HarmonyPatch(typeof(InnerNetServer), nameof(InnerNetServer.OnServerConnect))] - // public static class FreeplayPatch - // { - // public static void Prefix([HarmonyArgument(0)] NewConnectionEventArgs evt) - // { - // var handshakeData = evt.HandshakeData; - // - // var protocolVersion = handshakeData.ReadInt32(); - // if (protocolVersion != -1) - // { - // // reset position if vanilla - // handshakeData._position -= sizeof(int); - // handshakeData.readHead -= sizeof(int); - // } - // } - // } - - // workaround for ^ - [HarmonyPatch(typeof(InnerNetServer), nameof(InnerNetServer.IsCompatibleVersion))] - public static class FreeplayPatch - { - public static bool Prefix([HarmonyArgument(0)] int version, ref bool __result) - { - if (version == -1) - { - __result = true; - return false; - } - - return true; - } - } - - [HarmonyPatch(typeof(AmongUsClient), nameof(AmongUsClient.GetConnectionData))] - public static class HandshakePatch - { - public static bool Prefix(out Il2CppStructArray __result) - { - var handshake = MessageWriter.Get(SendOption.Reliable); - - var plugins = IL2CPPChainloader.Instance.Plugins; - - if (plugins.Values - .Select(pluginInfo => pluginInfo.Instance.GetType().GetCustomAttribute()) - .Any(attribute => attribute == null || attribute.Side == PluginSide.Both)) - { - handshake.Write(-1); - } - - handshake.Write(Constants.GetBroadcastVersion()); - handshake.Write(SaveManager.PlayerName); - - handshake.WritePacked(plugins.Count); - - foreach (var plugin in plugins) - { - handshake.Write(plugin.Key); - handshake.Write(plugin.Value.Metadata.Version.ToString()); - } - - __result = handshake.ToByteArray(false); - handshake.Recycle(); - - return false; - } - } - } -} diff --git a/Reactor/Patches/ModdedHandshakePatches.cs b/Reactor/Patches/ModdedHandshakePatches.cs new file mode 100644 index 0000000..b90430d --- /dev/null +++ b/Reactor/Patches/ModdedHandshakePatches.cs @@ -0,0 +1,127 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Reflection; +using HarmonyLib; +using Hazel; +using Hazel.Udp; +using Reactor.Extensions; +using Reactor.Net; +using UnhollowerBaseLib; +using UnityEngine; + +namespace Reactor.Patches +{ + internal static class ModdedHandshakePatches + { + // [HarmonyPatch(typeof(InnerNetServer), nameof(InnerNetServer.OnServerConnect))] + [HarmonyPatch] + public static class ServerPatch + { + public static IEnumerable TargetMethods() + { + return typeof(InnerNetServer).GetMethods(typeof(void), typeof(NewConnectionEventArgs)); + } + + private static readonly string ServerBrand = $"Among Us {Application.version}"; + + public static void Postfix([HarmonyArgument(0)] NewConnectionEventArgs evt) + { + var writer = MessageWriter.Get(SendOption.Reliable); + ModdedHandshakeS2C.Serialize(writer, ServerBrand); + evt.Connection.Send(writer); + writer.Recycle(); + } + } + + // [HarmonyPatch(typeof(InnerNetClient), nameof(InnerNetClient.HandleMessage))] + [HarmonyPatch] + public static class HandleMessagePatch + { + public static IEnumerable TargetMethods() + { + return typeof(InnerNetClient).GetMethods(typeof(void), typeof(MessageReader), typeof(SendOption)); + } + + public static bool Prefix(InnerNetClient __instance, [HarmonyArgument(0)] MessageReader reader, [HarmonyArgument(1)] SendOption sendOption) + { + if (reader.Tag == byte.MaxValue) + { + var serverBrand = reader.ReadString(); + + __instance.connection.State = ConnectionState.Connected; + + PluginSingleton.Instance.Log.LogDebug($"Connected to modded server ({serverBrand})"); + + return false; + } + + return true; + } + } + + [HarmonyPatch(typeof(UdpConnection), nameof(UdpConnection.HandleSend))] + public static class SendHelloPatch + { + public static void Prefix(UdpConnection __instance, byte sendOption, ref Il2CppSystem.Action ackCallback) + { + if (sendOption != 8 || AmongUsClient.Instance == null || AmongUsClient.Instance.connection == null || !AmongUsClient.Instance.connection.Equals(__instance)) + { + return; + } + + if (!PluginSingleton.Instance.ModdedHandshake.Value) + { + return; + } + + ackCallback = (Action) (() => + { + PluginSingleton.Instance.Log.LogDebug("Hello was acked, waiting for modded handshake response"); + + Coroutines.Start(Coroutine()); + + __instance.add_Disconnected((Action) ((_, _) => + { + Coroutines.Stop(Coroutine()); + })); + }); + } + } + + private static IEnumerator Coroutine() + { + yield return new WaitForSeconds(3); + + var client = AmongUsClient.Instance; + + if (client != null && client.connection != null && client.connection.State == ConnectionState.Connecting) + { + client.LastDisconnectReason = DisconnectReasons.Custom; + client.LastCustomDisconnect = "Server didn't respond to modded handshake"; + client.HandleDisconnect(client.LastDisconnectReason, client.LastCustomDisconnect); + } + } + + [HarmonyPatch(typeof(AmongUsClient), nameof(AmongUsClient.GetConnectionData))] + public static class HandshakePatch + { + public static bool Prefix(out Il2CppStructArray __result) + { + var handshake = MessageWriter.Get(SendOption.Reliable); + + ModdedHandshakeC2S.Serialize( + handshake, + Constants.GetBroadcastVersionBytes(), + SaveManager.PlayerName, + ModList.GetCurrent() + ); + + __result = handshake.ToByteArray(false); + handshake.Recycle(); + + return false; + } + } + } +} diff --git a/Reactor/Patches/SteamPatch.cs b/Reactor/Patches/SteamPatch.cs index 1c4532b..133ff7f 100644 --- a/Reactor/Patches/SteamPatch.cs +++ b/Reactor/Patches/SteamPatch.cs @@ -1,3 +1,4 @@ +#if STEAM using System.IO; using HarmonyLib; using Steamworks; @@ -23,3 +24,4 @@ namespace Reactor.Patches } } } +#endif diff --git a/Reactor/ReactorPlugin.cs b/Reactor/ReactorPlugin.cs index afb52df..1e59ef7 100644 --- a/Reactor/ReactorPlugin.cs +++ b/Reactor/ReactorPlugin.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using BepInEx; +using BepInEx.Configuration; using BepInEx.IL2CPP; using HarmonyLib; using Reactor.Extensions; @@ -19,12 +20,18 @@ namespace Reactor public const string Id = "gg.reactor.api"; public Harmony Harmony { get; } = new Harmony(Id); + public CustomRpcManager CustomRpcManager { get; } = new CustomRpcManager(); + + public ConfigEntry ModdedHandshake; + private RegionInfoWatcher RegionInfoWatcher { get; } = new RegionInfoWatcher(); public override void Load() { RegisterInIl2CppAttribute.Register(); + ModdedHandshake = Config.Bind("Features", "Modded handshake", true); + var gameObject = new GameObject(nameof(ReactorPlugin)).DontDestroy(); gameObject.AddComponent().Plugin = this; @@ -52,6 +59,8 @@ namespace Reactor public void Start() { + Plugin.CustomRpcManager.ReloadPluginIdMap(); + Camera.onPostRender = Camera.onPostRender == null ? new Action(OnPostRenderM) : Il2CppSystem.Delegate.Combine(Camera.onPostRender, Il2CppSystem.Delegate.CreateDelegate(GetIl2CppType(), GetIl2CppType().GetMethod(nameof(OnPostRenderM), Il2CppSystem.Reflection.BindingFlags.Static | Il2CppSystem.Reflection.BindingFlags.Public))).Cast(); diff --git a/Reactor/ReactorPluginSideAttribute.cs b/Reactor/ReactorPluginSideAttribute.cs index 0a23ce4..af32b5f 100644 --- a/Reactor/ReactorPluginSideAttribute.cs +++ b/Reactor/ReactorPluginSideAttribute.cs @@ -24,7 +24,7 @@ namespace Reactor /// /// Plugin side used in modded handshake /// - public enum PluginSide + public enum PluginSide : byte { /// /// Required by both sides, reject connection if missing on the other side diff --git a/Reactor/RegisterCustomRpcAttribute.cs b/Reactor/RegisterCustomRpcAttribute.cs new file mode 100644 index 0000000..cec430e --- /dev/null +++ b/Reactor/RegisterCustomRpcAttribute.cs @@ -0,0 +1,38 @@ +using System; +using System.Reflection; +using BepInEx.IL2CPP; +using HarmonyLib; + +namespace Reactor +{ + /// + /// Utility attribute for automatically registering CustomRpc + /// + [AttributeUsage(AttributeTargets.Class)] + public class RegisterCustomRpcAttribute : Attribute + { + public static void Register(BasePlugin plugin) + { + Register(Assembly.GetCallingAssembly(), plugin); + } + + public static void Register(Assembly assembly, BasePlugin plugin) + { + foreach (var type in assembly.GetTypes()) + { + var attribute = type.GetCustomAttribute(); + + if (attribute != null) + { + if (!type.IsSubclassOf(typeof(UnsafeCustomRpc))) + { + throw new InvalidOperationException($"Type {type.FullDescription()} has {nameof(RegisterCustomRpcAttribute)} but doesn't extend {nameof(UnsafeCustomRpc)}."); + } + + var customRpc = (UnsafeCustomRpc) Activator.CreateInstance(type, plugin); + PluginSingleton.Instance.CustomRpcManager.Register(customRpc); + } + } + } + } +} diff --git a/build/Program.cs b/build/Program.cs index 4af3563..b4548e7 100644 --- a/build/Program.cs +++ b/build/Program.cs @@ -122,6 +122,7 @@ public sealed class BuildTask : FrostingTask Configuration = "Release", EnvironmentVariables = { + ["GamePlatform"] = "Steam", ["AmongUs"] = Path.GetFullPath(context.AmongUsPath) } });