From 4f8bd7275bf3ad5914b5ac82caa5aeef3514f6c3 Mon Sep 17 00:00:00 2001
From: VALERA771 <72030575+VALERA771@users.noreply.github.com>
Date: Thu, 23 Jul 2026 16:15:18 +0300
Subject: [PATCH 1/2] feat: multigroup system
---
EXILED/Exiled.Events/Commands/TpsCommand.cs | 11 +-
EXILED/Exiled.Permissions/Config.cs | 6 +
.../Extensions/Permissions.cs | 47 ++++++--
.../Features/MultipleGroups/BaseCheckPatch.cs | 81 ++++++++++++++
.../Features/MultipleGroups/MultiGroup.cs | 105 ++++++++++++++++++
EXILED/Exiled.Permissions/Permissions.cs | 5 +
6 files changed, 240 insertions(+), 15 deletions(-)
create mode 100644 EXILED/Exiled.Permissions/Features/MultipleGroups/BaseCheckPatch.cs
create mode 100644 EXILED/Exiled.Permissions/Features/MultipleGroups/MultiGroup.cs
diff --git a/EXILED/Exiled.Events/Commands/TpsCommand.cs b/EXILED/Exiled.Events/Commands/TpsCommand.cs
index 14c0def05e..d23d2a3f88 100644
--- a/EXILED/Exiled.Events/Commands/TpsCommand.cs
+++ b/EXILED/Exiled.Events/Commands/TpsCommand.cs
@@ -30,15 +30,12 @@ public class TpsCommand : ICommand
///
public bool Execute(ArraySegment arguments, ICommandSender sender, out string response)
{
- double diff = Server.SmoothTps / Server.MaxTps;
- string color = diff switch
+ foreach (string data in ServerStatic.PermissionsHandler._config.GetStringList("AdditionalGroups"))
{
- > 0.9 => "green",
- > 0.5 => "yellow",
- _ => "red"
- };
+ Log.Info(data);
+ }
- response = $"{Server.SmoothTps}/{Server.MaxTps}";
+ response = "uwu";
return true;
}
}
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();
From 3396f2afe510773dad465164e973ed2538ddcc76 Mon Sep 17 00:00:00 2001
From: VALERA771 <72030575+VALERA771@users.noreply.github.com>
Date: Thu, 23 Jul 2026 16:23:11 +0300
Subject: [PATCH 2/2] revert: tps command chance
---
EXILED/Exiled.Events/Commands/TpsCommand.cs | 11 +++++++----
1 file changed, 7 insertions(+), 4 deletions(-)
diff --git a/EXILED/Exiled.Events/Commands/TpsCommand.cs b/EXILED/Exiled.Events/Commands/TpsCommand.cs
index d23d2a3f88..14c0def05e 100644
--- a/EXILED/Exiled.Events/Commands/TpsCommand.cs
+++ b/EXILED/Exiled.Events/Commands/TpsCommand.cs
@@ -30,12 +30,15 @@ public class TpsCommand : ICommand
///
public bool Execute(ArraySegment arguments, ICommandSender sender, out string response)
{
- foreach (string data in ServerStatic.PermissionsHandler._config.GetStringList("AdditionalGroups"))
+ double diff = Server.SmoothTps / Server.MaxTps;
+ string color = diff switch
{
- Log.Info(data);
- }
+ > 0.9 => "green",
+ > 0.5 => "yellow",
+ _ => "red"
+ };
- response = "uwu";
+ response = $"{Server.SmoothTps}/{Server.MaxTps}";
return true;
}
}