diff --git a/EXILED/Exiled.Permissions/Config.cs b/EXILED/Exiled.Permissions/Config.cs
index b0aabc52f0..81cf8fb394 100644
--- a/EXILED/Exiled.Permissions/Config.cs
+++ b/EXILED/Exiled.Permissions/Config.cs
@@ -44,6 +44,12 @@ public Config()
[Description("The permissions full path")]
public string FullPath { get; private set; }
+ ///
+ /// Gets a value indicating whether multi-group system should be used.
+ ///
+ [Description("Whether MultiGroup system should be used")]
+ public bool UseMultiGroup { get; private set; } = false;
+
///
public bool IsEnabled { get; set; } = true;
diff --git a/EXILED/Exiled.Permissions/Extensions/Permissions.cs b/EXILED/Exiled.Permissions/Extensions/Permissions.cs
index 56823e3feb..8127cca42e 100644
--- a/EXILED/Exiled.Permissions/Extensions/Permissions.cs
+++ b/EXILED/Exiled.Permissions/Extensions/Permissions.cs
@@ -14,16 +14,14 @@ namespace Exiled.Permissions.Extensions
using System.Text;
using CommandSystem;
-
using Exiled.API.Extensions;
using Exiled.API.Features;
using Exiled.API.Features.Pools;
using Features;
-
+ using Features.MultipleGroups;
using Properties;
using Query;
using RemoteAdmin;
-
using YamlDotNet.Core;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
@@ -207,15 +205,48 @@ public static bool CheckPermission(this Player player, string permission)
Log.Debug($"UserID: {player.UserId} | PlayerId: {player.Id}");
Log.Debug($"Permission string: {permission}");
- string plyGroupKey = player.Group is not null ? ServerStatic.PermissionsHandler.Groups.FirstOrDefault(g => g.Value.EqualsTo(player.Group)).Key : null;
- Log.Debug($"GroupKey: {plyGroupKey ?? "(null)"}");
+ if (!MultiGroup.IsActive)
+ {
+ string plyGroupKey = player.Group is not null ? ServerStatic.PermissionsHandler.Groups.FirstOrDefault(g => g.Value.EqualsTo(player.Group)).Key : null;
+ Log.Debug($"GroupKey: {plyGroupKey ?? "(null)"}");
+
+ if (plyGroupKey is null || !Groups.TryGetValue(plyGroupKey, out Group group))
+ {
+ Log.Debug("The source group is null, the default group is used");
+ group = DefaultGroup;
+ }
+
+ return group.CheckPermission(permission);
+ }
+
+ if (!MultiGroup.Permissions.TryGetValue(player.UserId, out string[] data))
+ return false;
- if (plyGroupKey is null || !Groups.TryGetValue(plyGroupKey, out Group group))
+ foreach (string group in data)
{
- Log.Debug("The source group is null, the default group is used");
- group = DefaultGroup;
+ if (!Groups.TryGetValue(group, out Group value))
+ {
+ Log.Error($"Invalid group name: {group}");
+ continue;
+ }
+
+ if (value.CheckPermission(permission))
+ return true;
}
+ return false;
+ }
+
+ ///
+ /// Checks a group's permission.
+ ///
+ /// The group to be checked.
+ /// The permission to be checked.
+ /// if the player's current or native group has permissions; otherwise, .
+ public static bool CheckPermission(this Group group, string permission)
+ {
+ group ??= DefaultGroup;
+
if (group is null)
{
Log.Debug("There's no default group, returning false...");
diff --git a/EXILED/Exiled.Permissions/Features/MultipleGroups/BaseCheckPatch.cs b/EXILED/Exiled.Permissions/Features/MultipleGroups/BaseCheckPatch.cs
new file mode 100644
index 0000000000..26520cc305
--- /dev/null
+++ b/EXILED/Exiled.Permissions/Features/MultipleGroups/BaseCheckPatch.cs
@@ -0,0 +1,81 @@
+// -----------------------------------------------------------------------
+//
+// Copyright (c) ExMod Team. All rights reserved.
+// Licensed under the CC BY-SA 3.0 license.
+//
+// -----------------------------------------------------------------------
+
+namespace Exiled.Permissions.Features.MultipleGroups
+{
+ using System.Collections.Generic;
+ using System.Reflection.Emit;
+
+ using CommandSystem;
+ using Exiled.API.Features;
+ using HarmonyLib;
+ using Query;
+ using RemoteAdmin;
+
+ ///
+ /// Patches base-game method to implement multiple groups check per user.
+ ///
+ [HarmonyPatch]
+ internal static class BaseCheckPatch
+ {
+ private static bool Check(ICommandSender sender, PlayerPermissions[] permissions)
+ {
+ if (sender is not CommandSender commandSender)
+ return false;
+
+ if (commandSender.FullPermissions || sender is ServerConsoleSender || commandSender == Server.Host.Sender)
+ {
+ return true;
+ }
+
+ if (sender is PlayerCommandSender || sender is QueryCommandSender)
+ {
+ if (!Player.TryGet(sender, out Player player))
+ return false;
+
+ if (!MultiGroup.Permissions.TryGetValue(player.UserId, out string[] data))
+ return false;
+
+ foreach (string groupName in data)
+ {
+ UserGroup group = ServerStatic.PermissionsHandler.GetGroup(groupName);
+ if (group == null)
+ {
+ Log.Error($"Invalid group name: {groupName}");
+ continue;
+ }
+
+ if (PermissionsHandler.IsPermitted(group.Permissions, permissions))
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private static bool Check(ICommandSender sender, PlayerPermissions permissions) => Check(sender, new[] { permissions });
+
+ [HarmonyPatch(typeof(Misc), nameof(Misc.CheckPermission), typeof(ICommandSender), typeof(PlayerPermissions))]
+ private static IEnumerable Transpiler(IEnumerable instructions)
+ {
+ yield return new(OpCodes.Ldarg_0);
+ yield return new(OpCodes.Ldarg_1);
+ yield return new(OpCodes.Call, AccessTools.Method(typeof(BaseCheckPatch), nameof(Check), new[] { typeof(ICommandSender), typeof(PlayerPermissions) }));
+ yield return new(OpCodes.Ret);
+ }
+
+ [HarmonyTranspiler]
+ [HarmonyPatch(typeof(Misc), nameof(Misc.CheckPermission), typeof(ICommandSender), typeof(PlayerPermissions[]))]
+ private static IEnumerable TranspilerMore(IEnumerable instructions)
+ {
+ yield return new(OpCodes.Ldarg_0);
+ yield return new(OpCodes.Ldarg_1);
+ yield return new(OpCodes.Call, AccessTools.Method(typeof(BaseCheckPatch), nameof(Check), new[] { typeof(ICommandSender), typeof(PlayerPermissions[]) }));
+ yield return new(OpCodes.Ret);
+ }
+ }
+}
\ No newline at end of file
diff --git a/EXILED/Exiled.Permissions/Features/MultipleGroups/MultiGroup.cs b/EXILED/Exiled.Permissions/Features/MultipleGroups/MultiGroup.cs
new file mode 100644
index 0000000000..01601b6d5d
--- /dev/null
+++ b/EXILED/Exiled.Permissions/Features/MultipleGroups/MultiGroup.cs
@@ -0,0 +1,105 @@
+// -----------------------------------------------------------------------
+//
+// Copyright (c) ExMod Team. All rights reserved.
+// Licensed under the CC BY-SA 3.0 license.
+//
+// -----------------------------------------------------------------------
+
+namespace Exiled.Permissions.Features.MultipleGroups
+{
+#pragma warning disable SA1401
+ using System;
+ using System.Collections.Generic;
+
+ using Exiled.API.Features;
+ using Exiled.API.Features.Pools;
+
+ using HarmonyLib;
+
+ ///
+ /// Main class for Multi-Group system.
+ ///
+ public static class MultiGroup
+ {
+ ///
+ /// A value indicating whether the system is active.
+ ///
+ internal static bool IsActive = false;
+
+ ///
+ /// Internal harmony instance.
+ ///
+ private static Harmony harmony;
+
+ ///
+ /// Gets a collection of all groups of all users.
+ ///
+ public static Dictionary Permissions { get; private set; }
+
+ ///
+ /// Main method for initializing MultiGroup.
+ ///
+ /// Whether the load should be forced, ignoring config.
+ public static void Init(bool force = false) // TODO: add list pooling
+ {
+ if (!Exiled.Permissions.Permissions.Instance.Config.UseMultiGroup && !force)
+ return;
+
+ List groups = ServerStatic.PermissionsHandler._config.GetStringList("AdditionalGroups");
+ Dictionary> users = new();
+
+ foreach (string entry in groups)
+ {
+ if (!entry.Contains(": "))
+ {
+ Log.Error($"Invalid entry at AdditionalGroups in config_remoteadmin.txt: {entry}");
+ continue;
+ }
+
+ int length = entry.IndexOf(": ", StringComparison.Ordinal);
+ string key = entry.Substring(0, length);
+ string value = entry.Substring(length + 2);
+
+ users.GetOrAdd(key, () => ListPool.Pool.Get()).Add(value);
+ }
+
+ if (users.Count == 0)
+ return;
+
+ try
+ {
+ harmony = new("exiled.permissions-multigroups");
+ harmony.CreateClassProcessor(typeof(BaseCheckPatch)).Patch();
+ }
+ catch (HarmonyException exception)
+ {
+ Log.Error($"Patch error while patching for MultiGroups! {exception}");
+ return;
+ }
+
+ IsActive = true;
+ Permissions = new();
+
+ foreach (KeyValuePair> kvp in users)
+ {
+ if (ServerStatic.PermissionsHandler.Members.TryGetValue(kvp.Key, out string group))
+ kvp.Value.Add(group);
+
+ Permissions[kvp.Key] = ListPool.Pool.ToArrayReturn(kvp.Value);
+ }
+ }
+
+ ///
+ /// Disables module and frees all resources.
+ ///
+ public static void Disable()
+ {
+ if (!IsActive)
+ return;
+
+ IsActive = false;
+ harmony.UnpatchAll("exiled.permissions-multigroups");
+ Permissions.Clear();
+ }
+ }
+}
\ No newline at end of file
diff --git a/EXILED/Exiled.Permissions/Permissions.cs b/EXILED/Exiled.Permissions/Permissions.cs
index eceb43810d..a279ac148e 100644
--- a/EXILED/Exiled.Permissions/Permissions.cs
+++ b/EXILED/Exiled.Permissions/Permissions.cs
@@ -8,6 +8,7 @@
namespace Exiled.Permissions
{
using Exiled.API.Features;
+ using Features.MultipleGroups;
using MEC;
@@ -36,12 +37,16 @@ public override void OnEnabled()
{
Extensions.Permissions.Create();
Extensions.Permissions.Reload();
+
+ MultiGroup.Init();
});
}
///
public override void OnDisabled()
{
+ MultiGroup.Disable();
+
base.OnDisabled();
Extensions.Permissions.Groups.Clear();