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;
- }
-}