-
Notifications
You must be signed in to change notification settings - Fork 60
Support AMCI in v18.0 #108
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
a0f2dd8
3d65f0f
69060d2
eda83f5
fc495e2
6d81025
f4e444c
4f5c8ce
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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)) | ||
| { | ||
| 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 | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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"
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
| } | ||
| } | ||
| 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. | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
#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
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
| /// </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; | ||
| } | ||
| } | ||
| 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> | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How about concatenating the plugin names together (
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
|
||
|
|
||
| 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; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| using System; | ||
| 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
|
||
| { | ||
| [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--) | ||
| { | ||
| 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
|
||
|
|
||
| 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
|
||
| } | ||
| } | ||
|
|
||
| public static void Postfix() | ||
| { | ||
| if (_savedGuid != null) | ||
| { | ||
| CurrentModRegistration.ModRegistrationGuidString = _savedGuid; | ||
| _savedGuid = null; | ||
|
Check warning on line 50 in Reactor/Networking/Patches/ModRegistrationPatch.cs
|
||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
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