From d01ea478f4a32458fcbbf3bb2a64900000960c3f Mon Sep 17 00:00:00 2001 From: js6pak Date: Tue, 30 Aug 2022 21:22:50 +0200 Subject: [PATCH] Protocol v3 --- Reactor.Debugger/DebuggerPlugin.cs | 1 - Reactor.Example/ExamplePlugin.cs | 1 + Reactor.Shared/ModFlags.cs | 24 ++ Reactor.Shared/Networking/Mod.cs | 61 ++- .../Networking/ReactorGameDataFlag.cs | 6 + .../Networking/ReactorMessageFlags.cs | 2 - .../Networking/ReactorProtocolVersion.cs | 13 +- Reactor.Shared/PluginSide.cs | 17 - Reactor/Networking/CustomRpcManager.cs | 50 +-- Reactor/Networking/Extensions.cs | 30 ++ Reactor/Networking/ModDeclaration.cs | 16 - Reactor/Networking/ModList.cs | 114 +++++- Reactor/Networking/ModdedHandshakeC2S.cs | 29 +- Reactor/Networking/Patches/ClientPatches.cs | 349 ++++++++++++------ Reactor/Networking/Patches/HttpPatches.cs | 119 ++++-- .../Networking/Patches/ReactorClientData.cs | 48 +++ .../Networking/Patches/ReactorConnection.cs | 37 ++ Reactor/Networking/Patches/ServerPatches.cs | 21 -- Reactor/Networking/ReactorHeader.cs | 30 ++ Reactor/Networking/UnsafeCustomRpc.cs | 7 +- Reactor/PluginSingleton.cs | 12 +- Reactor/Reactor.csproj | 4 - Reactor/ReactorModFlagsAttribute.cs | 33 ++ Reactor/ReactorPlugin.cs | 2 + Reactor/ReactorPluginSideAttribute.cs | 36 -- 25 files changed, 755 insertions(+), 307 deletions(-) create mode 100644 Reactor.Shared/ModFlags.cs create mode 100644 Reactor.Shared/Networking/ReactorGameDataFlag.cs delete mode 100644 Reactor.Shared/PluginSide.cs create mode 100644 Reactor/Networking/Extensions.cs delete mode 100644 Reactor/Networking/ModDeclaration.cs create mode 100644 Reactor/Networking/Patches/ReactorClientData.cs create mode 100644 Reactor/Networking/Patches/ReactorConnection.cs delete mode 100644 Reactor/Networking/Patches/ServerPatches.cs create mode 100644 Reactor/Networking/ReactorHeader.cs create mode 100644 Reactor/ReactorModFlagsAttribute.cs delete mode 100644 Reactor/ReactorPluginSideAttribute.cs diff --git a/Reactor.Debugger/DebuggerPlugin.cs b/Reactor.Debugger/DebuggerPlugin.cs index fc5526c..b601747 100644 --- a/Reactor.Debugger/DebuggerPlugin.cs +++ b/Reactor.Debugger/DebuggerPlugin.cs @@ -14,7 +14,6 @@ namespace Reactor.Debugger; [BepInAutoPlugin("gg.reactor.debugger")] [BepInProcess("Among Us.exe")] [BepInDependency(ReactorPlugin.Id)] -[ReactorPluginSide(PluginSide.ClientOnly)] public partial class DebuggerPlugin : BasePlugin { public Harmony Harmony { get; } = new(Id); diff --git a/Reactor.Example/ExamplePlugin.cs b/Reactor.Example/ExamplePlugin.cs index 0b53f18..3911819 100644 --- a/Reactor.Example/ExamplePlugin.cs +++ b/Reactor.Example/ExamplePlugin.cs @@ -13,6 +13,7 @@ namespace Reactor.Example; [BepInAutoPlugin("gg.reactor.Example")] [BepInProcess("Among Us.exe")] [BepInDependency(ReactorPlugin.Id)] +[ReactorModFlags(ModFlags.RequireOnAllClients)] public partial class ExamplePlugin : BasePlugin { public override void Load() diff --git a/Reactor.Shared/ModFlags.cs b/Reactor.Shared/ModFlags.cs new file mode 100644 index 0000000..d9f8c09 --- /dev/null +++ b/Reactor.Shared/ModFlags.cs @@ -0,0 +1,24 @@ +using System; + +namespace Reactor; + +[Flags] +public enum ModFlags : ushort +{ + None = 0, + + /// + /// Requires all clients in a lobby to have the mod. + /// + RequireOnAllClients = 1 << 0, + + /// + /// Requires the server to have a plugin that handles the mod. + /// + RequireOnServer = 1 << 1, + + /// + /// Requires the host of the lobby to have the mod. + /// + RequireOnHost = 1 << 2, +} diff --git a/Reactor.Shared/Networking/Mod.cs b/Reactor.Shared/Networking/Mod.cs index ef3a106..2532ab8 100644 --- a/Reactor.Shared/Networking/Mod.cs +++ b/Reactor.Shared/Networking/Mod.cs @@ -1,39 +1,78 @@ #nullable enable using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Text; namespace Reactor.Networking; -public class Mod +public struct Mod { - public Mod(uint netId, string id, string version, PluginSide side) + public Mod(string id, string version, ModFlags flags, string? name = null) { - NetId = netId; Id = id; Version = version; - Side = side; + Flags = flags; + Name = name; } - public uint NetId { get; } public string Id { get; } public string Version { get; } - public PluginSide Side { get; } + public ModFlags Flags { get; } + public string? Name { get; } - protected bool Equals(Mod other) + public bool IsRequiredOnAllClients => (Flags & ModFlags.RequireOnAllClients) != 0; + + public bool Equals(Mod other) { return Id == other.Id && Version == other.Version; } public override bool Equals(object? obj) { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != GetType()) return false; - return Equals((Mod) obj); + return obj is Mod other && Equals(other); } public override int GetHashCode() { return HashCode.Combine(Id, Version); } + + public override string ToString() + { + return $"{Name ?? Id} ({Version})"; + } + + internal static bool Validate(IReadOnlyCollection clientMods, IReadOnlyCollection hostMods, [NotNullWhen(false)] out string? reason) + { + var clientMissing = hostMods.Where(mod => mod.IsRequiredOnAllClients && !clientMods.Contains(mod)).ToArray(); + var hostMissing = clientMods.Where(mod => mod.IsRequiredOnAllClients && !hostMods.Contains(mod)).ToArray(); + + if (clientMissing.Any() || hostMissing.Any()) + { + var message = new StringBuilder(); + + if (clientMissing.Any()) + { + message.Append("You are missing: "); + message.AppendJoin(", ", clientMissing.Select(x => x.ToString())); + message.AppendLine(); + } + + if (hostMissing.Any()) + { + message.Append("Host is missing: "); + message.AppendJoin(", ", hostMissing.Select(x => x.ToString())); + message.AppendLine(); + } + + reason = message.ToString(); + return false; + } + + reason = null; + return true; + } } diff --git a/Reactor.Shared/Networking/ReactorGameDataFlag.cs b/Reactor.Shared/Networking/ReactorGameDataFlag.cs new file mode 100644 index 0000000..1851fd7 --- /dev/null +++ b/Reactor.Shared/Networking/ReactorGameDataFlag.cs @@ -0,0 +1,6 @@ +namespace Reactor.Networking; + +public enum ReactorGameDataFlag : byte +{ + KickWithReason, +} diff --git a/Reactor.Shared/Networking/ReactorMessageFlags.cs b/Reactor.Shared/Networking/ReactorMessageFlags.cs index 372971e..36d5f89 100644 --- a/Reactor.Shared/Networking/ReactorMessageFlags.cs +++ b/Reactor.Shared/Networking/ReactorMessageFlags.cs @@ -3,6 +3,4 @@ namespace Reactor.Networking; public enum ReactorMessageFlags : byte { Handshake, - ModDeclaration, - PluginDeclaration, } diff --git a/Reactor.Shared/Networking/ReactorProtocolVersion.cs b/Reactor.Shared/Networking/ReactorProtocolVersion.cs index cca0ad4..78bfd7e 100644 --- a/Reactor.Shared/Networking/ReactorProtocolVersion.cs +++ b/Reactor.Shared/Networking/ReactorProtocolVersion.cs @@ -1,17 +1,22 @@ namespace Reactor.Networking; /// -/// Version of the Reactor.Net protocol format. +/// Version of the Reactor.Networking protocol format. /// public enum ReactorProtocolVersion : byte { /// - /// Initial version. + /// First public Reactor Protocol version. /// - Initial = 1, + V2 = 1, + + /// + /// Version introducing vanilla server support, syncer concept and registries. + /// + V3 = 2, /// /// Latest version. /// - Latest = Initial, + Latest = V3, } diff --git a/Reactor.Shared/PluginSide.cs b/Reactor.Shared/PluginSide.cs deleted file mode 100644 index f7fb1db..0000000 --- a/Reactor.Shared/PluginSide.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Reactor; - -/// -/// Plugin side used in modded handshake -/// -public enum PluginSide : byte -{ - /// - /// Required by both sides, reject connection if missing on the other side - /// - Both, - - /// - /// Required only by client - /// - ClientOnly, -} diff --git a/Reactor/Networking/CustomRpcManager.cs b/Reactor/Networking/CustomRpcManager.cs index 20aca5a..be76b7a 100644 --- a/Reactor/Networking/CustomRpcManager.cs +++ b/Reactor/Networking/CustomRpcManager.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Reflection; using HarmonyLib; using Hazel; +using Il2CppInterop.Generator.Extensions; using InnerNet; namespace Reactor.Networking; @@ -20,32 +21,17 @@ public class CustomRpcManager public const byte CallId = byte.MaxValue; private readonly List _list = new(); - private readonly Dictionary> _map = new(); + private readonly Dictionary>> _map = new(); 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 CustomRpcManager() - { - foreach (var type in HandleRpcPatch.InnerNetObjectTypes) - { - _map[type] = new List(); - } - } - public UnsafeCustomRpc Register(UnsafeCustomRpc customRpc) { - if (_list.Any(x => x.UnsafePlugin == customRpc.UnsafePlugin && x.Id == customRpc.Id)) - { - throw new ArgumentException("Rpc with that id was already registered"); - } - customRpc.Manager = this; _list.Add(customRpc); - _map[customRpc.InnerNetObjectType].Add(customRpc); + _map.GetOrCreate(customRpc.InnerNetObjectType, _ => new Dictionary>()) + .GetOrCreate(customRpc.Mod, _ => new Dictionary()) + .Add(customRpc.Id, customRpc); if (customRpc.IsSingleton) { @@ -67,24 +53,28 @@ public class CustomRpcManager 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()]; + return !HandleRpc(__instance, callId, reader); + } + } - var pluginNetId = reader.ReadPackedUInt32(); - var id = reader.ReadPackedUInt32(); + public static bool HandleRpc(InnerNetObject innerNetObject, byte callId, MessageReader reader) + { + if (callId == CallId) + { + var manager = PluginSingleton.Instance.CustomRpcManager; + var customRpcs = manager._map[innerNetObject.GetType()]; - var pluginId = ModList.GetByNetId(pluginNetId).Id; - var customRpc = customRpcs.Single(x => x.PluginId == pluginId && x.Id == id); + var mod = reader.ReadMod(); + var id = reader.ReadPackedUInt32(); - customRpc.UnsafeHandle(__instance, customRpc.UnsafeRead(reader.ReadMessage())); + var customRpc = customRpcs[mod][id]; - return false; - } + customRpc.UnsafeHandle(innerNetObject, customRpc.UnsafeRead(reader.ReadMessage())); return true; } + + return false; } } diff --git a/Reactor/Networking/Extensions.cs b/Reactor/Networking/Extensions.cs new file mode 100644 index 0000000..9e44ecd --- /dev/null +++ b/Reactor/Networking/Extensions.cs @@ -0,0 +1,30 @@ +using Hazel; +using InnerNet; + +namespace Reactor.Networking; + +public static class Extensions +{ + public static void KickWithReason(this InnerNetClient innerNetClient, int targetClientId, string reason) + { + var writer = MessageWriter.Get(SendOption.Reliable); + writer.StartMessage(Tags.GameDataTo); + writer.Write(innerNetClient.GameId); + writer.WritePacked(targetClientId); + { + writer.StartMessage(byte.MaxValue); + writer.Write((byte) ReactorGameDataFlag.KickWithReason); + writer.Write(reason); + writer.EndMessage(); + } + writer.EndMessage(); + innerNetClient.SendOrDisconnect(writer); + writer.Recycle(); + } + + public static void DisconnectWithReason(this InnerNetClient innerNetClient, string reason) + { + innerNetClient.LastCustomDisconnect = reason; + innerNetClient.HandleDisconnect(innerNetClient.LastDisconnectReason = DisconnectReasons.Custom, reason); + } +} diff --git a/Reactor/Networking/ModDeclaration.cs b/Reactor/Networking/ModDeclaration.cs deleted file mode 100644 index c2c81ec..0000000 --- a/Reactor/Networking/ModDeclaration.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Hazel; - -namespace Reactor.Networking; - -public static class ModDeclaration -{ - public static void Serialize(MessageWriter writer, Mod mod) - { - writer.StartMessage(byte.MaxValue); - writer.Write((byte) ReactorMessageFlags.ModDeclaration); - writer.WritePacked(mod.NetId); - writer.Write(mod.Id); - writer.Write(mod.Version); - writer.Write((byte) mod.Side); - } -} diff --git a/Reactor/Networking/ModList.cs b/Reactor/Networking/ModList.cs index 41cdcd4..c652dc9 100644 --- a/Reactor/Networking/ModList.cs +++ b/Reactor/Networking/ModList.cs @@ -1,43 +1,121 @@ +using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Text; using BepInEx.Unity.IL2CPP; +using Hazel; +using Reactor.Networking.Patches; namespace Reactor.Networking; public static class ModList { - private static Dictionary? _mapById; - private static Dictionary? _mapByNetId; + public static IReadOnlyCollection Current { get; private set; } = null!; - public static ISet? Current { get; private set; } + private static readonly Dictionary _mapById = new(); + private static readonly Dictionary _mapByPluginType = new(); public static Mod GetById(string id) { - return _mapById![id]; + return _mapById[id]; } - public static Mod GetByNetId(uint netId) + internal static Mod GetByPluginType(Type pluginType) { - return _mapByNetId![netId]; + return _mapByPluginType[pluginType]; } - internal static ISet Update() + private static readonly Dictionary _mapByNetId = new(); + private static readonly Dictionary _netIdMap = new(); + + internal static Mod GetByNetId(uint netId) { - var i = (uint) 0; + return _mapByNetId[netId]; + } + + internal static uint GetNetId(this Mod mod) + { + if (!mod.IsRequiredOnAllClients) throw new ArgumentException("Cannot get a net id for a mod without RequireOnAllClients flag", nameof(mod)); + return _netIdMap[mod]; + } + + public static Mod ReadMod(this MessageReader reader) + { + var netId = reader.ReadPackedUInt32(); + if (netId > 0) + { + return GetByNetId(netId); + } + + var id = reader.ReadString(); + return GetById(id); + } + + public static void Write(this MessageWriter writer, Mod mod) + { + if (mod.IsRequiredOnAllClients) + { + writer.WritePacked(mod.GetNetId()); + } + else + { + writer.WritePacked(0); + writer.Write(mod.Id); + } + } + + private static void Refresh() + { + if (ReactorConnection.Instance != null) throw new InvalidOperationException("Can't refresh the mod list during a connection"); Current = IL2CPPChainloader.Instance.Plugins.Values - .OrderByDescending(x => x.Metadata.GUID == ReactorPlugin.Id) - .Select(plugin => new Mod( - i++, - plugin.Metadata.GUID, - plugin.Metadata.Version.Clean(), - ReactorPluginSideAttribute.GetPluginSide(plugin.Instance.GetType()) - )) + .Select(pluginInfo => GetById(pluginInfo.Metadata.GUID)) + .OrderByDescending(x => x.Id == ReactorPlugin.Id) + .ThenBy(x => x.Id, StringComparer.Ordinal) .ToHashSet(); - _mapById = Current.ToDictionary(mod => mod.Id, mod => mod); - _mapByNetId = Current.ToDictionary(mod => mod.NetId, mod => mod); + _mapByNetId.Clear(); + _netIdMap.Clear(); - return Current; + uint netId = 1; + foreach (var mod in Current) + { + if (!mod.IsRequiredOnAllClients) continue; + + _mapByNetId[netId] = mod; + _netIdMap[mod] = netId; + netId++; + } + + var debug = new StringBuilder("Mod list:"); + foreach (var mod in Current) + { + debug.AppendLine(); + debug.Append($" - {mod.Id} version: {mod.Version}, flags: {mod.Flags}"); + if (mod.IsRequiredOnAllClients) debug.Append($", netId: {mod.GetNetId()}"); + } + + Debug(debug.ToString()); + } + + internal static void Initialize() + { + IL2CPPChainloader.Instance.PluginLoad += (pluginInfo, _, plugin) => + { + var pluginType = plugin.GetType(); + + var mod = new Mod( + pluginInfo.Metadata.GUID, + pluginInfo.Metadata.Version.Clean(), + ReactorModFlagsAttribute.GetModFlags(pluginType), + pluginInfo.Metadata.Name + ); + + _mapById[mod.Id] = mod; + _mapByPluginType[pluginType] = mod; + }; + + IL2CPPChainloader.Instance.Finished += Refresh; } } diff --git a/Reactor/Networking/ModdedHandshakeC2S.cs b/Reactor/Networking/ModdedHandshakeC2S.cs index b39aa6d..d19fa37 100644 --- a/Reactor/Networking/ModdedHandshakeC2S.cs +++ b/Reactor/Networking/ModdedHandshakeC2S.cs @@ -1,12 +1,35 @@ +using System.Collections.Generic; using Hazel; namespace Reactor.Networking; public static class ModdedHandshakeC2S { - public static void Serialize(MessageWriter writer, int modCount) + public static void Serialize(MessageWriter writer, IReadOnlyCollection mods) { - writer.Write((byte) ReactorProtocolVersion.Initial); - writer.WritePacked(modCount); + writer.WritePacked(mods.Count); + foreach (var mod in mods) + { + writer.Write(mod.Id); + writer.Write(mod.Version); + writer.Write((ushort) mod.Flags); + if (mod.IsRequiredOnAllClients) writer.Write(mod.Name); + } + } + + public static void Deserialize(MessageReader reader, out Mod[] mods) + { + var modCount = reader.ReadPackedInt32(); + mods = new Mod[modCount]; + + for (var i = 0; i < modCount; i++) + { + var id = reader.ReadString(); + var version = reader.ReadString(); + var flags = (ModFlags) reader.ReadUInt16(); + var name = (flags & ModFlags.RequireOnAllClients) != 0 ? reader.ReadString() : null; + + mods[i] = new Mod(id, version, flags, name); + } } } diff --git a/Reactor/Networking/Patches/ClientPatches.cs b/Reactor/Networking/Patches/ClientPatches.cs index 83bb8f9..3af5e3e 100644 --- a/Reactor/Networking/Patches/ClientPatches.cs +++ b/Reactor/Networking/Patches/ClientPatches.cs @@ -1,23 +1,241 @@ using System; -using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Text; using HarmonyLib; using Hazel; -using Hazel.Udp; +using Il2CppInterop.Runtime; using Il2CppInterop.Runtime.InteropTypes.Arrays; using InnerNet; -using Reactor.Extensions; using UnityEngine; namespace Reactor.Networking.Patches; internal static class ClientPatches { + [HarmonyPatch(typeof(InnerNetClient._HandleGameDataInner_d__38), nameof(InnerNetClient._HandleGameDataInner_d__38.MoveNext))] + public static class HandleGameDataInnerPatch + { + public static bool Prefix(InnerNetClient._HandleGameDataInner_d__38 __instance, ref bool __result) + { + var innerNetClient = __instance.__4__this; + var reader = __instance.reader; + + if (__instance.__1__state != 0) return true; + + if (reader.Tag == byte.MaxValue) + { + var flag = (ReactorGameDataFlag) reader.ReadByte(); + switch (flag) + { + case ReactorGameDataFlag.KickWithReason: + { + var reason = reader.ReadString(); + Debug("Received KickWithReason: " + reason); + innerNetClient.DisconnectWithReason(reason); + break; + } + } + + reader.Recycle(); + + __result = false; + return false; + } + + if (ReactorConnection.Instance!.Syncer == Syncer.Host && reader.Tag == InnerNetClient.SceneChangeFlag) + { + var clientId = reader.ReadPackedInt32(); + var clientData = innerNetClient.FindClientById(clientId); + var sceneName = reader.ReadString(); + + if (clientData != null && !string.IsNullOrWhiteSpace(sceneName)) + { + ReactorClientData? reactorClientData = null; + + // PATCH - Read injected ReactorHandshakeC2S + { + if (reader.BytesRemaining >= ReactorHeader.Size && ReactorHeader.Read(reader)) + { + ModdedHandshakeC2S.Deserialize(reader, out var mods); + + reactorClientData = new ReactorClientData(clientData, mods); + ReactorClientData.Set(clientId, reactorClientData); + + Debug("Received reactor handshake for " + clientData.PlayerName); + } + + if (innerNetClient.AmHost) + { + if (reactorClientData == null) + { + Warning("Kicking " + clientData.PlayerName + " for not having Reactor installed"); + PlayerControl.LocalPlayer.RpcSendChat(clientData.PlayerName + " tried joining without Reactor installed"); + innerNetClient.KickPlayer(clientData.Id, false); + + __result = false; + return false; + } + + if (!Mod.Validate(reactorClientData.Mods, ModList.Current, out var reason)) + { + innerNetClient.KickWithReason(clientData.Id, reason); + __result = false; + return false; + } + } + } + + innerNetClient.StartCoroutine(innerNetClient.CoOnPlayerChangedScene(clientData, sceneName)); + } + else + { + UnityEngine.Debug.Log($"Couldn't find client {clientId} to change scene to {sceneName}"); + reader.Recycle(); + } + + __result = false; + return false; + } + + return true; + } + } + + [HarmonyPatch(typeof(InnerNetClient._CoSendSceneChange_d__29), nameof(InnerNetClient._CoSendSceneChange_d__29.MoveNext))] + public static class CoSendSceneChangePatch + { + public static bool Prefix(InnerNetClient._CoSendSceneChange_d__29 __instance, ref bool __result) + { + if (ReactorConnection.Instance!.Syncer != Syncer.Host) return true; + + var innerNetClient = __instance.__4__this; + + if (__instance.__1__state == 2) + { + if (!innerNetClient.AmHost && innerNetClient.connection.State == ConnectionState.Connected) + { + var writer = MessageWriter.Get(SendOption.Reliable); + writer.StartMessage(Tags.GameData); + writer.Write(innerNetClient.GameId); + writer.StartMessage(InnerNetClient.SceneChangeFlag); + writer.WritePacked(innerNetClient.ClientId); + writer.Write(__instance.sceneName); + + // PATCH - Inject ReactorHandshakeC2S + Debug("Injecting ReactorHandshakeC2S to CoSendSceneChange"); + ReactorHeader.Write(writer); + ModdedHandshakeC2S.Serialize(writer, ModList.Current); + // + + writer.EndMessage(); + writer.EndMessage(); + innerNetClient.SendOrDisconnect(writer); + writer.Recycle(); + } + + __instance.__1__state = -1; + __result = false; + return false; + } + + return true; + } + } + + [HarmonyPatch(typeof(InnerNetClient._CoHandleSpawn_d__39), nameof(InnerNetClient._CoHandleSpawn_d__39.MoveNext))] + public static class CoHandleSpawnPatch + { + public static void Postfix(InnerNetClient._CoHandleSpawn_d__39 __instance, bool __result) + { + if (ReactorConnection.Instance!.Syncer != Syncer.Host) return; + + if (!__result && !AmongUsClient.Instance.AmHost && __instance._ownerId_5__2 == AmongUsClient.Instance.ClientId) + { + var reader = __instance.reader; + if (reader.BytesRemaining >= ReactorHeader.Size && ReactorHeader.Read(reader)) + { + ModdedHandshakeS2C.Deserialize(reader, out var serverName, out var serverVersion, out _); + Debug($"Host is modded ({serverName} {serverVersion})"); + } + else + { + Debug("Host is not modded"); + if (!Mod.Validate(ModList.Current, Array.Empty(), out var reason)) + { + AmongUsClient.Instance.DisconnectWithReason(reason); + } + } + } + } + } + + [HarmonyPatch(typeof(InnerNetClient), nameof(InnerNetClient.WriteSpawnMessage))] + public static class WriteSpawnMessagePatch + { + public static bool Prefix(InnerNetClient __instance, InnerNetObject netObjParent, int ownerId, SpawnFlags flags, MessageWriter msg) + { + if (ReactorConnection.Instance!.Syncer != Syncer.Host) return true; + + msg.StartMessage(4); + msg.WritePacked(netObjParent.SpawnId); + msg.WritePacked(ownerId); + msg.Write((byte) flags); + InnerNetObject[] componentsInChildren = netObjParent.GetComponentsInChildren(); + msg.WritePacked(componentsInChildren.Length); + foreach (InnerNetObject innerNetObject in componentsInChildren) + { + innerNetObject.OwnerId = ownerId; + innerNetObject.SpawnFlags = flags; + if (innerNetObject.NetId == 0) + { + innerNetObject.NetId = __instance.NetIdCnt++; + __instance.allObjects.Add(innerNetObject); + __instance.allObjectsFast.Add(innerNetObject.NetId, innerNetObject); + } + + msg.WritePacked(innerNetObject.NetId); + msg.StartMessage(1); + innerNetObject.Serialize(msg, initialState: true); + msg.EndMessage(); + } + + // PATCH - Inject ReactorHandshakeS2C + if (__instance.ClientId != ownerId && IL2CPP.il2cpp_object_get_class(netObjParent.Pointer) == Il2CppClassPointerStore.NativeClassPtr) + { + Debug("Injecting ReactorHandshakeS2C to WriteSpawnMessage"); + ReactorHeader.Write(msg); + ModdedHandshakeS2C.Serialize(msg, "Among Us", Application.version, 0); // TODO + } + // + + msg.EndMessage(); + + return false; + } + } + [HarmonyPatch(typeof(InnerNetClient), nameof(InnerNetClient.HandleMessage))] public static class HandleMessagePatch { public static bool Prefix(InnerNetClient __instance, [HarmonyArgument(0)] MessageReader reader) { + if (__instance.GameMode == GameModes.FreePlay) return true; + + var isFirst = false; + + if (ReactorConnection.Instance!.Syncer == null) + { + var parentBuffer = reader.Parent.Buffer; + var sendOption = (SendOption) parentBuffer[0]; + if (sendOption == SendOption.Reliable) + { + var id = (ushort) ((parentBuffer[1] << 8) + parentBuffer[2]); + isFirst = id == 1; + } + } + if (reader.Tag == byte.MaxValue) { var flag = (ReactorMessageFlags) reader.ReadByte(); @@ -28,129 +246,34 @@ internal static class ClientPatches { ModdedHandshakeS2C.Deserialize(reader, out var serverName, out var serverVersion, out var pluginCount); - Logger.Info($"Connected to a modded server ({serverName} {serverVersion}, {pluginCount} plugins), sending mod declarations"); - - var mods = ModList.Current!; - - var writer = MessageWriter.Get(SendOption.Reliable); - - var expected = 0; - var got = 0; - - void Send() + if (isFirst) { - expected++; - - __instance.connection.Send(writer, () => - { - got++; - - if (got >= expected) - { - Logger.Debug("Received all acks"); - __instance.connection.State = ConnectionState.Connected; - } - }); - writer.Recycle(); + Info($"Connected to a modded server ({serverName} {serverVersion}, {pluginCount} plugins)"); + ReactorConnection.Instance.Syncer = Syncer.Server; } - - foreach (var mod in mods) + else { - ModDeclaration.Serialize(writer, mod); - - if (writer.Length >= 500) - { - writer.CancelMessage(); - - Send(); - - writer = MessageWriter.Get(SendOption.Reliable); - ModDeclaration.Serialize(writer, mod); - } - - writer.EndMessage(); + Warning("Modded handshake came in late"); + __instance.DisconnectWithReason("Reactor handshake was sent out of order, please try connecting again."); } - Send(); - break; } - - case ReactorMessageFlags.PluginDeclaration: - { - // TODO - break; - } - - default: - throw new ArgumentOutOfRangeException(); } return false; } + if (isFirst) + { + ReactorConnection.Instance.Syncer = Syncer.Host; + Warning("Server is not modded, falling back to HAAS"); + } + 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; - } - - ackCallback = (Action) (() => - { - if (__instance.State == ConnectionState.Connected) - return; - - Logger.Debug("Hello was acked, waiting for modded handshake response"); - - var coroutine = Coroutines.Start(Coroutine(__instance)); - - __instance.Disconnected = Il2CppSystem.Delegate.Combine( - (Il2CppSystem.EventHandler) (Action) ((_, _) => - { - Coroutines.Stop(coroutine); - }), - __instance.Disconnected - ).Cast>(); - }); - } - - private static IEnumerator Coroutine(UdpConnection connection) - { - yield return new WaitForSeconds(3); - - var client = AmongUsClient.Instance; - if (client != null && client.connection != null && client.connection.Equals(connection) && client.connection.State == ConnectionState.Connecting) - { - var reactorPlugin = PluginSingleton.Instance; - - if (reactorPlugin.AllowVanillaServers!.Value) - { - if (reactorPlugin.CustomRpcManager.List.Any()) - { - Logger.Warning("Config option AllowVanillaServers was set to true, but there are custom rpcs registered!"); - } - else - { - connection.State = ConnectionState.Connected; - yield break; - } - } - - client.LastDisconnectReason = DisconnectReasons.Custom; - client.LastCustomDisconnect = "Server didn't respond to modded handshake"; - client.HandleDisconnect(client.LastDisconnectReason, client.LastCustomDisconnect); - } - } - } - [HarmonyPatch(typeof(InnerNetClient), nameof(InnerNetClient.GetConnectionData))] public static class HandshakePatch { @@ -165,15 +288,15 @@ internal static class ClientPatches public static void Postfix(ref Il2CppStructArray __result) { - var mods = ModList.Update(); - var handshake = new MessageWriter(1000); handshake.Write(__result); + ReactorHeader.Write(handshake); + ModdedHandshakeC2S.Serialize( handshake, - mods.Count + ModList.Current ); __result = handshake.ToByteArray(true); diff --git a/Reactor/Networking/Patches/HttpPatches.cs b/Reactor/Networking/Patches/HttpPatches.cs index 2615d2b..2df565f 100644 --- a/Reactor/Networking/Patches/HttpPatches.cs +++ b/Reactor/Networking/Patches/HttpPatches.cs @@ -2,12 +2,13 @@ using System; using System.Linq; using System.Text; using HarmonyLib; -using Il2CppInterop.Runtime.InteropTypes.Arrays; +using UnityEngine; using UnityEngine.Networking; +using UnityEngine.UI; +using Object = UnityEngine.Object; namespace Reactor.Networking.Patches; -[HarmonyPatch] internal static class HttpPatches { public const int Version = 1; @@ -17,53 +18,119 @@ internal static class HttpPatches var stringBuilder = new StringBuilder(); stringBuilder.Append(Version); - stringBuilder.Append(";"); + stringBuilder.Append(';'); - var mods = ModList.Update().Where(m => m.Side == PluginSide.Both).OrderBy(m => m.Id, StringComparer.Ordinal).ToArray(); + var mods = ModList.Current.Where(m => m.IsRequiredOnAllClients).ToArray(); stringBuilder.Append(mods.Length); foreach (var mod in mods) { - stringBuilder.Append(";"); + stringBuilder.Append(';'); stringBuilder.Append(mod.Id); - stringBuilder.Append("="); + stringBuilder.Append('='); stringBuilder.Append(mod.Version); } return stringBuilder.ToString(); } - [HarmonyPatch(typeof(UnityWebRequest), nameof(UnityWebRequest.Get), typeof(string))] - [HarmonyPatch(typeof(UnityWebRequest), nameof(UnityWebRequest.Post), typeof(string), typeof(string))] - [HarmonyPatch(typeof(UnityWebRequest), nameof(UnityWebRequest.Put), typeof(string), typeof(Il2CppStructArray))] - [HarmonyPostfix] - public static void UnityWebRequestPatch(UnityWebRequest __result) + internal static bool IsCurrentRegionModded() { - var path = __result.uri.AbsolutePath; + var currentRegion = ServerManager.Instance.CurrentRegion; - if (path == "/api/games") + if (currentRegion.TryCast() is { } httpRegionInfo) { - __result.SetRequestHeader("Client-Mods", BuildHeader()); + var lastConnection = SendWebRequestPatch.LastConnection; + return lastConnection.HasValue && lastConnection.Value.RegionInfo.Equals(httpRegionInfo) && lastConnection.Value.IsModded; + } + + return ReactorConnection.Instance is { Syncer: Syncer.Server }; + } + + [HarmonyPatch(typeof(UnityWebRequest), nameof(UnityWebRequest.SendWebRequest))] + private static class SendWebRequestPatch + { + internal static (StaticHttpRegionInfo RegionInfo, bool IsModded)? LastConnection { get; private set; } + + public static void Prefix(UnityWebRequest __instance) + { + var path = __instance.uri.AbsolutePath; + if (path == "/api/games") + { + Debug($"{__instance.method} {path}"); + __instance.SetRequestHeader("Client-Mods", BuildHeader()); + } + } + + public static void Postfix(UnityWebRequest __instance, UnityWebRequestAsyncOperation __result) + { + var path = __instance.uri.AbsolutePath; + if (path == "/api/games") + { + __result.add_completed((Action) (_ => + { + if (!HttpUtils.IsSuccess(__instance.responseCode)) return; + + var responseHeader = __instance.GetResponseHeader("Client-Mods-Processed"); + + if (responseHeader != null) + { + Debug("Connected to a modded HTTP matchmaking server"); + } + else + { + Warning("Connected to a vanilla HTTP matchmaking server"); + } + + if (__instance.GetMethod() == UnityWebRequest.UnityWebRequestMethod.Get) + { + if (responseHeader == null) + { + DisconnectPopup.Instance.ShowCustom("This region doesn't support modded handshake.\nThe lobbies shown may not be compatible with your current mods.\nFor more info see https://reactor.gg/handshake"); + } + } + + LastConnection = (ServerManager.Instance.CurrentRegion.Cast(), responseHeader != null); + })); + } } } - [HarmonyPatch(typeof(HttpMatchmakerManager._CoRequestGameList_d__7), nameof(HttpMatchmakerManager._CoRequestGameList_d__7.MoveNext))] - public static class CoRequestGameListPatch + [HarmonyPatch(typeof(GameStartManager), nameof(GameStartManager.Start))] + private static class GameStartManagerPatch { - public static void Prefix(HttpMatchmakerManager._CoRequestGameList_d__7 __instance) + public static void Postfix(GameStartManager __instance) { - if (__instance.__1__state == 2) + if (ModList.Current.Any(m => m.IsRequiredOnAllClients) && !IsCurrentRegionModded()) { - if (HttpUtils.IsSuccess(__instance._request_5__3.responseCode)) + Warning("Vanilla region, locking public toggle"); + + __instance.MakePublicButtonBehaviour.enabled = false; + var actionMapGlyphDisplay = __instance.MakePublicButton.GetComponentInChildren(includeInactive: true); + if (actionMapGlyphDisplay) { - var responseHeader = __instance._request_5__3.GetResponseHeader("Client-Mods-Processed"); - if (responseHeader == null) - { - AmongUsClient.Instance.LastDisconnectReason = DisconnectReasons.Custom; - AmongUsClient.Instance.LastCustomDisconnect = "This region doesn't support modded handshake.\nThe lobbies shown may not be compatible with your current mods.\nFor more info see https://reactor.gg/handshake"; - DisconnectPopup.Instance.Show(); - } + actionMapGlyphDisplay.gameObject.SetActive(value: false); + } + + __instance.MakePublicButton.color = new Color(1, 1, 1, 0.5f); + __instance.MakePublicButton.sprite = __instance.PrivateGameImage; + + var onClick = __instance.MakePublicButton.GetComponent().OnClick = new Button.ButtonClickedEvent(); + onClick.AddListener((Action) (() => + { + var popup = Object.Instantiate(DiscordManager.Instance.discordPopup, Camera.main!.transform); + var background = popup.transform.Find("Background").GetComponent(); + var size = background.size; + size.x *= 2.5f; + background.size = size; + popup.TextAreaTMP.fontSizeMin = 2; + popup.Show("You can't make public lobbies on regions that don't support modded handshake.\nFor more info see https://reactor.gg/handshake"); + })); + + if (AmongUsClient.Instance.AmHost && AmongUsClient.Instance.IsGamePublic) + { + AmongUsClient.Instance.ChangeGamePublic(false); } } } diff --git a/Reactor/Networking/Patches/ReactorClientData.cs b/Reactor/Networking/Patches/ReactorClientData.cs new file mode 100644 index 0000000..1fe5f38 --- /dev/null +++ b/Reactor/Networking/Patches/ReactorClientData.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using HarmonyLib; +using InnerNet; + +namespace Reactor.Networking.Patches; + +internal class ReactorClientData +{ + public ClientData ClientData { get; } + + public Mod[] Mods { get; } + + public ReactorClientData(ClientData clientData, Mod[] mods) + { + ClientData = clientData; + Mods = mods; + } + + private static readonly Dictionary _map = new(); + + public static ReactorClientData Get(int clientId) + { + return _map[clientId]; + } + + internal static void Set(int clientId, ReactorClientData data) + { + _map[clientId] = data; + } + + [HarmonyPatch] + private static class ClearPatches + { + [HarmonyPatch(typeof(InnerNetClient), nameof(InnerNetClient.DisconnectInternal))] + [HarmonyPostfix] + public static void DisconnectInternalPostfix() + { + _map.Clear(); + } + + [HarmonyPatch(typeof(EndGameResult), nameof(EndGameResult.Create))] + [HarmonyPostfix] + public static void EndGamePostfix() + { + _map.Clear(); + } + } +} diff --git a/Reactor/Networking/Patches/ReactorConnection.cs b/Reactor/Networking/Patches/ReactorConnection.cs new file mode 100644 index 0000000..c550546 --- /dev/null +++ b/Reactor/Networking/Patches/ReactorConnection.cs @@ -0,0 +1,37 @@ +using HarmonyLib; +using InnerNet; + +namespace Reactor.Networking.Patches; + +public class ReactorConnection +{ + public Syncer? Syncer { get; internal set; } + + public static ReactorConnection? Instance { get; private set; } + + [HarmonyPatch] + private static class Patches + { + [HarmonyPatch(typeof(InnerNetClient), nameof(InnerNetClient.CoConnect), typeof(string))] + [HarmonyPrefix] + public static void CoConnect() + { + Debug("New ReactorConnection created"); + Instance = new ReactorConnection(); + } + + [HarmonyPatch(typeof(InnerNetClient), nameof(InnerNetClient.DisconnectInternal))] + [HarmonyPostfix] + public static void DisconnectInternalPostfix() + { + Debug("ReactorConnection disconnected"); + Instance = null; + } + } +} + +public enum Syncer +{ + Server, + Host, +} diff --git a/Reactor/Networking/Patches/ServerPatches.cs b/Reactor/Networking/Patches/ServerPatches.cs deleted file mode 100644 index b75a980..0000000 --- a/Reactor/Networking/Patches/ServerPatches.cs +++ /dev/null @@ -1,21 +0,0 @@ -using HarmonyLib; -using Hazel; -using InnerNet; -using UnityEngine; - -namespace Reactor.Networking.Patches; - -internal static class ServerPatches -{ - [HarmonyPatch(typeof(InnerNetServer), nameof(InnerNetServer.OnServerConnect))] - public static class HandshakeResponsePatch - { - public static void Postfix([HarmonyArgument(0)] NewConnectionEventArgs evt) - { - var writer = MessageWriter.Get(SendOption.Reliable); - ModdedHandshakeS2C.Serialize(writer, "Among Us", Application.version, 0); - evt.Connection.Send(writer); - writer.Recycle(); - } - } -} diff --git a/Reactor/Networking/ReactorHeader.cs b/Reactor/Networking/ReactorHeader.cs new file mode 100644 index 0000000..d495225 --- /dev/null +++ b/Reactor/Networking/ReactorHeader.cs @@ -0,0 +1,30 @@ +using Hazel; + +namespace Reactor.Networking; + +public static class ReactorHeader +{ + private const ulong MAGIC = 0x72656163746f72; // "reactor" in ascii, 7 bytes + + public static int Size => sizeof(ulong); + + public static void Read(MessageReader reader, out ulong magic, out byte version) + { + var value = reader.ReadUInt64(); + magic = value >> 8; + version = (byte) (value & 0xFF); + } + + public static bool Read(MessageReader reader) + { + Read(reader, out var magic, out var version); + return magic == MAGIC && version == (int) ReactorProtocolVersion.Latest; + } + + public static void Write(MessageWriter writer) + { + var version = (byte) ReactorProtocolVersion.Latest; + var value = (MAGIC << 8) | version; + writer.Write(value); + } +} diff --git a/Reactor/Networking/UnsafeCustomRpc.cs b/Reactor/Networking/UnsafeCustomRpc.cs index a06e75d..c1c0164 100644 --- a/Reactor/Networking/UnsafeCustomRpc.cs +++ b/Reactor/Networking/UnsafeCustomRpc.cs @@ -17,7 +17,7 @@ public abstract class UnsafeCustomRpc public uint Id { get; } public BasePlugin UnsafePlugin { get; } - public string PluginId { get; } + public Mod Mod { get; } public abstract Type InnerNetObjectType { get; } @@ -27,7 +27,7 @@ public abstract class UnsafeCustomRpc protected UnsafeCustomRpc(BasePlugin plugin, uint id) { UnsafePlugin = plugin; - PluginId = MetadataHelper.GetMetadata(plugin).GUID; + Mod = ModList.GetByPluginType(plugin.GetType()); Id = id; } @@ -55,8 +55,7 @@ public abstract class UnsafeCustomRpc true => AmongUsClient.Instance.StartRpcImmediately(innerNetObject.NetId, CustomRpcManager.CallId, SendOption, targetClientId), }; - var pluginNetId = ModList.GetById(PluginId).NetId; - writer.WritePacked(pluginNetId); + writer.Write(Mod); writer.WritePacked(Id); writer.StartMessage(0); diff --git a/Reactor/PluginSingleton.cs b/Reactor/PluginSingleton.cs index dd8b5be..55a7902 100644 --- a/Reactor/PluginSingleton.cs +++ b/Reactor/PluginSingleton.cs @@ -1,3 +1,4 @@ +using System; using System.Linq; using System.Reflection; using BepInEx.Unity.IL2CPP; @@ -8,7 +9,16 @@ public static class PluginSingleton where T : BasePlugin { private static T? _instance; - public static T Instance => _instance ??= IL2CPPChainloader.Instance.Plugins.Values.Select(x => x.Instance).OfType().Single(); + public static T Instance + { + get => _instance ??= IL2CPPChainloader.Instance.Plugins.Values.Select(x => x.Instance).OfType().Single(); + set + { + if (_instance == value) return; + if (_instance != null) throw new InvalidOperationException($"Instance for {typeof(T)} is already set"); + _instance = value; + } + } internal static void Initialize() { diff --git a/Reactor/Reactor.csproj b/Reactor/Reactor.csproj index 91f4143..b79f44f 100644 --- a/Reactor/Reactor.csproj +++ b/Reactor/Reactor.csproj @@ -1,10 +1,6 @@ - net6.0 latest - embedded - enable - true 2.0.0 dev diff --git a/Reactor/ReactorModFlagsAttribute.cs b/Reactor/ReactorModFlagsAttribute.cs new file mode 100644 index 0000000..18e8b0e --- /dev/null +++ b/Reactor/ReactorModFlagsAttribute.cs @@ -0,0 +1,33 @@ +using System; +using System.Linq; +using System.Reflection; + +namespace Reactor; + +[AttributeUsage(AttributeTargets.Class)] +public class ReactorModFlagsAttribute : Attribute +{ + public ModFlags Flags { get; } + + public ReactorModFlagsAttribute(ModFlags flags) + { + Flags = flags; + } + + public static ModFlags GetModFlags(Type type) + { + var attribute = type.GetCustomAttribute(); + if (attribute != null) + { + return attribute.Flags; + } + + var metadataAttribute = type.Assembly.GetCustomAttributes().SingleOrDefault(x => x.Key == "Reactor.ModFlags"); + if (metadataAttribute != null && metadataAttribute.Value != null) + { + return Enum.Parse(metadataAttribute.Value); + } + + return ModFlags.None; + } +} diff --git a/Reactor/ReactorPlugin.cs b/Reactor/ReactorPlugin.cs index bb07b3a..6637c5e 100644 --- a/Reactor/ReactorPlugin.cs +++ b/Reactor/ReactorPlugin.cs @@ -30,8 +30,10 @@ public partial class ReactorPlugin : BasePlugin public ReactorPlugin() { + PluginSingleton.Instance = this; PluginSingleton.Initialize(); RegisterInIl2CppAttribute.Initialize(); + ModList.Initialize(); RegisterCustomRpcAttribute.Initialize(); MessageConverterAttribute.Initialize(); MethodRpcAttribute.Initialize(); diff --git a/Reactor/ReactorPluginSideAttribute.cs b/Reactor/ReactorPluginSideAttribute.cs deleted file mode 100644 index ccbf36e..0000000 --- a/Reactor/ReactorPluginSideAttribute.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using System.Linq; -using System.Reflection; - -namespace Reactor; - -/// -/// This attribute specifies plugin's side -/// -[AttributeUsage(AttributeTargets.Class)] -public class ReactorPluginSideAttribute : Attribute -{ - public PluginSide Side { get; } - - public ReactorPluginSideAttribute(PluginSide side) - { - Side = side; - } - - public static PluginSide GetPluginSide(Type type) - { - var attribute = type.GetCustomAttribute(); - if (attribute != null) - { - return attribute.Side; - } - - var metadataAttribute = type.Assembly.GetCustomAttributes().SingleOrDefault(x => x.Key == "Reactor.PluginSide"); - if (metadataAttribute != null) - { - return Enum.Parse(metadataAttribute.Value, true); - } - - return PluginSide.Both; - } -}