Protocol v3

This commit is contained in:
js6pak committed 2022-08-30 21:22:50 +02:00
1 parent f56aa32927
commit d01ea478f4
25 files changed
+755 -307

No files matched your search

-1
View File
@@ -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);
+1
View File
@@ -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()
+24
View File
@@ -0,0 +1,24 @@
using System;
namespace Reactor;
[Flags]
public enum ModFlags : ushort
{
None = 0,
/// <summary>
/// Requires all clients in a lobby to have the mod.
/// </summary>
RequireOnAllClients = 1 << 0,
/// <summary>
/// Requires the server to have a plugin that handles the mod.
/// </summary>
RequireOnServer = 1 << 1,
/// <summary>
/// Requires the host of the lobby to have the mod.
/// </summary>
RequireOnHost = 1 << 2,
}
+50 -11
View File
@@ -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<Mod> clientMods, IReadOnlyCollection<Mod> 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;
}
}
@@ -0,0 +1,6 @@
namespace Reactor.Networking;
public enum ReactorGameDataFlag : byte
{
KickWithReason,
}
@@ -3,6 +3,4 @@ namespace Reactor.Networking;
public enum ReactorMessageFlags : byte
{
Handshake,
ModDeclaration,
PluginDeclaration,
}
@@ -1,17 +1,22 @@
namespace Reactor.Networking;
/// <summary>
/// Version of the Reactor.Net protocol format.
/// Version of the Reactor.Networking protocol format.
/// </summary>
public enum ReactorProtocolVersion : byte
{
/// <summary>
/// Initial version.
/// First public Reactor Protocol version.
/// </summary>
Initial = 1,
V2 = 1,
/// <summary>
/// Version introducing vanilla server support, syncer concept and registries.
/// </summary>
V3 = 2,
/// <summary>
/// Latest version.
/// </summary>
Latest = Initial,
Latest = V3,
}
-17
View File
@@ -1,17 +0,0 @@
namespace Reactor;
/// <summary>
/// Plugin side used in modded handshake
/// </summary>
public enum PluginSide : byte
{
/// <summary>
/// Required by both sides, reject connection if missing on the other side
/// </summary>
Both,
/// <summary>
/// Required only by client
/// </summary>
ClientOnly,
}
+20 -30
View File
@@ -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<UnsafeCustomRpc> _list = new();
private readonly Dictionary<Type, List<UnsafeCustomRpc>> _map = new();
private readonly Dictionary<Type, Dictionary<Mod, Dictionary<uint, UnsafeCustomRpc>>> _map = new();
public IReadOnlyList<UnsafeCustomRpc> List => _list.AsReadOnly();
public ILookup<Type, UnsafeCustomRpc> 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<UnsafeCustomRpc>();
}
}
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<Mod, Dictionary<uint, UnsafeCustomRpc>>())
.GetOrCreate(customRpc.Mod, _ => new Dictionary<uint, UnsafeCustomRpc>())
.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<ReactorPlugin>.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<ReactorPlugin>.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;
}
}
+30
View File
@@ -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);
}
}
-16
View File
@@ -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);
}
}
+96 -18
View File
@@ -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<string, Mod>? _mapById;
private static Dictionary<uint, Mod>? _mapByNetId;
public static IReadOnlyCollection<Mod> Current { get; private set; } = null!;
public static ISet<Mod>? Current { get; private set; }
private static readonly Dictionary<string, Mod> _mapById = new();
private static readonly Dictionary<Type, Mod> _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<Mod> Update()
private static readonly Dictionary<uint, Mod> _mapByNetId = new();
private static readonly Dictionary<Mod, uint> _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;
}
}
+26 -3
View File
@@ -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<Mod> 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);
}
}
}
+236 -113
View File
@@ -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<Mod>(), 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<InnerNetObject>();
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<PlayerControl>.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<ReactorPlugin>.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<ReactorPlugin>.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<ReactorPlugin>.Debug("Hello was acked, waiting for modded handshake response");
var coroutine = Coroutines.Start(Coroutine(__instance));
__instance.Disconnected = Il2CppSystem.Delegate.Combine(
(Il2CppSystem.EventHandler<DisconnectedEventArgs>) (Action<Il2CppSystem.Object, DisconnectedEventArgs>) ((_, _) =>
{
Coroutines.Stop(coroutine);
}),
__instance.Disconnected
).Cast<Il2CppSystem.EventHandler<DisconnectedEventArgs>>();
});
}
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<ReactorPlugin>.Instance;
if (reactorPlugin.AllowVanillaServers!.Value)
{
if (reactorPlugin.CustomRpcManager.List.Any())
{
Logger<ReactorPlugin>.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<byte> __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);
+93 -26
View File
@@ -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<byte>))]
[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<StaticHttpRegionInfo>() 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<AsyncOperation>) (_ =>
{
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<StaticHttpRegionInfo>(), 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<ActionMapGlyphDisplay>(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<PassiveButton>().OnClick = new Button.ButtonClickedEvent();
onClick.AddListener((Action) (() =>
{
var popup = Object.Instantiate(DiscordManager.Instance.discordPopup, Camera.main!.transform);
var background = popup.transform.Find("Background").GetComponent<SpriteRenderer>();
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);
}
}
}
@@ -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<int, ReactorClientData> _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();
}
}
}
@@ -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,
}
@@ -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();
}
}
}
+30
View File
@@ -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);
}
}
+3 -4
View File
@@ -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);
+11 -1
View File
@@ -1,3 +1,4 @@
using System;
using System.Linq;
using System.Reflection;
using BepInEx.Unity.IL2CPP;
@@ -8,7 +9,16 @@ public static class PluginSingleton<T> where T : BasePlugin
{
private static T? _instance;
public static T Instance => _instance ??= IL2CPPChainloader.Instance.Plugins.Values.Select(x => x.Instance).OfType<T>().Single();
public static T Instance
{
get => _instance ??= IL2CPPChainloader.Instance.Plugins.Values.Select(x => x.Instance).OfType<T>().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()
{
-4
View File
@@ -1,10 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<LangVersion>latest</LangVersion>
<DebugType>embedded</DebugType>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<VersionPrefix>2.0.0</VersionPrefix>
<VersionSuffix>dev</VersionSuffix>
+33
View File
@@ -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<ReactorModFlagsAttribute>();
if (attribute != null)
{
return attribute.Flags;
}
var metadataAttribute = type.Assembly.GetCustomAttributes<AssemblyMetadataAttribute>().SingleOrDefault(x => x.Key == "Reactor.ModFlags");
if (metadataAttribute != null && metadataAttribute.Value != null)
{
return Enum.Parse<ModFlags>(metadataAttribute.Value);
}
return ModFlags.None;
}
}
+2
View File
@@ -30,8 +30,10 @@ public partial class ReactorPlugin : BasePlugin
public ReactorPlugin()
{
PluginSingleton<ReactorPlugin>.Instance = this;
PluginSingleton<BasePlugin>.Initialize();
RegisterInIl2CppAttribute.Initialize();
ModList.Initialize();
RegisterCustomRpcAttribute.Initialize();
MessageConverterAttribute.Initialize();
MethodRpcAttribute.Initialize();
-36
View File
@@ -1,36 +0,0 @@
using System;
using System.Linq;
using System.Reflection;
namespace Reactor;
/// <summary>
/// This attribute specifies plugin's side
/// </summary>
[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<ReactorPluginSideAttribute>();
if (attribute != null)
{
return attribute.Side;
}
var metadataAttribute = type.Assembly.GetCustomAttributes<AssemblyMetadataAttribute>().SingleOrDefault(x => x.Key == "Reactor.PluginSide");
if (metadataAttribute != null)
{
return Enum.Parse<PluginSide>(metadataAttribute.Value, true);
}
return PluginSide.Both;
}
}