Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AmongUs.props
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project>
<ItemGroup>
<PackageReference Include="BepInEx.Unity.IL2CPP" Version="6.0.0-be.735" Private="false" ExcludeAssets="runtime;native" />
<PackageReference Include="AmongUs.GameLibs.Steam" Version="2025.4.15" PrivateAssets="all" />
<PackageReference Include="AmongUs.GameLibs.Steam" Version="2026.8.18" PrivateAssets="all" />

<PackageReference Include="BepInEx.AutoPlugin" Version="1.1.0" PrivateAssets="all" />
<PackageReference Include="BepInEx.IL2CPP.MSBuild" Version="2.1.0-rc.1" PrivateAssets="all" ExcludeAssets="runtime" />
Expand Down
139 changes: 139 additions & 0 deletions Reactor/Networking/AmciMods.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
#pragma warning disable CA5350

using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using BepInEx;
using BepInEx.Unity.IL2CPP;
using Reactor.Networking.Attributes;

namespace Reactor.Networking;

/// <summary>
/// Tracks the AMCI mod registrations of the loaded plugins and drives the vanilla
/// <see cref="CurrentModRegistration"/> integration.
/// </summary>
public static class AmciMods
{
private static readonly Dictionary<string, Guid> _guidByModId = new();

/// <summary>
/// Gets the loaded mod ids and their registered AMCI GUIDs.
/// </summary>
public static IReadOnlyDictionary<string, Guid> Registered => _guidByModId;

/// <summary>
/// Gets the calculated active AMCI GUID (Single mod GUID or deterministic Composite GUID for multiple mods),
/// or null when no loaded mod registered one.
/// </summary>
public static Guid? Primary { get; private set; }

/// <summary>
/// Gets a value indicating whether AMCI is currently active: at least one mod GUID is registered.
/// </summary>
public static bool IsEnabled => Primary != null;

/// <summary>
/// Applies the current AMCI state to the vanilla client.
/// </summary>
public static void Apply()
{
var primary = Primary;
CurrentModRegistration.ModRegistrationGuidString = IsEnabled && primary != null ? primary.Value.ToString() : string.Empty;
}

internal static void Initialize()
{
foreach (var pluginInfo in IL2CPPChainloader.Instance.Plugins.Values)
{
if (pluginInfo.Instance != null)
{
Register(pluginInfo, (BasePlugin) pluginInfo.Instance);
}
}

IL2CPPChainloader.Instance.PluginLoad += (pluginInfo, _, plugin) => Register(pluginInfo, plugin);

IL2CPPChainloader.Instance.Finished += () =>
{
foreach (var pluginInfo in IL2CPPChainloader.Instance.Plugins.Values)
{
if (pluginInfo.Instance != null)
{
Register(pluginInfo, (BasePlugin) pluginInfo.Instance);
}
}

RefreshPrimary();
Apply();
Info($"AMCI initialized: registered={_guidByModId.Count} activeGuid={Primary} CurrentModRegistration={CurrentModRegistration.ModRegistrationGuidString}");
};
}

private static void Register(PluginInfo pluginInfo, BasePlugin plugin)
{
if (plugin == null)
{
return;
}

var pluginType = plugin.GetType();
if (AmciIgnoreAttribute.IsIgnored(pluginType))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the AmciModGuidAttribute is not defined, the plugin is always ignored. So this is another reason why this attribute is not needed

{
return;
}

var guid = AmciModGuidAttribute.GetGuid(pluginType);
if (guid.HasValue)
{
_guidByModId[pluginInfo.Metadata.GUID] = guid.Value;
}
}

private static void RefreshPrimary()
{
if (_guidByModId.Count == 0)
{
Primary = null;
}
else if (_guidByModId.Count == 1)
{
Primary = _guidByModId.Values.First();
}
else
{
Primary = ComputeCompositeGuid(_guidByModId.Values);
}
}

private static Guid ComputeCompositeGuid(IEnumerable<Guid> guids)
{
var sortedGuids = guids
.Distinct()
.OrderBy(g => g.ToString(), StringComparer.OrdinalIgnoreCase)
.ToArray();

if (sortedGuids.Length == 1)
{
return sortedGuids[0];
}

var buffer = new byte[sortedGuids.Length * 16];
for (var i = 0; i < sortedGuids.Length; i++)
{
var bytes = sortedGuids[i].ToByteArray();
Buffer.BlockCopy(bytes, 0, buffer, i * 16, 16);
}

var hash = SHA1.HashData(buffer);
var guidBytes = new byte[16];
Array.Copy(hash, 0, guidBytes, 0, 16);

// Conform to RFC 4122 version 4

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I get why you're normalizing this to UUIDv4, but wouldn't it be better to set it to a different UUID type to indicate it's a composite UUID? Either V5 which is used for SHA1 hashed namespaces or V8 for "Custom"

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think they’re pretty much the same in terms of functionality, but it’s certainly better

guidBytes[6] = (byte) ((guidBytes[6] & 0x0F) | 0x40); // version 4
guidBytes[8] = (byte) ((guidBytes[8] & 0x3F) | 0x80); // variant RFC 4122

return new Guid(guidBytes);
}
}
28 changes: 28 additions & 0 deletions Reactor/Networking/Attributes/AmciIgnoreAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using System;
using System.Linq;
using System.Reflection;

namespace Reactor.Networking.Attributes;

/// <summary>
/// Marks a plugin to be ignored during AMCI mod GUID registration and composite calculation.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this is the wrong approach. There are currently 3 types of mods:

  1. All client mods, which should use AMCI
  2. Host-only mods, which use +25 instead. In some cases they register for AMCI as well as the server does not check AMCI on join.
  3. Client-only utility mods, which don't require any registration as they should not interact with the network at all (think UnityExplorer)

#3 can't change their registration as they usually cannot depend on Reactor as they're targeting multiple games. And their presence shouldn't influence matchmaking anyway

For #1 and #2 you can get their names from their existing ModFlags declaration and use that as the input for the composite calculation, and this attibute is therefore obsolete as I don't see why a mod should need to opt out of AMCI once Reactor handles it.

That leaves the question: what to do if nobody registers ModFlags, but Reactor is loaded. I'm not sure yet, I'm thinking right now that it's best for Reactor to be invisible then, and not affect the public lobby menu

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You’re right, can easily achieve the same functionality without registering AMCI at all.
I’m not entirely sure why this was written either; perhaps it was intended specifically for the API mods at the time. However, after the composite GUID calculation feature was added later on, I forgot to remove it.

/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public sealed class AmciIgnoreAttribute : Attribute
{
internal static bool IsIgnored(Type type)
{
if (type.GetCustomAttribute<AmciIgnoreAttribute>() != null)
{
return true;
}

var metadata = type.Assembly.GetCustomAttributes<AssemblyMetadataAttribute>().SingleOrDefault(x => x.Key == "Reactor.AmciIgnore");
if (metadata is { Value: not null } && bool.TryParse(metadata.Value, out var ignored))
{
return ignored;
}

return false;
}
}
40 changes: 40 additions & 0 deletions Reactor/Networking/Attributes/AmciModGuidAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using System;
using System.Linq;
using System.Reflection;

namespace Reactor.Networking.Attributes;

/// <summary>
/// Registers the annotated plugin with the official AMCI protocol.
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public sealed class AmciModGuidAttribute : Attribute
{
/// <summary>
/// Initializes a new instance of the <see cref="AmciModGuidAttribute"/> class.
/// </summary>
/// <param name="guid">The self-assigned v4 mod GUID of the mod.</param>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about concatenating the plugin names together (gg.reactor.api, auavengers.tou.mira, etc etc)
Every mod has these already, no need to manually configure a GUID for each mod

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Of course can, but do you intend to automatically register the mod name as a GUID, or do you need to register it manually via an attribute declaration? If choose the automatic option, there may be mod creators with specific requirements who do not wish for their mods to be registered by AMCI.

public AmciModGuidAttribute(string guid)
{
Guid = Guid.Parse(guid);
}

public Guid Guid { get; }

Check warning on line 22 in Reactor/Networking/Attributes/AmciModGuidAttribute.cs

View workflow job for this annotation

GitHub Actions / build

Missing XML comment for publicly visible type or member 'AmciModGuidAttribute.Guid'

Check warning on line 22 in Reactor/Networking/Attributes/AmciModGuidAttribute.cs

View workflow job for this annotation

GitHub Actions / build

Missing XML comment for publicly visible type or member 'AmciModGuidAttribute.Guid'

internal static Guid? GetGuid(Type type)
{
var attribute = type.GetCustomAttribute<AmciModGuidAttribute>();
if (attribute != null)
{
return attribute.Guid;
}

var metadataAttribute = type.Assembly.GetCustomAttributes<AssemblyMetadataAttribute>().SingleOrDefault(x => x.Key == "Reactor.AmciModGuid");
if (metadataAttribute is { Value: not null } && Guid.TryParse(metadataAttribute.Value, out var guid))
{
return guid;
}

return null;
}
}
3 changes: 2 additions & 1 deletion Reactor/Networking/Patches/HttpPatches.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ public static void Postfix(UnityWebRequest __instance, UnityWebRequestAsyncOpera

if (__instance.GetMethod() == UnityWebRequest.UnityWebRequestMethod.Get)
{
if (responseHeader == null && ModList.IsAnyModRequiredOnAllClients)
if (responseHeader == null && !AmciMods.IsEnabled && ModList.IsAnyModRequiredOnAllClients)
{
HandshakePopup.Show();
}
Expand All @@ -110,6 +110,7 @@ private static class GameStartManagerPatch
public static void Postfix(GameStartManager __instance)
{
if (AmongUsClient.Instance.NetworkMode != NetworkModes.OnlineGame) return;
if (AmciMods.IsEnabled) return;
if (ModList.IsAnyModRequiredOnAllClients && !IsCurrentRegionModded())
{
Warning("Vanilla region, locking public toggle");
Expand Down
55 changes: 55 additions & 0 deletions Reactor/Networking/Patches/ModRegistrationPatch.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using System;

Check warning on line 1 in Reactor/Networking/Patches/ModRegistrationPatch.cs

View workflow job for this annotation

GitHub Actions / build

Using directive is unnecessary. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0005)
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using AmongUs.GameOptions;
using AmongUs.Matchmaking;
using HarmonyLib;
using InnerNet;

namespace Reactor.Networking.Patches
{
internal class ModRegistrationPatch

Check warning on line 13 in Reactor/Networking/Patches/ModRegistrationPatch.cs

View workflow job for this annotation

GitHub Actions / build

Type 'ModRegistrationPatch' can be sealed because it has no subtypes in its containing assembly and is not externally visible (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1852)
{
[HarmonyPatch(typeof(CurrentModRegistration), nameof(CurrentModRegistration.UpdateFilterSetWithModRegistrationSettings))]
public static class CurrentModRegistrationPatch
{
public static void Prefix(GameFilterSet filterSet)
{
for (int i = filterSet.Filters.Count - 1; i >= 0; i--)

Check warning on line 20 in Reactor/Networking/Patches/ModRegistrationPatch.cs

View workflow job for this annotation

GitHub Actions / build

use 'var' instead of explicit type (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0007)
{
if (filterSet.Filters[i].Key == "mod")
{
filterSet.Filters.RemoveAt(i);
}
}
}
}

[HarmonyPatch(typeof(InnerNetClient), nameof(InnerNetClient.HostGame), typeof(IGameOptions), typeof(GameFilterOptions))]
public static class LocalGamePatch
{
private static string _savedGuid;

Check warning on line 33 in Reactor/Networking/Patches/ModRegistrationPatch.cs

View workflow job for this annotation

GitHub Actions / build

Non-nullable field '_savedGuid' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable.

Check warning on line 33 in Reactor/Networking/Patches/ModRegistrationPatch.cs

View workflow job for this annotation

GitHub Actions / build

Non-nullable field '_savedGuid' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable.

public static void Prefix()
{
if (AmongUsClient.Instance != null
&& AmongUsClient.Instance.NetworkMode != NetworkModes.OnlineGame)
{
_savedGuid = CurrentModRegistration.ModRegistrationGuidString;
CurrentModRegistration.ModRegistrationGuidString = "";

Check warning on line 41 in Reactor/Networking/Patches/ModRegistrationPatch.cs

View workflow job for this annotation

GitHub Actions / build

}
}

public static void Postfix()
{
if (_savedGuid != null)
{
CurrentModRegistration.ModRegistrationGuidString = _savedGuid;
_savedGuid = null;

Check warning on line 50 in Reactor/Networking/Patches/ModRegistrationPatch.cs

View workflow job for this annotation

GitHub Actions / build

Cannot convert null literal to non-nullable reference type.

Check warning on line 50 in Reactor/Networking/Patches/ModRegistrationPatch.cs

View workflow job for this annotation

GitHub Actions / build

Cannot convert null literal to non-nullable reference type.
}
}
}
}
}
1 change: 1 addition & 0 deletions Reactor/ReactorPlugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ public ReactorPlugin()

RegisterInIl2CppAttribute.Initialize();
ModList.Initialize();
AmciMods.Initialize();

RegisterCustomRpcAttribute.Initialize();
MessageConverterAttribute.Initialize();
Expand Down
6 changes: 3 additions & 3 deletions Reactor/packages.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
"net6.0": {
"AmongUs.GameLibs.Steam": {
"type": "Direct",
"requested": "[2025.4.15, )",
"resolved": "2025.4.15",
"contentHash": "R3RxzHGZOGUJ9nMwrP0cjS6g+sh60QUU506Mo47lh17155enUjH6PtErijihSoU7t3aQpzCIyC67W3XrrHhH4g=="
"requested": "[2026.8.18, )",
"resolved": "2026.8.18",
"contentHash": "sSGu2zGYvLboOc83PzPxQ0yGsK1YrhIO/538Tfzc/5FKfEbGCtCQErcKV0wdiYZCHGpCe561RzzfrBtp46jcgQ=="
},
"BepInEx.AutoPlugin": {
"type": "Direct",
Expand Down
Loading