From bb7d282f8450be40811048462972d290fec16414 Mon Sep 17 00:00:00 2001 From: MedveMarci Date: Mon, 29 Jun 2026 17:29:13 +0200 Subject: [PATCH 01/47] Added PlaceholderReplacing to CustomKeycard --- .../API/Features/CustomModules/CustomKeycard.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/UncomplicatedCustomRoles/API/Features/CustomModules/CustomKeycard.cs b/UncomplicatedCustomRoles/API/Features/CustomModules/CustomKeycard.cs index 21872b7..23d937c 100644 --- a/UncomplicatedCustomRoles/API/Features/CustomModules/CustomKeycard.cs +++ b/UncomplicatedCustomRoles/API/Features/CustomModules/CustomKeycard.cs @@ -29,9 +29,9 @@ public class CustomKeycard : CustomModule private KeycardItem _keycardItem; internal ItemType KeycardType => ParseEnum("KeycardType", ItemType.None); - internal string ItemName => TryGetStringValue("ItemName", "Custom Keycard"); - internal string HolderName => TryGetStringValue("HolderName", "Unknown"); - internal string CardLabel => TryGetStringValue("CardLabel", string.Empty); + internal string ItemName => PlaceholderManager.ApplyPlaceholders(TryGetStringValue("ItemName", "Custom Keycard"), Player, CustomRole.Role); + internal string HolderName => PlaceholderManager.ApplyPlaceholders(TryGetStringValue("HolderName", "Unknown"), Player, CustomRole.Role); + internal string CardLabel => PlaceholderManager.ApplyPlaceholders(TryGetStringValue("CardLabel", string.Empty), Player, CustomRole.Role); internal KeycardLevels Permissions => new(ParseEnum("Permissions", DoorPermissionFlags.None)); internal Color KeycardColor => ParseColor("KeycardColor", Color.white); internal Color PermissionsColor => ParseColor("PermissionsColor", Color.white); From cdc186371e86e5e6f433cb4a588985a612e49aea Mon Sep 17 00:00:00 2001 From: MedveMarci Date: Mon, 29 Jun 2026 17:29:33 +0200 Subject: [PATCH 02/47] Fixed LastHuman checker with fake SCP role --- UncomplicatedCustomRoles/Patches/TeamPatch.cs | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/UncomplicatedCustomRoles/Patches/TeamPatch.cs b/UncomplicatedCustomRoles/Patches/TeamPatch.cs index d2f869a..a2b2391 100644 --- a/UncomplicatedCustomRoles/Patches/TeamPatch.cs +++ b/UncomplicatedCustomRoles/Patches/TeamPatch.cs @@ -25,6 +25,7 @@ using InventorySystem.Items; using InventorySystem.Searching; using MapGeneration.Distributors; +using PlayerRoles.PlayableScps.HumanTracker; using PlayerRoles.PlayableScps.Scp079; using PlayerStatsSystem; using UncomplicatedCustomRoles.API.Features; @@ -143,6 +144,7 @@ static IEnumerable TargetMethods() => .Concat(Declared(typeof(ExplosionGrenade), nameof(ExplosionGrenade.Explode))) .Concat(Declared(typeof(FlashbangGrenade), nameof(FlashbangGrenade.ServerFuseEnd))) .Concat(Declared(typeof(AttackerDamageHandler), nameof(AttackerDamageHandler.ProcessDamage))) + .Concat(Declared(typeof(LastHumanTracker), nameof(LastHumanTracker.IsLastTarget))) .Concat(Declared(typeof(Scp079Recontainer), nameof(Scp079Recontainer.OnServerRoleChanged))); static IEnumerable Declared(Type type, string name) => @@ -255,6 +257,34 @@ static bool Prefix(ReferenceHub hub, IDoorPermissionRequester requester, ref Doo } } + [HarmonyPatchCategory(TeamPatchManager.Category)] + [HarmonyPatch(typeof(PlayerRolesUtils), nameof(PlayerRolesUtils.IsSCP), new[] { typeof(ReferenceHub), typeof(bool) })] + internal class IsScpPatch + { + static bool Prefix(ReferenceHub hub, ref bool __result) + { + if (hub == null || !DisguiseTeam.List.TryGetValue(hub.PlayerId, out Team team)) + return true; + + __result = team == Team.SCPs; + return false; + } + } + + [HarmonyPatchCategory(TeamPatchManager.Category)] + [HarmonyPatch(typeof(PlayerRolesUtils), nameof(PlayerRolesUtils.IsHuman), new[] { typeof(ReferenceHub) })] + internal class IsHumanPatch + { + static bool Prefix(ReferenceHub hub, ref bool __result) + { + if (hub == null || !DisguiseTeam.List.TryGetValue(hub.PlayerId, out Team team)) + return true; + + __result = team != Team.SCPs && team != Team.Dead && team != Team.Flamingos; + return false; + } + } + [HarmonyPatchCategory(TeamPatchManager.Category)] [HarmonyPatch(typeof(Scp079Recontainer), nameof(Scp079Recontainer.OnServerRoleChanged))] public class Scp079RecontainerPatch From 6bb86c9334f3c817c29b247532f848b45db2f817 Mon Sep 17 00:00:00 2001 From: MedveMarci Date: Mon, 29 Jun 2026 17:50:17 +0200 Subject: [PATCH 03/47] Fixed FF in TeamPatch when the player is attacking the same Team --- UncomplicatedCustomRoles/Patches/TeamPatch.cs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/UncomplicatedCustomRoles/Patches/TeamPatch.cs b/UncomplicatedCustomRoles/Patches/TeamPatch.cs index a2b2391..5c7a1cb 100644 --- a/UncomplicatedCustomRoles/Patches/TeamPatch.cs +++ b/UncomplicatedCustomRoles/Patches/TeamPatch.cs @@ -21,6 +21,7 @@ using System.Linq; using System.Reflection; using System.Reflection.Emit; +using Footprinting; using InventorySystem.Disarming; using InventorySystem.Items; using InventorySystem.Searching; @@ -156,6 +157,28 @@ static IEnumerable Declared(Type type, string name) => static void Finalizer() => TeamFakeContext.Exit(); } + [HarmonyPatchCategory(TeamPatchManager.Category)] + [HarmonyPatch(typeof(AttackerDamageHandler), nameof(AttackerDamageHandler.ProcessDamage))] + internal class FriendlyFireDisguiseTranspiler + { + static IEnumerable Transpiler(IEnumerable instructions) + { + FieldInfo roleField = Field(typeof(Footprint), nameof(Footprint.Role)); + MethodInfo resolver = Method(typeof(FriendlyFireDisguiseTranspiler), nameof(ResolveAttackerRole)); + + foreach (CodeInstruction instruction in instructions) + { + if (instruction.opcode == OpCodes.Ldfld && instruction.operand is FieldInfo field && field == roleField) + yield return new CodeInstruction(OpCodes.Call, resolver); + else + yield return instruction; + } + } + + private static RoleTypeId ResolveAttackerRole(Footprint attacker) => + attacker.Hub?.GetRoleId() ?? attacker.Role; + } + [HarmonyPatchCategory(TeamPatchManager.Category)] [HarmonyPatch(typeof(ExplosionGrenade), nameof(ExplosionGrenade.ExplodeDestructible))] internal class GrenadeTranspiler From bd73b191b4d37e2d97435a827185f8d613b378f2 Mon Sep 17 00:00:00 2001 From: MedveMarci Date: Sun, 12 Jul 2026 15:08:27 +0200 Subject: [PATCH 04/47] Refactored event handling in CustomRoleEventHandler and PlayerEventPrefix for reducing TPS usage --- .../API/Features/CustomRoleEventHandler.cs | 46 ++++++++++++------- .../API/Features/SummonedCustomRole.cs | 28 +++++++++++ .../Patches/PlayerEventPrefix.cs | 20 +++++--- 3 files changed, 72 insertions(+), 22 deletions(-) diff --git a/UncomplicatedCustomRoles/API/Features/CustomRoleEventHandler.cs b/UncomplicatedCustomRoles/API/Features/CustomRoleEventHandler.cs index 76903b1..a2452b8 100644 --- a/UncomplicatedCustomRoles/API/Features/CustomRoleEventHandler.cs +++ b/UncomplicatedCustomRoles/API/Features/CustomRoleEventHandler.cs @@ -25,11 +25,22 @@ public class CustomRoleEventHandler public ICustomRole Role => SummonedInstance.Role; public List Listeners { get; } = new(); + + private static int _activeListeners; internal CustomRoleEventHandler(SummonedCustomRole summonedInstance) { SummonedInstance = summonedInstance; LoadListeners(); + _activeListeners += Listeners.Count; + } + + internal void Unload() + { + _activeListeners -= Listeners.Count; + if (_activeListeners < 0) + _activeListeners = 0; + Listeners.Clear(); } private void LoadListeners() @@ -62,34 +73,37 @@ private void LoadListeners() internal void InvokeSafely(IPlayerEvent playerEvent) { - if (playerEvent is ICancellableEvent cancellableEvent && !cancellableEvent.IsAllowed) + if (Listeners.Count == 0) return; - Listener listener = Listeners.FirstOrDefault(l => l.Event == playerEvent.GetType()); + if (playerEvent is ICancellableEvent { IsAllowed: false }) + return; - listener?.Method.Invoke(listener.Instance, new object[] { playerEvent }); + Type eventType = playerEvent.GetType(); + foreach (Listener listener in Listeners) + if (listener.Event == eventType) + { + listener.Method.Invoke(listener.Instance, [playerEvent]); + return; + } } internal static void InvokeAll(IPlayerEvent ev) { - foreach (SummonedCustomRole summonedCustomRole in SummonedCustomRole.List.Values) - summonedCustomRole.EventHandler?.InvokeSafely(ev); + if (_activeListeners == 0) + return; + + foreach (KeyValuePair pair in SummonedCustomRole.List) + pair.Value.EventHandler?.InvokeSafely(ev); } } - public class Listener + public class Listener(Type @event, MethodInfo method, object instance) { - public Type Event { get; } - - public MethodInfo Method { get; } + public Type Event { get; } = @event; - public object Instance { get; } + public MethodInfo Method { get; } = method; - public Listener(Type @event, MethodInfo method, object instance) - { - Event = @event; - Method = method; - Instance = instance; - } + public object Instance { get; } = instance; } } diff --git a/UncomplicatedCustomRoles/API/Features/SummonedCustomRole.cs b/UncomplicatedCustomRoles/API/Features/SummonedCustomRole.cs index 61de6d5..874e9b9 100644 --- a/UncomplicatedCustomRoles/API/Features/SummonedCustomRole.cs +++ b/UncomplicatedCustomRoles/API/Features/SummonedCustomRole.cs @@ -137,6 +137,10 @@ public class SummonedCustomRole private bool _isRegeneratingHume { get; set; } private List _customModules { get; } + + private int _eventModuleCount; + + internal static int EventTriggeredModuleTotal; internal RoleTypeId Appearance => Role.RoleAppearance != Role.Role ? Role.RoleAppearance : RoleTypeId.None; @@ -165,6 +169,9 @@ internal SummonedCustomRole(Player player, ICustomRole role, Triplet m.TriggerOnEvents.Count > 0); + EventTriggeredModuleTotal += _eventModuleCount; + if (Role.Team is not null && Role.Team != Role.Role.GetTeam()) { EvaluateRoleBase(); @@ -378,6 +385,13 @@ public void Remove() LogManager.Error($"Failed to act SummonedCustomRole::Remove() - {e.GetType().FullName}: {e.Message}\n{e.StackTrace}"); } + EventHandler?.Unload(); + + EventTriggeredModuleTotal -= _eventModuleCount; + if (EventTriggeredModuleTotal < 0) + EventTriggeredModuleTotal = 0; + _eventModuleCount = 0; + _customModules.Clear(); _internalValid = false; } @@ -461,7 +475,14 @@ public bool TryGetModule(out T module) where T : CustomModule public void AddModule(Type type, Dictionary? args = null) { if (CustomModule.FastAdd(type, this, args) is CustomModule module) + { _customModules.Add(module); + if (module.TriggerOnEvents.Count > 0) + { + _eventModuleCount++; + EventTriggeredModuleTotal++; + } + } } #nullable disable @@ -473,6 +494,13 @@ public void RemoveModule() where T : CustomModule { if (TryGetModule(out T module)) { + if (module.TriggerOnEvents.Count > 0) + { + _eventModuleCount--; + EventTriggeredModuleTotal--; + if (EventTriggeredModuleTotal < 0) + EventTriggeredModuleTotal = 0; + } module.OnRemoved(); _customModules.Remove(module); } diff --git a/UncomplicatedCustomRoles/Patches/PlayerEventPrefix.cs b/UncomplicatedCustomRoles/Patches/PlayerEventPrefix.cs index a28b6ec..e875b3a 100644 --- a/UncomplicatedCustomRoles/Patches/PlayerEventPrefix.cs +++ b/UncomplicatedCustomRoles/Patches/PlayerEventPrefix.cs @@ -24,7 +24,9 @@ namespace UncomplicatedCustomRoles.Patches { internal class PlayerEventPrefix { - private static IEnumerable PatchedMethods = new List(); + private static IEnumerable _patchedMethods = new List(); + + private static readonly Dictionary EventNameCache = new(); private static void Prefix(IPlayerEvent ev) { @@ -32,9 +34,15 @@ private static void Prefix(IPlayerEvent ev) { CustomRoleEventHandler.InvokeAll(ev); - if (ev.Player is not null && ev.Player.TryGetSummonedInstance(out SummonedCustomRole customRole)) + if (SummonedCustomRole.EventTriggeredModuleTotal > 0 + && ev.Player is not null && ev.Player.TryGetSummonedInstance(out SummonedCustomRole customRole)) { - string name = ev.GetType().Name.Replace("EventArgs", string.Empty).Replace("Player", string.Empty); + Type eventType = ev.GetType(); + if (!EventNameCache.TryGetValue(eventType, out string name)) + { + name = eventType.Name.Replace("EventArgs", string.Empty).Replace("Player", string.Empty); + EventNameCache[eventType] = name; + } foreach (CustomModule module in customRole.CustomModules) if (module.TriggerOnEvents.Contains(name)) @@ -52,15 +60,15 @@ internal static void Patch(Harmony harmony) { HarmonyMethod prefixMethod = new(typeof(PlayerEventPrefix).GetMethod("Prefix", BindingFlags.Static | BindingFlags.NonPublic)); - PatchedMethods = typeof(PlayerEvents).GetMethods().Where(m => m.Name.StartsWith("On") && m.GetParameters().Length > 0 && typeof(IPlayerEvent).IsAssignableFrom(m.GetParameters()[0].ParameterType)); + _patchedMethods = typeof(PlayerEvents).GetMethods().Where(m => m.Name.StartsWith("On") && m.GetParameters().Length > 0 && typeof(IPlayerEvent).IsAssignableFrom(m.GetParameters()[0].ParameterType)); - foreach (MethodInfo method in PatchedMethods) + foreach (MethodInfo method in _patchedMethods) harmony.Patch(method, prefix: prefixMethod); } internal static void Unpatch(Harmony harmony) { - foreach (MethodInfo method in PatchedMethods) + foreach (MethodInfo method in _patchedMethods) harmony.Unpatch(method, HarmonyPatchType.All); } } From 650614aab6bd03796cd99d59411c030947d3b3a6 Mon Sep 17 00:00:00 2001 From: MedveMarci Date: Sun, 12 Jul 2026 15:09:57 +0200 Subject: [PATCH 05/47] Refactored role attribute checks in ImportManager and PluginImportManager; improved role listing in Percentages; imported roles now correctly getting reloaded and shown in Percentages --- .../Commands/Percentages.cs | 18 ++++++++---------- UncomplicatedCustomRoles/Commands/Reload.cs | 5 +++++ .../Manager/ImportManager.cs | 2 +- .../Manager/PluginImportManager.cs | 2 +- 4 files changed, 15 insertions(+), 12 deletions(-) diff --git a/UncomplicatedCustomRoles/Commands/Percentages.cs b/UncomplicatedCustomRoles/Commands/Percentages.cs index 15d1b36..e57a333 100644 --- a/UncomplicatedCustomRoles/Commands/Percentages.cs +++ b/UncomplicatedCustomRoles/Commands/Percentages.cs @@ -34,19 +34,17 @@ public bool Executor(List args, ICommandSender sender, out string respon foreach (RoleTypeId role in Enum.GetValues(typeof(RoleTypeId))) { - IEnumerable roles = CustomRole.List.Where(r => r.SpawnSettings?.CanReplaceRoles != null && r.SpawnSettings.CanReplaceRoles.Contains(role)); - if (roles.Any()) + IEnumerable manualRoles = CustomRole.List.Where(r => r.SpawnSettings?.CanReplaceRoles == null || !r.SpawnSettings.CanReplaceRoles.Any()); + if (manualRoles.Any()) { - float total = roles.Sum(r => r.SpawnSettings.SpawnChance); - response += $"\n\n{(total >= 100 ? $"❗" : "✔️")} {role.GetFullName()} ({roles.Count()})"; - response += $"\nChanche of spawning as a CustomRole: {total}%\nChanche of spawning as a regular role: {100 - total}%"; - - if (detailed) - foreach (ICustomRole customRole in roles.Where(r => r.SpawnSettings.SpawnChance > 0)) - response += $"\n ∟ {customRole} - {customRole.SpawnSettings.SpawnChance}%"; + response += $"\n\nℹ️ Roles without a linked vanilla role ({manualRoles.Count()}) - spawned manually or by another plugin:"; + foreach (ICustomRole customRole in manualRoles) + response += customRole.SpawnSettings is not null && customRole.SpawnSettings.SpawnChance > 0 + ? $"\n ∟ {customRole} - {customRole.SpawnSettings.SpawnChance}%" + : $"\n ∟ {customRole}"; } } - + response += "\nOwO"; // We want to render everything return true; diff --git a/UncomplicatedCustomRoles/Commands/Reload.cs b/UncomplicatedCustomRoles/Commands/Reload.cs index b582706..6eee546 100644 --- a/UncomplicatedCustomRoles/Commands/Reload.cs +++ b/UncomplicatedCustomRoles/Commands/Reload.cs @@ -15,6 +15,7 @@ using System.Linq; using UncomplicatedCustomRoles.API.Features; using UncomplicatedCustomRoles.API.Interfaces; +using UncomplicatedCustomRoles.Compatibility; using UncomplicatedCustomRoles.Extensions; using UncomplicatedCustomRoles.Manager; @@ -40,6 +41,10 @@ public bool Executor(List arguments, ICommandSender sender, out string r FileConfigs.LoadAll(); FileConfigs.LoadAll(Server.Port.ToString()); ImportManager.Reload(); + + foreach (KeyValuePair oldRole in oldRoles) + if (!CustomRole.CustomRoles.ContainsKey(oldRole.Key) && !CompatibilityManager.RolePaths.ContainsKey(oldRole.Value)) + CustomRole.Register(oldRole.Value); IEnumerable removedRoles = oldRoles.Keys.Except(CustomRole.CustomRoles.Keys); diff --git a/UncomplicatedCustomRoles/Manager/ImportManager.cs b/UncomplicatedCustomRoles/Manager/ImportManager.cs index 5e24003..8e6f08d 100644 --- a/UncomplicatedCustomRoles/Manager/ImportManager.cs +++ b/UncomplicatedCustomRoles/Manager/ImportManager.cs @@ -67,7 +67,7 @@ private static void Actor() try { object[] attribs = type.GetCustomAttributes(typeof(PluginCustomRole), false); - if (attribs != null && attribs.Length > 0 && (type.IsSubclassOf(typeof(ICustomRole)) || type.IsSubclassOf(typeof(CustomRole)) || type.IsSubclassOf(typeof(EventCustomRole)))) + if (attribs != null && attribs.Length > 0 && typeof(ICustomRole).IsAssignableFrom(type) && !type.IsAbstract && !type.IsInterface) { ActivePlugins.TryAdd(plugin.Key); diff --git a/UncomplicatedCustomRoles/Manager/PluginImportManager.cs b/UncomplicatedCustomRoles/Manager/PluginImportManager.cs index 420024d..41aeee4 100644 --- a/UncomplicatedCustomRoles/Manager/PluginImportManager.cs +++ b/UncomplicatedCustomRoles/Manager/PluginImportManager.cs @@ -39,7 +39,7 @@ private static void ImportCustomRoles(Assembly assembly) try { object[] attribs = type.GetCustomAttributes(typeof(PluginCustomRole), false); - if (attribs != null && attribs.Length > 0 && (type.IsSubclassOf(typeof(ICustomRole)) || type.IsSubclassOf(typeof(CustomRole)) || type.IsSubclassOf(typeof(EventCustomRole)))) + if (attribs != null && attribs.Length > 0 && typeof(ICustomRole).IsAssignableFrom(type) && !type.IsAbstract && !type.IsInterface) { ICustomRole Role = Activator.CreateInstance(type) as ICustomRole; From e1d70d020aac337b86d79000f911394f86b4ee7f Mon Sep 17 00:00:00 2001 From: MedveMarci Date: Sun, 12 Jul 2026 15:10:39 +0200 Subject: [PATCH 06/47] Optimized TeamPatch; Improved FakeRole getter --- UncomplicatedCustomRoles/Patches/TeamPatch.cs | 70 +++++++++++-------- 1 file changed, 39 insertions(+), 31 deletions(-) diff --git a/UncomplicatedCustomRoles/Patches/TeamPatch.cs b/UncomplicatedCustomRoles/Patches/TeamPatch.cs index 5c7a1cb..bc32fe6 100644 --- a/UncomplicatedCustomRoles/Patches/TeamPatch.cs +++ b/UncomplicatedCustomRoles/Patches/TeamPatch.cs @@ -9,6 +9,7 @@ */ using Achievements.Handlers; +using Footprinting; using HarmonyLib; using Interactables.Interobjects.DoorUtils; using Mirror; @@ -21,7 +22,6 @@ using System.Linq; using System.Reflection; using System.Reflection.Emit; -using Footprinting; using InventorySystem.Disarming; using InventorySystem.Items; using InventorySystem.Searching; @@ -41,23 +41,17 @@ internal class PlayerRoleManagerPatch { static bool Prefix(PlayerRoleManager __instance, ref PlayerRoleBase __result) { - if (__instance.Hub == null || __instance.Hub.netId == 0) + + ReferenceHub hub = __instance.Hub; + if (hub is null || !DisguiseTeam.RoleBaseList.TryGetValue(hub.PlayerId, out PlayerRoleBase role) || role is null) return true; - + + if (RoleSerializationContext.Active) return true; - if (__instance.Hub is not null && DisguiseTeam.RoleBaseList.TryGetValue(__instance.Hub.PlayerId, out PlayerRoleBase role)) - { - if (role is null) - LogManager.Error($"Disguised role for player {__instance.Hub.PlayerId} is null!"); - - __result = role; - - return false; - } - - return true; + __result = role; + return false; } } @@ -131,6 +125,33 @@ static bool Prefix(ReferenceHub hub, ref RoleTypeId __result) return true; } + + internal static RoleTypeId GetCombatRoleId(ReferenceHub hub) + { + if (hub != null && DisguiseTeam.List.TryGetValue(hub.PlayerId, out Team team) && + _roleTeam.TryGetValue(team, out RoleTypeId fakeRole)) + return fakeRole; + + return hub.GetRoleId(); + } + } + + [HarmonyPatchCategory(TeamPatchManager.Category)] + [HarmonyPatch(typeof(AttackerDamageHandler), nameof(AttackerDamageHandler.ProcessDamage))] + internal class ProcessDamageRolePatch + { + static IEnumerable Transpiler(IEnumerable instructions) + { + List code = new(instructions); + MethodInfo original = Method(typeof(PlayerRolesUtils), nameof(PlayerRolesUtils.GetRoleId), [typeof(ReferenceHub)]); + MethodInfo replacement = Method(typeof(PlayerRolesUtilsPatch), nameof(PlayerRolesUtilsPatch.GetCombatRoleId)); + + foreach (CodeInstruction instruction in code) + if (instruction.opcode == OpCodes.Call && instruction.operand is MethodInfo method && method == original) + instruction.operand = replacement; + + return code; + } } [HarmonyPatchCategory(TeamPatchManager.Category)] @@ -158,25 +179,12 @@ static IEnumerable Declared(Type type, string name) => } [HarmonyPatchCategory(TeamPatchManager.Category)] - [HarmonyPatch(typeof(AttackerDamageHandler), nameof(AttackerDamageHandler.ProcessDamage))] - internal class FriendlyFireDisguiseTranspiler + [HarmonyPatch(typeof(Footprint), MethodType.Constructor, new[] { typeof(ReferenceHub) })] + internal class FootprintContextPatch { - static IEnumerable Transpiler(IEnumerable instructions) - { - FieldInfo roleField = Field(typeof(Footprint), nameof(Footprint.Role)); - MethodInfo resolver = Method(typeof(FriendlyFireDisguiseTranspiler), nameof(ResolveAttackerRole)); - - foreach (CodeInstruction instruction in instructions) - { - if (instruction.opcode == OpCodes.Ldfld && instruction.operand is FieldInfo field && field == roleField) - yield return new CodeInstruction(OpCodes.Call, resolver); - else - yield return instruction; - } - } + static void Prefix() => TeamFakeContext.Enter(); - private static RoleTypeId ResolveAttackerRole(Footprint attacker) => - attacker.Hub?.GetRoleId() ?? attacker.Role; + static void Finalizer() => TeamFakeContext.Exit(); } [HarmonyPatchCategory(TeamPatchManager.Category)] From dee8ee14f1fdb08116aa2114473e046027538472 Mon Sep 17 00:00:00 2001 From: MedveMarci Date: Sun, 12 Jul 2026 15:20:29 +0200 Subject: [PATCH 07/47] Added null check for placeholder origin --- UncomplicatedCustomRoles/Manager/PlaceholderManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UncomplicatedCustomRoles/Manager/PlaceholderManager.cs b/UncomplicatedCustomRoles/Manager/PlaceholderManager.cs index f604c5d..7cd7770 100644 --- a/UncomplicatedCustomRoles/Manager/PlaceholderManager.cs +++ b/UncomplicatedCustomRoles/Manager/PlaceholderManager.cs @@ -20,7 +20,7 @@ namespace UncomplicatedCustomRoles.Manager public class PlaceholderManager { - public static string ApplyPlaceholders(string origin, Player player, ICustomRole? role) => origin.BulkReplace(new() + public static string ApplyPlaceholders(string? origin, Player player, ICustomRole? role) => (origin ?? string.Empty).BulkReplace(new() { { "nick", player.Nickname }, { "displayname", player.DisplayName }, From 014c3a2a6f1cdafaa7b635c4adc21eb99360a5ff Mon Sep 17 00:00:00 2001 From: MedveMarci Date: Sun, 12 Jul 2026 15:27:57 +0200 Subject: [PATCH 08/47] Improved CustomKeycard Flag --- .../Features/CustomModules/CustomKeycard.cs | 76 +++++++++++++++---- 1 file changed, 62 insertions(+), 14 deletions(-) diff --git a/UncomplicatedCustomRoles/API/Features/CustomModules/CustomKeycard.cs b/UncomplicatedCustomRoles/API/Features/CustomModules/CustomKeycard.cs index 23d937c..32c3621 100644 --- a/UncomplicatedCustomRoles/API/Features/CustomModules/CustomKeycard.cs +++ b/UncomplicatedCustomRoles/API/Features/CustomModules/CustomKeycard.cs @@ -9,7 +9,9 @@ */ using System; +using System.Collections; using System.Collections.Generic; +using System.Linq; using Interactables.Interobjects.DoorUtils; using InventorySystem; using LabApi.Features.Wrappers; @@ -21,18 +23,39 @@ namespace UncomplicatedCustomRoles.API.Features.CustomModules; public class CustomKeycard : CustomModule { - public override List RequiredArgs => new() + public override List RequiredArgs => ["KeycardType"]; + + private KeycardItem _keycardItem; + + private static readonly Dictionary KeycardTypeAliases = new(StringComparer.OrdinalIgnoreCase) { - "KeycardType" + { "Management", ItemType.KeycardCustomManagement }, + { "Metal", ItemType.KeycardCustomMetalCase }, + { "MetalCase", ItemType.KeycardCustomMetalCase }, + { "Site02", ItemType.KeycardCustomSite02 }, + { "Site", ItemType.KeycardCustomSite02 }, + { "TaskForce", ItemType.KeycardCustomTaskForce }, }; - private KeycardItem _keycardItem; + internal ItemType KeycardType + { + get + { + string raw = TryGetStringValue("KeycardType")?.Trim(); + if (string.IsNullOrEmpty(raw)) + return ItemType.None; + + if (KeycardTypeAliases.TryGetValue(raw, out ItemType alias)) + return alias; + + return Enum.TryParse(raw, true, out ItemType parsed) ? parsed : ItemType.None; + } + } - internal ItemType KeycardType => ParseEnum("KeycardType", ItemType.None); internal string ItemName => PlaceholderManager.ApplyPlaceholders(TryGetStringValue("ItemName", "Custom Keycard"), Player, CustomRole.Role); internal string HolderName => PlaceholderManager.ApplyPlaceholders(TryGetStringValue("HolderName", "Unknown"), Player, CustomRole.Role); internal string CardLabel => PlaceholderManager.ApplyPlaceholders(TryGetStringValue("CardLabel", string.Empty), Player, CustomRole.Role); - internal KeycardLevels Permissions => new(ParseEnum("Permissions", DoorPermissionFlags.None)); + internal KeycardLevels Permissions => BuildPermissions(); internal Color KeycardColor => ParseColor("KeycardColor", Color.white); internal Color PermissionsColor => ParseColor("PermissionsColor", Color.white); internal Color LabelColor => ParseColor("LabelColor", Color.white); @@ -40,8 +63,8 @@ public class CustomKeycard : CustomModule internal string SerialLabel => TryGetStringValue("SerialLabel", "000000000000"); internal int RankIndex => TryGetCastedValue("RankIndex", 0); - private const string ValidKeycardTypes = - "KeycardCustomManagement, KeycardCustomMetalCase, KeycardCustomSite02, KeycardCustomTaskForce"; + private static readonly string ValidKeycardTypes = + string.Join(", ", KeycardTypeAliases.Keys.OrderBy(k => k)); public override void OnAdded() { @@ -82,17 +105,42 @@ public override void OnAdded() }); base.OnAdded(); } + + private KeycardLevels BuildPermissions() + { + bool hasLevels = HasArg("ContainmentLevel") || HasArg("ArmoryLevel") || HasArg("AdminLevel"); + + KeycardLevels levels = new( + TryGetCastedValue("ContainmentLevel", 0), + TryGetCastedValue("ArmoryLevel", 0), + TryGetCastedValue("AdminLevel", 0)); - private T ParseEnum(string param, T def) where T : struct, Enum + DoorPermissionFlags rawFlags = ParseFlags("Permissions"); + + if (!hasLevels && rawFlags == DoorPermissionFlags.None) + return levels; + + return new KeycardLevels(levels.Permissions | rawFlags); + } + + private bool HasArg(string param) => Args is not null && Args.ContainsKey(param); + + private DoorPermissionFlags ParseFlags(string param) { - string raw = TryGetStringValue(param); - if (raw is null) - return def; + if (Args is null || !Args.TryGetValue(param, out object raw) || raw is null) + return DoorPermissionFlags.None; + + string joined = raw as string ?? (raw is IEnumerable enumerable + ? string.Join(",", enumerable.Cast().Where(o => o is not null).Select(o => o.ToString())) + : raw.ToString()); - if (!Enum.TryParse(raw, true, out T result)) + if (string.IsNullOrWhiteSpace(joined)) + return DoorPermissionFlags.None; + + if (!Enum.TryParse(joined.Replace(" ", string.Empty), true, out DoorPermissionFlags result)) { - LogManager.Warn($"[CustomKeycard] Invalid value '{raw}' for '{param}'. Valid values: {string.Join(", ", Enum.GetNames(typeof(T)))}. Using default: {def}"); - return def; + LogManager.Warn($"[CustomKeycard] Invalid value '{joined}' for '{param}'. Valid flags: {string.Join(", ", Enum.GetNames(typeof(DoorPermissionFlags)))}. Ignoring it."); + return DoorPermissionFlags.None; } return result; From f6ac4a63cd36a69c137f91e539908dc4d2c72e56 Mon Sep 17 00:00:00 2001 From: MedveMarci Date: Sun, 12 Jul 2026 20:54:34 +0200 Subject: [PATCH 09/47] Prevented spamming in EscapeController --- .../Features/Controllers/EscapeController.cs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/UncomplicatedCustomRoles/API/Features/Controllers/EscapeController.cs b/UncomplicatedCustomRoles/API/Features/Controllers/EscapeController.cs index a33c5d8..12a4952 100644 --- a/UncomplicatedCustomRoles/API/Features/Controllers/EscapeController.cs +++ b/UncomplicatedCustomRoles/API/Features/Controllers/EscapeController.cs @@ -18,6 +18,8 @@ internal class EscapeController : MonoBehaviour { private SummonedCustomRole _role; + private bool _wasInEscapeZone; + public void Init(SummonedCustomRole role) { _role = role; @@ -25,9 +27,22 @@ public void Init(SummonedCustomRole role) private void Update() { + if (_role is null || PlayerEventHandler.Instance is null) + return; + + bool inZone = false; foreach (Bounds escapeZone in global::Escape.EscapeZones) if (escapeZone.Contains(_role.Player.Position)) - PlayerEventHandler.Instance.OnEscaping(new(_role.Player.ReferenceHub, _role.Player.Role, RoleTypeId.ChaosConscript, global::Escape.EscapeScenarioType.Custom, escapeZone)); + { + inZone = true; + + if (!_wasInEscapeZone) + PlayerEventHandler.Instance.OnEscaping(new(_role.Player.ReferenceHub, _role.Player.Role, RoleTypeId.ChaosConscript, global::Escape.EscapeScenarioType.Custom, escapeZone)); + + break; + } + + _wasInEscapeZone = inZone; } private void OnDestroy() From 4fde917f8a6b2ee0c07921629513c98958ccc9aa Mon Sep 17 00:00:00 2001 From: MedveMarci Date: Sun, 12 Jul 2026 20:56:04 +0200 Subject: [PATCH 10/47] Fixed a hume shield config with amount: 0 but maximum > 0 (start empty,regenerate over time) never applied its maximum --- .../API/Features/Behaviour/HumeShieldBehaviour.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UncomplicatedCustomRoles/API/Features/Behaviour/HumeShieldBehaviour.cs b/UncomplicatedCustomRoles/API/Features/Behaviour/HumeShieldBehaviour.cs index d0d8d07..6fff30c 100644 --- a/UncomplicatedCustomRoles/API/Features/Behaviour/HumeShieldBehaviour.cs +++ b/UncomplicatedCustomRoles/API/Features/Behaviour/HumeShieldBehaviour.cs @@ -46,7 +46,7 @@ public class HumeShieldBehaviour /// public void Apply(Player player) { - if (Amount > 0) + if (Amount > 0 || Maximum > 0) { player.HumeShield = Amount; player.MaxHumeShield = Maximum; From c4ca47d8e751b60a2741accbfb553ed8967e04f1 Mon Sep 17 00:00:00 2001 From: MedveMarci Date: Sun, 12 Jul 2026 21:29:52 +0200 Subject: [PATCH 11/47] Refactored list initializations to use array syntax for consistency; Added Role and CustomFlag value validators; Some improvements in code; Cleaned up and formatted solution; --- .../API/Attributes/PluginCustomRole.cs | 13 +- .../API/Enums/CustomFlags.cs | 55 +- .../API/Enums/DamageType.cs | 115 +- .../API/Enums/LoadStatusType.cs | 19 +- .../API/Enums/SpawnType.cs | 27 +- .../API/Features/Behaviour/AhpBehaviour.cs | 87 +- .../API/Features/Behaviour/HealthBehaviour.cs | 45 +- .../Features/Behaviour/HumeShieldBehaviour.cs | 82 +- .../API/Features/Behaviour/SpawnBehaviour.cs | 111 +- .../Features/Behaviour/StaminaBehaviour.cs | 48 +- .../Features/Controllers/EscapeController.cs | 58 +- .../API/Features/Controllers/Presence.cs | 29 +- .../Controllers/SchematicController.cs | 82 +- .../API/Features/CustomInfo.cs | 296 +- .../CustomModules/AmnesiaResistance.cs | 6 +- .../CustomModules/ChangeAppearanceOnKill.cs | 55 +- .../CustomModules/ColorfulNickname.cs | 43 +- .../Features/CustomModules/ColorfulRaName.cs | 24 +- .../Features/CustomModules/CustomInfoOrder.cs | 54 +- .../Features/CustomModules/CustomKeycard.cs | 155 +- .../Features/CustomModules/CustomModule.cs | 540 +-- .../CustomModules/CustomPermissions.cs | 55 +- .../CustomModules/CustomScpAnnouncer.cs | 14 +- .../CustomModules/DamageResistance.cs | 362 +- .../Features/CustomModules/DoNotTrigger096.cs | 12 +- .../CustomModules/DoNotTriggerTeslaGates.cs | 23 +- .../Features/CustomModules/DropItemOnDeath.cs | 49 +- .../CustomModules/DropNothingOnDeath.cs | 13 +- .../Features/CustomModules/FullCandyBag.cs | 40 +- .../API/Features/CustomModules/ItemBan.cs | 34 +- .../CustomModules/KeepInventoryOnEscape.cs | 17 +- .../API/Features/CustomModules/LifeStealer.cs | 35 +- .../API/Features/CustomModules/NoUnitName.cs | 12 +- .../CustomModules/NotAffectedByAppearance.cs | 12 +- .../CustomModules/PacifismUntilDamage.cs | 12 +- .../API/Features/CustomModules/Schematic.cs | 51 +- .../Features/CustomModules/SilentAnnouncer.cs | 12 +- .../Features/CustomModules/SilentWalker.cs | 12 +- .../Features/CustomModules/TutorialRagdoll.cs | 12 +- .../API/Features/CustomModules/Wardrobe.cs | 54 +- .../API/Features/CustomRole.cs | 578 ++- .../API/Features/CustomRoleEventHandler.cs | 142 +- .../API/Features/DisguiseTeam.cs | 93 +- .../API/Features/Effect.cs | 43 +- .../API/Features/Escape.cs | 31 +- .../API/Features/EventCustomRole.cs | 3114 +++++++++-------- .../API/Features/InfiniteEffect.cs | 85 +- .../API/Features/LogEntry.cs | 87 +- .../API/Features/Messages/OwnerMessage.cs | 27 +- .../API/Features/Messages/PresenceMessage.cs | 42 +- .../API/Features/Messages/ShareLogMessage.cs | 32 +- .../API/Features/Spawn.cs | 51 +- .../API/Features/SpawnPoint.cs | 388 +- .../API/Features/SummonedCustomRole.cs | 1283 +++---- .../API/Interfaces/ICustomRole.cs | 78 +- .../API/Interfaces/IEffect.cs | 21 +- .../API/Interfaces/IUCRCommand.cs | 23 +- .../API/Struct/Quadruple.cs | 79 +- .../API/Struct/Triplet.cs | 82 +- .../Commands/CommandParent.cs | 121 +- .../Commands/CustomInfo.cs | 92 +- UncomplicatedCustomRoles/Commands/Debug.cs | 216 +- UncomplicatedCustomRoles/Commands/Errors.cs | 126 +- UncomplicatedCustomRoles/Commands/Generate.cs | 59 +- UncomplicatedCustomRoles/Commands/Info.cs | 122 +- UncomplicatedCustomRoles/Commands/List.cs | 68 +- UncomplicatedCustomRoles/Commands/LogShare.cs | 100 +- UncomplicatedCustomRoles/Commands/Owner.cs | 54 +- .../Commands/Percentages.cs | 73 +- UncomplicatedCustomRoles/Commands/Reload.cs | 65 +- UncomplicatedCustomRoles/Commands/Role.cs | 76 +- UncomplicatedCustomRoles/Commands/Spawn.cs | 155 +- .../Commands/SpawnPoint.cs | 385 +- UncomplicatedCustomRoles/Commands/Update.cs | 74 +- UncomplicatedCustomRoles/Commands/Version.cs | 48 +- .../Compatibility/CompatibilityManager.cs | 286 +- .../Compatibility/ErrorCustomRole.cs | 96 +- .../Compatibility/OutdatedCustomRole.cs | 29 +- .../BonolisSpawnBehaviour.cs | 45 +- .../Enums/ExiledAmmoType.cs | 21 +- .../Enums/ExiledRoomType.cs | 136 +- .../Enums/ExiledZoneType.cs | 26 +- .../FossuonHealthBehaviour.cs | 23 +- .../PreviousVersionRoles/BonolisCustomRole.cs | 191 +- .../PreviousVersionRoles/FossuonCustomRole.cs | 223 +- .../IPreviousVersionRole.cs | 15 +- .../PreviousVersionRole.cs | 225 +- UncomplicatedCustomRoles/Config.cs | 91 +- .../Events/EventHandlerBase.cs | 71 +- .../Events/PlayerEventHandler.cs | 705 ++-- .../Events/ScpEventHandler.cs | 122 +- .../Events/ServerEventHandler.cs | 122 +- .../Extensions/CompatibilityExtension.cs | 84 +- .../Extensions/DictionaryExtension.cs | 141 +- .../Extensions/ListExtension.cs | 55 +- .../Extensions/MirrorExtension.cs | 948 ++--- .../Extensions/PlayerExtension.cs | 401 ++- .../Extensions/RoleExtension.cs | 58 +- .../Extensions/StringExtension.cs | 132 +- .../Extensions/Vector3Extension.cs | 93 +- .../Integrations/DynamicInvoke.cs | 235 +- UncomplicatedCustomRoles/Integrations/ECI.cs | 21 +- .../Integrations/LabApiExtensions.cs | 53 +- .../Integrations/RespawnTimer.cs | 71 +- .../Integrations/SLWardobe.cs | 107 +- .../Integrations/ScriptedEvents.cs | 288 +- UncomplicatedCustomRoles/Integrations/UCI.cs | 80 +- UncomplicatedCustomRoles/Integrations/UCT.cs | 51 +- .../Manager/FileConfigs.cs | 125 +- .../Manager/ImportManager.cs | 109 +- .../Manager/LogManager.cs | 151 +- .../Manager/MapSpawnValidator.cs | 87 + .../Manager/NET/HttpManager.cs | 334 +- .../Manager/NET/SpawnPointApiCommunicator.cs | 266 +- .../Manager/NET/VersionInfo.cs | 82 +- .../Manager/PlaceholderManager.cs | 55 +- .../Manager/PluginImportManager.cs | 82 +- .../Manager/RoleValidator.cs | 557 +++ .../Manager/SpawnManager.cs | 995 +++--- .../Manager/VersionManager.cs | 132 +- .../Manager/YamlFlagsHandler.cs | 79 +- UncomplicatedCustomRoles/Patches/Announcer.cs | 97 +- .../Patches/ChangeCustomPlayerInfoPatch.cs | 37 +- .../Patches/MakingNoise.cs | 25 +- .../Patches/PlayerEventPrefix.cs | 91 +- .../Patches/PlayerInfoSyncPatch.cs | 58 +- .../Patches/Scp3114StranglePrefix.cs | 51 +- .../Patches/ServerNamePatch.cs | 17 +- .../Patches/SetRolePatch.cs | 73 +- .../Patches/StaminaUsagePatch.cs | 33 +- UncomplicatedCustomRoles/Patches/TeamPatch.cs | 536 +-- .../Patches/TeamPatchManager.cs | 121 +- UncomplicatedCustomRoles/Plugin.cs | 178 +- .../Properties/AssemblyInfo.cs | 2 +- .../UncomplicatedCustomRoles.csproj | 6 +- UncomplicatedCustomRoles/app.config | 29 +- 136 files changed, 11255 insertions(+), 9286 deletions(-) create mode 100644 UncomplicatedCustomRoles/Manager/MapSpawnValidator.cs create mode 100644 UncomplicatedCustomRoles/Manager/RoleValidator.cs diff --git a/UncomplicatedCustomRoles/API/Attributes/PluginCustomRole.cs b/UncomplicatedCustomRoles/API/Attributes/PluginCustomRole.cs index e1b993c..d68208f 100644 --- a/UncomplicatedCustomRoles/API/Attributes/PluginCustomRole.cs +++ b/UncomplicatedCustomRoles/API/Attributes/PluginCustomRole.cs @@ -1,8 +1,8 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . @@ -10,8 +10,9 @@ using System; -namespace UncomplicatedCustomRoles.API.Attributes +namespace UncomplicatedCustomRoles.API.Attributes; + +[AttributeUsage(AttributeTargets.Class)] +public class PluginCustomRole : Attribute { - [AttributeUsage(AttributeTargets.Class)] - public class PluginCustomRole : Attribute { } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/API/Enums/CustomFlags.cs b/UncomplicatedCustomRoles/API/Enums/CustomFlags.cs index 01e92d1..2ad596f 100644 --- a/UncomplicatedCustomRoles/API/Enums/CustomFlags.cs +++ b/UncomplicatedCustomRoles/API/Enums/CustomFlags.cs @@ -1,8 +1,8 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . @@ -10,31 +10,30 @@ using System; -namespace UncomplicatedCustomRoles.API.Enums +namespace UncomplicatedCustomRoles.API.Enums; + +[Flags] +public enum CustomFlags { - [Flags] - public enum CustomFlags - { - NotExecutable = -1, - None = 0, - DoNotTriggerTeslaGates = 1 << 0, - LifeStealer = 1 << 1, - HalfLifeStealer = 1 << 2, - NotAffectedByAppearance = 1 << 3, - PacifismUntilDamage = 1 << 4, - CustomScpAnnouncer = 1 << 5, - ShowOnlyCustomInfo = 1 << 6, - SilentWalker = 1 << 7, - SilentAnnouncer = 1 << 8, - TutorialRagdoll = 1 << 9, - DoNotTrigger096 = 1 << 10, - BanKeycards = 1 << 11, - BanMedicals = 1 << 12, - BanRadios = 1 << 13, - BanFirearms = 1 << 14, - BanGrenades = 1 << 15, - BanSCPItems = 1 << 16, - BanMicroHID = 1 << 17, - BanArmors = 1 << 18, - } + NotExecutable = -1, + None = 0, + DoNotTriggerTeslaGates = 1 << 0, + LifeStealer = 1 << 1, + HalfLifeStealer = 1 << 2, + NotAffectedByAppearance = 1 << 3, + PacifismUntilDamage = 1 << 4, + CustomScpAnnouncer = 1 << 5, + ShowOnlyCustomInfo = 1 << 6, + SilentWalker = 1 << 7, + SilentAnnouncer = 1 << 8, + TutorialRagdoll = 1 << 9, + DoNotTrigger096 = 1 << 10, + BanKeycards = 1 << 11, + BanMedicals = 1 << 12, + BanRadios = 1 << 13, + BanFirearms = 1 << 14, + BanGrenades = 1 << 15, + BanSCPItems = 1 << 16, + BanMicroHID = 1 << 17, + BanArmors = 1 << 18 } \ No newline at end of file diff --git a/UncomplicatedCustomRoles/API/Enums/DamageType.cs b/UncomplicatedCustomRoles/API/Enums/DamageType.cs index 670ea37..31d66ab 100644 --- a/UncomplicatedCustomRoles/API/Enums/DamageType.cs +++ b/UncomplicatedCustomRoles/API/Enums/DamageType.cs @@ -8,63 +8,62 @@ * If not, see . */ -namespace UncomplicatedCustomRoles.API.Enums +namespace UncomplicatedCustomRoles.API.Enums; + +public enum DamageType { - public enum DamageType - { - Unknown, - Falldown, - Warhead, - Decontamination, - Asphyxiation, - Poison, - Bleeding, - Firearm, - MicroHid, - Tesla, - Scp, - Explosion, - Scp018, - Scp207, - Recontainment, - Crushed, - FemurBreaker, - PocketDimension, - FriendlyFireDetector, - SeveredHands, - SeveredEyes, - Custom, - Scp049, - Scp096, - Scp173, - Scp939, - Scp0492, - Scp106, - Crossvec, - Logicer, - Revolver, - Shotgun, - AK, - Com15, - Com18, - Fsp9, - E11Sr, - Hypothermia, - ParticleDisruptor, - CardiacArrest, - Com45, - Jailbird, - Frmg0, - A7, - Scp3114, - Strangled, - Marshmallow, - Scp1507, - Scp956, - SnowBall, - Scp127, - Silent, - GrayCandy, - Scp1509, - } + Unknown, + Falldown, + Warhead, + Decontamination, + Asphyxiation, + Poison, + Bleeding, + Firearm, + MicroHid, + Tesla, + Scp, + Explosion, + Scp018, + Scp207, + Recontainment, + Crushed, + FemurBreaker, + PocketDimension, + FriendlyFireDetector, + SeveredHands, + SeveredEyes, + Custom, + Scp049, + Scp096, + Scp173, + Scp939, + Scp0492, + Scp106, + Crossvec, + Logicer, + Revolver, + Shotgun, + AK, + Com15, + Com18, + Fsp9, + E11Sr, + Hypothermia, + ParticleDisruptor, + CardiacArrest, + Com45, + Jailbird, + Frmg0, + A7, + Scp3114, + Strangled, + Marshmallow, + Scp1507, + Scp956, + SnowBall, + Scp127, + Silent, + GrayCandy, + Scp1509 } \ No newline at end of file diff --git a/UncomplicatedCustomRoles/API/Enums/LoadStatusType.cs b/UncomplicatedCustomRoles/API/Enums/LoadStatusType.cs index 44c9759..00b98e6 100644 --- a/UncomplicatedCustomRoles/API/Enums/LoadStatusType.cs +++ b/UncomplicatedCustomRoles/API/Enums/LoadStatusType.cs @@ -1,19 +1,18 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . */ -namespace UncomplicatedCustomRoles.API.Enums +namespace UncomplicatedCustomRoles.API.Enums; + +public enum LoadStatusType { - public enum LoadStatusType - { - Success, - ValidatorError, - SameId - } -} + Success, + ValidatorError, + SameId +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/API/Enums/SpawnType.cs b/UncomplicatedCustomRoles/API/Enums/SpawnType.cs index e5d5d17..1e0c362 100644 --- a/UncomplicatedCustomRoles/API/Enums/SpawnType.cs +++ b/UncomplicatedCustomRoles/API/Enums/SpawnType.cs @@ -1,24 +1,23 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . */ -namespace UncomplicatedCustomRoles.API.Enums +namespace UncomplicatedCustomRoles.API.Enums; + +public enum SpawnType { - public enum SpawnType - { - CompleteRandomSpawn, - ZoneSpawn, - RoomsSpawn, - SpawnPointSpawn, - KeepRoleSpawn, - KeepCurrentPositionSpawn, - ClassDCell, - RoleSpawn - } + CompleteRandomSpawn, + ZoneSpawn, + RoomsSpawn, + SpawnPointSpawn, + KeepRoleSpawn, + KeepCurrentPositionSpawn, + ClassDCell, + RoleSpawn } \ No newline at end of file diff --git a/UncomplicatedCustomRoles/API/Features/Behaviour/AhpBehaviour.cs b/UncomplicatedCustomRoles/API/Features/Behaviour/AhpBehaviour.cs index f2809d3..ff25bc7 100644 --- a/UncomplicatedCustomRoles/API/Features/Behaviour/AhpBehaviour.cs +++ b/UncomplicatedCustomRoles/API/Features/Behaviour/AhpBehaviour.cs @@ -1,8 +1,8 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . @@ -10,48 +10,47 @@ using LabApi.Features.Wrappers; -namespace UncomplicatedCustomRoles.API.Features.Behaviour +namespace UncomplicatedCustomRoles.API.Features.Behaviour; + +public class AhpBehaviour { - public class AhpBehaviour + /// + /// Gets or sets the amout of AHP + /// + public float Amount { get; set; } = 0; + + /// + /// Gets or sets the limit of AHP + /// + public float Limit { get; set; } = 75; + + /// + /// Gets or sets the decay speed of AHP + /// + public float Decay { get; set; } = 1.2f; + + /// + /// Gets or sets the efficacy of AHP + /// + public float Efficacy { get; set; } = 0.7f; + + /// + /// Gets or sets for how long will the AHP remain static (do not decay) + /// + public float Sustain { get; set; } = 0f; + + /// + /// Gets or sets whether the AHP must be persistant or not + /// + public bool Persistant { get; set; } = false; + + /// + /// Apply the current instance of to the given + /// + /// + public void Apply(Player player) { - /// - /// Gets or sets the amout of AHP - /// - public float Amount { get; set; } = 0; - - /// - /// Gets or sets the limit of AHP - /// - public float Limit { get; set; } = 75; - - /// - /// Gets or sets the decay speed of AHP - /// - public float Decay { get; set; } = 1.2f; - - /// - /// Gets or sets the efficacy of AHP - /// - public float Efficacy { get; set; } = 0.7f; - - /// - /// Gets or sets for how long will the AHP remain static (do not decay) - /// - public float Sustain { get; set; } = 0f; - - /// - /// Gets or sets whether the AHP must be persistant or not - /// - public bool Persistant { get; set; } = false; - - /// - /// Apply the current instance of to the given - /// - /// - public void Apply(Player player) - { - if (Amount > 0) - player.CreateAhpProcess(Amount, Limit, Decay, Efficacy, Sustain, Persistant); - } + if (Amount > 0) + player.CreateAhpProcess(Amount, Limit, Decay, Efficacy, Sustain, Persistant); } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/API/Features/Behaviour/HealthBehaviour.cs b/UncomplicatedCustomRoles/API/Features/Behaviour/HealthBehaviour.cs index 34eb951..f56e520 100644 --- a/UncomplicatedCustomRoles/API/Features/Behaviour/HealthBehaviour.cs +++ b/UncomplicatedCustomRoles/API/Features/Behaviour/HealthBehaviour.cs @@ -1,8 +1,8 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . @@ -10,28 +10,27 @@ using LabApi.Features.Wrappers; -namespace UncomplicatedCustomRoles.API.Features.Behaviour +namespace UncomplicatedCustomRoles.API.Features.Behaviour; + +public class HealthBehaviour { - public class HealthBehaviour - { - /// - /// Gets or sets the amout of health that has to be given to the player - /// - public int Amount { get; set; } = 100; + /// + /// Gets or sets the amout of health that has to be given to the player + /// + public int Amount { get; set; } = 100; - /// - /// Gets or sets the maximum amout of health - /// - public int Maximum { get; set; } = 100; + /// + /// Gets or sets the maximum amout of health + /// + public int Maximum { get; set; } = 100; - /// - /// Apply the current instance of to the given - /// - /// - public void Apply(Player player) - { - player.Health = Amount; - player.MaxHealth = Maximum; - } + /// + /// Apply the current instance of to the given + /// + /// + public void Apply(Player player) + { + player.Health = Amount; + player.MaxHealth = Maximum; } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/API/Features/Behaviour/HumeShieldBehaviour.cs b/UncomplicatedCustomRoles/API/Features/Behaviour/HumeShieldBehaviour.cs index 6fff30c..cfb8530 100644 --- a/UncomplicatedCustomRoles/API/Features/Behaviour/HumeShieldBehaviour.cs +++ b/UncomplicatedCustomRoles/API/Features/Behaviour/HumeShieldBehaviour.cs @@ -1,8 +1,8 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . @@ -10,47 +10,47 @@ using LabApi.Features.Wrappers; -namespace UncomplicatedCustomRoles.API.Features.Behaviour +namespace UncomplicatedCustomRoles.API.Features.Behaviour; + +public class HumeShieldBehaviour { - public class HumeShieldBehaviour + /// + /// Gets or sets the hume shield amount + /// + public int Amount { get; set; } = 0; + + /// + /// Gets or sets the maximum hume shield amount + /// + public int Maximum { get; set; } = 0; + + /// + /// Gets or sets the speed of the regeneration of the hume shield in units/second + /// + public float RegenerationAmount { get; set; } = 2; + + /// + /// Gets or sets the time that the player has to be untouched (not damaged) in order to regen the hume shield (in + /// seconds) + /// + public float RegenerationDelay { get; set; } = 7.5f; + + /// + /// Gets or sets how fast the Hume Shield regenerates (in seconds) + /// 0 is 1 frame. + /// + public float RegenerationSpeed { get; set; } = 0f; + + /// + /// Apply the current instance of to the given + /// + /// + public void Apply(Player player) { - /// - /// Gets or sets the hume shield amount - /// - public int Amount { get; set; } = 0; - - /// - /// Gets or sets the maximum hume shield amount - /// - public int Maximum { get; set; } = 0; - - /// - /// Gets or sets the speed of the regeneration of the hume shield in units/second - /// - public float RegenerationAmount { get; set; } = 2; - - /// - /// Gets or sets the time that the player has to be untouched (not damaged) in order to regen the hume shield (in seconds) - /// - public float RegenerationDelay { get; set; } = 7.5f; - - /// - /// Gets or sets how fast the Hume Shield regenerates (in seconds) - /// 0 is 1 frame. - /// - public float RegenerationSpeed { get; set; } = 0f; - - /// - /// Apply the current instance of to the given - /// - /// - public void Apply(Player player) + if (Amount > 0 || Maximum > 0) { - if (Amount > 0 || Maximum > 0) - { - player.HumeShield = Amount; - player.MaxHumeShield = Maximum; - } + player.HumeShield = Amount; + player.MaxHumeShield = Maximum; } } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/API/Features/Behaviour/SpawnBehaviour.cs b/UncomplicatedCustomRoles/API/Features/Behaviour/SpawnBehaviour.cs index 555b81b..bb2eb18 100644 --- a/UncomplicatedCustomRoles/API/Features/Behaviour/SpawnBehaviour.cs +++ b/UncomplicatedCustomRoles/API/Features/Behaviour/SpawnBehaviour.cs @@ -1,82 +1,71 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . */ -using PlayerRoles; using System.Collections.Generic; -using UncomplicatedCustomRoles.API.Interfaces; -using UncomplicatedCustomRoles.API.Enums; using MapGeneration; +using PlayerRoles; +using UncomplicatedCustomRoles.API.Enums; +using UncomplicatedCustomRoles.API.Interfaces; -namespace UncomplicatedCustomRoles.API.Features.Behaviour -{ +namespace UncomplicatedCustomRoles.API.Features.Behaviour; #nullable enable - public class SpawnBehaviour - { - /// - /// Gets or sets a of that this role can override - /// - public List CanReplaceRoles { get; set; } = new() - { - RoleTypeId.ClassD - }; +public class SpawnBehaviour +{ + /// + /// Gets or sets a of that this role can override + /// + public List CanReplaceRoles { get; set; } = [RoleTypeId.ClassD]; - /// - /// Gets or sets the maximum number of the given can be alive at the same time - /// - public int MaxPlayers { get; set; } = 10; + /// + /// Gets or sets the maximum number of the given can be alive at the same time + /// + public int MaxPlayers { get; set; } = 10; - /// - /// Gets or sets the minimum number of players that are required by the given to spawn - /// - public int MinPlayers { get; set; } = 1; + /// + /// Gets or sets the minimum number of players that are required by the given to spawn + /// + public int MinPlayers { get; set; } = 1; - /// - /// Gets or sets the spawn chance of the role.

- /// 0 is 0% and 100 is 100% - ///
- public float SpawnChance { get; set; } = 60; + /// + /// Gets or sets the spawn chance of the role.

+ /// 0 is 0% and 100 is 100% + ///
+ public float SpawnChance { get; set; } = 60; - /// - /// Gets or sets the of the role - /// - public SpawnType Spawn { get; set; } = SpawnType.RoomsSpawn; + /// + /// Gets or sets the of the role + /// + public SpawnType Spawn { get; set; } = SpawnType.RoomsSpawn; - /// - /// Gets or sets a of zones that will be evaluated as spawnpoints - /// - public List SpawnZones { get; set; } = new(); + /// + /// Gets or sets a of zones that will be evaluated as spawnpoints + /// + public List SpawnZones { get; set; } = []; - /// - /// Gets or sets a of rooms that will be evaluated as spawnpoints - /// - public List SpawnRooms { get; set; } = new() - { - "LCZ_ClassDSpawn" - }; + /// + /// Gets or sets a of rooms that will be evaluated as spawnpoints + /// + public List SpawnRooms { get; set; } = ["LCZ_ClassDSpawn"]; - /// - /// Gets or sets a of roles that will be evaluated as spawnpoints - /// - public List SpawnRoles { get; set; } = new() - { - RoleTypeId.ClassD - }; + /// + /// Gets or sets a of roles that will be evaluated as spawnpoints + /// + public List SpawnRoles { get; set; } = [RoleTypeId.ClassD]; - /// - /// Gets or sets a of SpawnPoints that will be evaluated as spawnpoints - /// - public List SpawnPoints { get; set; } = new(); + /// + /// Gets or sets a of SpawnPoints that will be evaluated as spawnpoints + /// + public List SpawnPoints { get; set; } = []; - /// - /// Gets or sets the required PlayerPermission to spawn as the given - /// - public object RequiredPermission { get; set; } = new(); - } -} + /// + /// Gets or sets the required PlayerPermission to spawn as the given + /// + public object RequiredPermission { get; set; } = new(); +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/API/Features/Behaviour/StaminaBehaviour.cs b/UncomplicatedCustomRoles/API/Features/Behaviour/StaminaBehaviour.cs index 726464d..cc77dae 100644 --- a/UncomplicatedCustomRoles/API/Features/Behaviour/StaminaBehaviour.cs +++ b/UncomplicatedCustomRoles/API/Features/Behaviour/StaminaBehaviour.cs @@ -1,8 +1,8 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . @@ -10,30 +10,30 @@ using LabApi.Features.Wrappers; -namespace UncomplicatedCustomRoles.API.Features.Behaviour +namespace UncomplicatedCustomRoles.API.Features.Behaviour; + +public class StaminaBehaviour { - public class StaminaBehaviour - { - /// - /// Gets or sets the regeneration multiplier - /// - public float RegenMultiplier { get; set; } = 1; + /// + /// Gets or sets the regeneration multiplier + /// + public float RegenMultiplier { get; set; } = 1; - /// - /// Gets or sets the usage multiplier - /// - public float UsageMultiplier { get; set; } = 1; + /// + /// Gets or sets the usage multiplier + /// + public float UsageMultiplier { get; set; } = 1; - /// - /// Gets or sets whether the stamina should be infinite or not - /// - public bool Infinite { get; set; } = false; + /// + /// Gets or sets whether the stamina should be infinite or not + /// + public bool Infinite { get; set; } = false; - /// - /// Apply the current instance of to the given - /// - /// - public void Apply(Player _) - { } + /// + /// Apply the current instance of to the given + /// + /// + public void Apply(Player _) + { } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/API/Features/Controllers/EscapeController.cs b/UncomplicatedCustomRoles/API/Features/Controllers/EscapeController.cs index 12a4952..1707383 100644 --- a/UncomplicatedCustomRoles/API/Features/Controllers/EscapeController.cs +++ b/UncomplicatedCustomRoles/API/Features/Controllers/EscapeController.cs @@ -8,46 +8,48 @@ * If not, see . */ +using LabApi.Events.Arguments.PlayerEvents; using PlayerRoles; using UncomplicatedCustomRoles.Events; using UnityEngine; -namespace UncomplicatedCustomRoles.API.Features.Controllers +namespace UncomplicatedCustomRoles.API.Features.Controllers; + +internal class EscapeController : MonoBehaviour { - internal class EscapeController : MonoBehaviour - { - private SummonedCustomRole _role; + private SummonedCustomRole _role; - private bool _wasInEscapeZone; + private bool _wasInEscapeZone; - public void Init(SummonedCustomRole role) - { - _role = role; - } + private void Update() + { + if (_role is null || PlayerEventHandler.Instance is null) + return; - private void Update() - { - if (_role is null || PlayerEventHandler.Instance is null) - return; + var inZone = false; + foreach (var escapeZone in global::Escape.EscapeZones) + if (escapeZone.Contains(_role.Player.Position)) + { + inZone = true; - bool inZone = false; - foreach (Bounds escapeZone in global::Escape.EscapeZones) - if (escapeZone.Contains(_role.Player.Position)) - { - inZone = true; + if (!_wasInEscapeZone) + PlayerEventHandler.Instance.OnEscaping(new PlayerEscapingEventArgs(_role.Player.ReferenceHub, + _role.Player.Role, RoleTypeId.ChaosConscript, global::Escape.EscapeScenarioType.Custom, + escapeZone)); - if (!_wasInEscapeZone) - PlayerEventHandler.Instance.OnEscaping(new(_role.Player.ReferenceHub, _role.Player.Role, RoleTypeId.ChaosConscript, global::Escape.EscapeScenarioType.Custom, escapeZone)); + break; + } - break; - } + _wasInEscapeZone = inZone; + } - _wasInEscapeZone = inZone; - } + private void OnDestroy() + { + _role = null; + } - private void OnDestroy() - { - _role = null; - } + public void Init(SummonedCustomRole role) + { + _role = role; } } \ No newline at end of file diff --git a/UncomplicatedCustomRoles/API/Features/Controllers/Presence.cs b/UncomplicatedCustomRoles/API/Features/Controllers/Presence.cs index 6fcbe1b..3aa6f6a 100644 --- a/UncomplicatedCustomRoles/API/Features/Controllers/Presence.cs +++ b/UncomplicatedCustomRoles/API/Features/Controllers/Presence.cs @@ -5,24 +5,25 @@ using UncomplicatedCustomRoles.API.Features.Messages; using UncomplicatedCustomRoles.Manager; -namespace UncomplicatedCustomRoles.API.Features.Controllers +namespace UncomplicatedCustomRoles.API.Features.Controllers; + +internal static class Presence { - internal static class Presence + internal static IEnumerator PresenceCoroutine() { - internal static IEnumerator PresenceCoroutine() + while (true) { - while (true) + try + { + HttpQuery.Post("https://api.ucserver.it/v3/plugin/ucr/presence", + JsonSerializer.Serialize(new PresenceMessage()), "application/json"); + } + catch (Exception e) { - try - { - HttpQuery.Post("https://api.ucserver.it/v3/plugin/ucr/presence", JsonSerializer.Serialize(new PresenceMessage()), "application/json"); - } - catch (Exception e) - { - LogManager.Error($"Failed to send presence data: {e.Message}"); - } - yield return Timing.WaitForSeconds(60f); + LogManager.Error($"Failed to send presence data: {e.Message}"); } + + yield return Timing.WaitForSeconds(60f); } } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/API/Features/Controllers/SchematicController.cs b/UncomplicatedCustomRoles/API/Features/Controllers/SchematicController.cs index 429d2f6..be24e2c 100644 --- a/UncomplicatedCustomRoles/API/Features/Controllers/SchematicController.cs +++ b/UncomplicatedCustomRoles/API/Features/Controllers/SchematicController.cs @@ -8,63 +8,65 @@ * If not, see . */ -using System.Reflection; using UncomplicatedCustomRoles.Integrations; using UncomplicatedCustomRoles.Manager; using UnityEngine; -namespace UncomplicatedCustomRoles.API.Features.Controllers +namespace UncomplicatedCustomRoles.API.Features.Controllers; + +internal class SchematicController : MonoBehaviour { - internal class SchematicController : MonoBehaviour - { - private MonoBehaviour _schematic; + private MonoBehaviour _schematic; - public void Init(string schematicName) - { - // Generate the schematic - MethodInfo method = DynamicInvoke.GetMethod("MapEditorReborn", "MapEditorReborn.API.Features.ObjectSpawner.SpawnSchematic", methodCounter:2); - method ??= DynamicInvoke.GetMethod("ProjectMER", "ProjectMER.Features.ObjectSpawner.SpawnSchematic", true, 2); + private void LateUpdate() + { + if (_schematic is null) + return; - object schematic = method?.Invoke(null, new object[] { schematicName, Vector3.zero }); + // This should reference to the player + _schematic.transform.position = gameObject.transform.position; + _schematic.transform.rotation = gameObject.transform.rotation; + } - if (method is null) - { - LogManager.Error($"[MER Extension] Failed to import MER or ProjectMER schematic {schematicName}!\nMethod not found!"); - Destroy(this); - return; - } + private void OnDestroy() + { + Destroy(_schematic); + _schematic = null; + } - if (method is null) - { - LogManager.Error($"[MER Extension] Failed to import MER or ProjectMER schematic {schematicName}!\nSchematic not found!"); - Destroy(this); - return; - } + public void Init(string schematicName) + { + // Generate the schematic + var method = DynamicInvoke.GetMethod("MapEditorReborn", + "MapEditorReborn.API.Features.ObjectSpawner.SpawnSchematic", methodCounter: 2); + method ??= DynamicInvoke.GetMethod("ProjectMER", "ProjectMER.Features.ObjectSpawner.SpawnSchematic", true, 2); - if (schematic is not MonoBehaviour monoSchematic) - { - LogManager.Error($"[MER Extension] Failed to import MER or ProjectMER schematic {schematicName}!\nThe schematic object was not MonoBehaviour but {schematic.GetType().FullName}!"); - Destroy(this); - return; - } + var schematic = method?.Invoke(null, [schematicName, Vector3.zero]); - _schematic = monoSchematic; + if (method is null) + { + LogManager.Error( + $"[MER Extension] Failed to import MER or ProjectMER schematic {schematicName}!\nMethod not found!"); + Destroy(this); + return; } - private void LateUpdate() + if (method is null) { - if (_schematic is null) - return; - - // This should reference to the player - _schematic.transform.position = gameObject.transform.position; - _schematic.transform.rotation = gameObject.transform.rotation; + LogManager.Error( + $"[MER Extension] Failed to import MER or ProjectMER schematic {schematicName}!\nSchematic not found!"); + Destroy(this); + return; } - private void OnDestroy() + if (schematic is not MonoBehaviour monoSchematic) { - Destroy(_schematic); - _schematic = null; + LogManager.Error( + $"[MER Extension] Failed to import MER or ProjectMER schematic {schematicName}!\nThe schematic object was not MonoBehaviour but {schematic.GetType().FullName}!"); + Destroy(this); + return; } + + _schematic = monoSchematic; } } \ No newline at end of file diff --git a/UncomplicatedCustomRoles/API/Features/CustomInfo.cs b/UncomplicatedCustomRoles/API/Features/CustomInfo.cs index aaa59ea..193f3c2 100644 --- a/UncomplicatedCustomRoles/API/Features/CustomInfo.cs +++ b/UncomplicatedCustomRoles/API/Features/CustomInfo.cs @@ -1,13 +1,14 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . */ +using System.Collections.Generic; using LabApi.Features.Wrappers; using PlayerRoles; using Respawning.NamingRules; @@ -16,180 +17,187 @@ using UncomplicatedCustomRoles.Extensions; using UncomplicatedCustomRoles.Manager; -namespace UncomplicatedCustomRoles.API.Features +namespace UncomplicatedCustomRoles.API.Features; + +public class CustomInfo { - public class CustomInfo + private Player _lastOwner; + + public CustomInfo(string nickname, string role, string info) { - public string Nickname - { - get; - set - { - field = value; - if (_lastOwner is not null) - UpdateInfo(_lastOwner); - } - } + Nickname = nickname; + Role = role; + Info = info; + } - public string Role - { - get; - set - { - field = value; - if (_lastOwner is not null) - UpdateInfo(_lastOwner); - } - } + public CustomInfo(Player player, string info) + { + Nickname = player.Nickname; + Role = player.Role.GetFullName(); + Info = info; - public string Info - { - get; - set - { - field = value; - if (_lastOwner is not null) - UpdateInfo(_lastOwner); - } - } + UpdateInfo(player); + } - private Player _lastOwner; - - internal static bool SuppressExternalSync { get; set; } + public CustomInfo(Player player, ICustomRole role) + { + Nickname = player.Nickname; + Role = role.OverrideRoleName ? role.Name : role.Role.GetFullName(); + Info = role.CustomInfo; + + UpdateInfo(player); + } - public CustomInfo(string nickname, string role, string info) + public string Nickname + { + get; + set { - Nickname = nickname; - Role = role; - Info = info; + field = value; + if (_lastOwner is not null) + UpdateInfo(_lastOwner); } + } - public CustomInfo(Player player, string info) + public string Role + { + get; + set { - Nickname = player.Nickname; - Role = player.Role.GetFullName(); - Info = info; - - UpdateInfo(player); + field = value; + if (_lastOwner is not null) + UpdateInfo(_lastOwner); } + } - public CustomInfo(Player player, ICustomRole role) + public string Info + { + get; + set { - Nickname = player.Nickname; - Role = role.OverrideRoleName ? role.Name : role.Role.GetFullName(); - Info = role.CustomInfo; - - UpdateInfo(player); + field = value; + if (_lastOwner is not null) + UpdateInfo(_lastOwner); } + } + + internal static bool SuppressExternalSync { get; set; } - public void UpdateInfo(Player player) + public void UpdateInfo(Player player) + { + _lastOwner = player; + + var previousSuppress = SuppressExternalSync; + SuppressExternalSync = true; + try { - _lastOwner = player; + player.InfoArea |= PlayerInfoArea.CustomInfo; + player.InfoArea &= ~PlayerInfoArea.Role; + player.InfoArea &= ~PlayerInfoArea.Nickname; + player.InfoArea &= ~PlayerInfoArea.UnitName; - bool previousSuppress = SuppressExternalSync; - SuppressExternalSync = true; - try + var rawCustomInfo = "%custominfo%%nickname%%rolename%"; + var rawNickname = Nickname; + var rawInfo = Info; + var rawRole = Role; + + if (!NicknameSync.ValidateCustomInfo(Info, out var customInfoError) && !string.IsNullOrEmpty(Info)) { - player.InfoArea |= PlayerInfoArea.CustomInfo; - player.InfoArea &= ~PlayerInfoArea.Role; - player.InfoArea &= ~PlayerInfoArea.Nickname; - player.InfoArea &= ~PlayerInfoArea.UnitName; + LogManager.Error( + $"CustomInfo is not correct, therefore the custom info part of player {player.PlayerId} won't be shown.\nCustomInfo: {Info}\nError: {customInfoError}"); + rawCustomInfo = rawCustomInfo.Replace("%custominfo%", ""); + rawInfo = string.Empty; + } - string rawCustomInfo = "%custominfo%%nickname%%rolename%"; - string rawNickname = Nickname; - string rawInfo = Info; - string rawRole = Role; + if (!NicknameSync.ValidateCustomInfo(Role, out var roleNameError) && !string.IsNullOrEmpty(Role)) + { + LogManager.Error( + $"RoleName is not correct, therefore the role name part of player {player.PlayerId} won't be shown.\nRoleName: {Role}\nError: {roleNameError}"); + rawCustomInfo = rawCustomInfo.Replace("%rolename%", ""); + rawRole = string.Empty; + } - if (!NicknameSync.ValidateCustomInfo(Info, out string customInfoError) && !string.IsNullOrEmpty(Info)) - { - LogManager.Error($"CustomInfo is not correct, therefore the custom info part of player {player.PlayerId} won't be shown.\nCustomInfo: {Info}\nError: {customInfoError}"); - rawCustomInfo = rawCustomInfo.Replace("%custominfo%", ""); - rawInfo = string.Empty; - } + if (player.TryGetSummonedInstance(out var summonedCustomRole)) + { + rawInfo = PlaceholderManager.ApplyPlaceholders(rawInfo, player, summonedCustomRole.Role); - if (!NicknameSync.ValidateCustomInfo(Role, out string roleNameError) && !string.IsNullOrEmpty(Role)) - { - LogManager.Error($"RoleName is not correct, therefore the role name part of player {player.PlayerId} won't be shown.\nRoleName: {Role}\nError: {roleNameError}"); - rawCustomInfo = rawCustomInfo.Replace("%rolename%", ""); - rawRole = string.Empty; - } + if (summonedCustomRole.TryGetModule(out CustomInfoOrder customInfoOrderModule)) + rawCustomInfo = $"{customInfoOrderModule.Order}"; - if (player.TryGetSummonedInstance(out SummonedCustomRole summonedCustomRole)) + if (summonedCustomRole.TryGetModule(out ColorfulNickname colorfulNickname)) { - rawInfo = PlaceholderManager.ApplyPlaceholders(rawInfo, player, summonedCustomRole.Role); + LogManager.Debug( + $"Applying ColorfulNickname module to player {player.PlayerId} with color {colorfulNickname.Color} and nickname {Nickname}"); - if (summonedCustomRole.TryGetModule(out CustomInfoOrder customInfoOrderModule)) - rawCustomInfo = $"{customInfoOrderModule.Order}"; - - if (summonedCustomRole.TryGetModule(out ColorfulNickname colorfulNickname)) + if (string.IsNullOrEmpty(colorfulNickname.Color)) { - LogManager.Debug($"Applying ColorfulNickname module to player {player.PlayerId} with color {colorfulNickname.Color} and nickname {Nickname}"); - - if (string.IsNullOrEmpty(colorfulNickname.Color)) - { - LogManager.Warn($"The ColorfulNickname module of player {player.PlayerId} has no color set, skipping the colouring."); - } + LogManager.Warn( + $"The ColorfulNickname module of player {player.PlayerId} has no color set, skipping the colouring."); + } + else + { + var nick = Nickname?.Replace("*", "") ?? string.Empty; + if (string.IsNullOrEmpty(nick)) + nick = player.Nickname; + var color = colorfulNickname.Color.StartsWith("#") + ? colorfulNickname.Color + : $"#{colorfulNickname.Color}"; + if (!Misc.AcceptedColours.Contains(color.Replace("#", ""))) + LogManager.Warn( + $"The color {color} is not acceptable by the game in ColorfulNicknames! Please use a valid hex color code."); else - { - string nick = Nickname?.Replace("*", "") ?? string.Empty; - if (string.IsNullOrEmpty(nick)) - nick = player.Nickname; - string color = colorfulNickname.Color.StartsWith("#") ? colorfulNickname.Color : $"#{colorfulNickname.Color}"; - if (!Misc.AcceptedColours.Contains(color.Replace("#", ""))) - LogManager.Warn($"The color {color} is not acceptable by the game in ColorfulNicknames! Please use a valid hex color code."); - else - rawNickname = $"{nick}"; - } + rawNickname = $"{nick}"; } - - Team roleTeam = summonedCustomRole.Role.Role.GetTeam(); - if (DisguiseTeam.List.TryGetValue(player.PlayerId, out Team fakeTeam)) - roleTeam = fakeTeam; - - if (!string.IsNullOrEmpty(rawRole) && !summonedCustomRole.HasModule() - && roleTeam is Team.FoundationForces - && NamingRulesManager.TryGetNamingRule(roleTeam, out UnitNamingRule unitNamingRule) - && !string.IsNullOrEmpty(unitNamingRule.LastGeneratedName)) - rawRole = $"{rawRole} ({unitNamingRule.LastGeneratedName})"; - } - else - { - rawInfo = PlaceholderManager.ApplyPlaceholders(rawInfo, player, null); } - if (string.IsNullOrEmpty(rawInfo)) - rawCustomInfo = rawCustomInfo.Replace("%custominfo%", ""); + var roleTeam = summonedCustomRole.Role.Role.GetTeam(); + if (DisguiseTeam.List.TryGetValue(player.PlayerId, out var fakeTeam)) + roleTeam = fakeTeam; - if (string.IsNullOrEmpty(rawNickname)) - rawNickname = player.Nickname; - - if (string.IsNullOrEmpty(rawInfo) && string.IsNullOrEmpty(rawRole) && string.IsNullOrEmpty(player.Nickname)) - { - player.InfoArea |= PlayerInfoArea.Nickname | PlayerInfoArea.Role | PlayerInfoArea.UnitName; - player.CustomInfo = string.Empty; - return; - } - - player.CustomInfo = rawCustomInfo.Replace("%%", "%\n%").BulkReplace(new() - { - { - "custominfo", - rawInfo - }, - { - "nickname", - rawNickname - }, - { - "rolename", - rawRole - }, - }, "%%"); + if (!string.IsNullOrEmpty(rawRole) && !summonedCustomRole.HasModule() + && roleTeam is Team.FoundationForces + && NamingRulesManager.TryGetNamingRule(roleTeam, + out var unitNamingRule) + && !string.IsNullOrEmpty(unitNamingRule.LastGeneratedName)) + rawRole = $"{rawRole} ({unitNamingRule.LastGeneratedName})"; + } + else + { + rawInfo = PlaceholderManager.ApplyPlaceholders(rawInfo, player, null); } - finally + + if (string.IsNullOrEmpty(rawInfo)) + rawCustomInfo = rawCustomInfo.Replace("%custominfo%", ""); + + if (string.IsNullOrEmpty(rawNickname)) + rawNickname = player.Nickname; + + if (string.IsNullOrEmpty(rawInfo) && string.IsNullOrEmpty(rawRole) && string.IsNullOrEmpty(player.Nickname)) { - SuppressExternalSync = previousSuppress; + player.InfoArea |= PlayerInfoArea.Nickname | PlayerInfoArea.Role | PlayerInfoArea.UnitName; + player.CustomInfo = string.Empty; + return; } + + player.CustomInfo = rawCustomInfo.Replace("%%", "%\n%").BulkReplace(new Dictionary + { + { + "custominfo", + rawInfo + }, + { + "nickname", + rawNickname + }, + { + "rolename", + rawRole + } + }, "%%"); + } + finally + { + SuppressExternalSync = previousSuppress; } } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/API/Features/CustomModules/AmnesiaResistance.cs b/UncomplicatedCustomRoles/API/Features/CustomModules/AmnesiaResistance.cs index dc833fa..83f3556 100644 --- a/UncomplicatedCustomRoles/API/Features/CustomModules/AmnesiaResistance.cs +++ b/UncomplicatedCustomRoles/API/Features/CustomModules/AmnesiaResistance.cs @@ -8,8 +8,8 @@ * If not, see . */ -namespace UncomplicatedCustomRoles.API.Features.CustomModules +namespace UncomplicatedCustomRoles.API.Features.CustomModules; + +internal class AmnesiaResistance : CustomModule { - internal class AmnesiaResistance : CustomModule - { } } \ No newline at end of file diff --git a/UncomplicatedCustomRoles/API/Features/CustomModules/ChangeAppearanceOnKill.cs b/UncomplicatedCustomRoles/API/Features/CustomModules/ChangeAppearanceOnKill.cs index 4773d3f..cf85341 100644 --- a/UncomplicatedCustomRoles/API/Features/CustomModules/ChangeAppearanceOnKill.cs +++ b/UncomplicatedCustomRoles/API/Features/CustomModules/ChangeAppearanceOnKill.cs @@ -1,34 +1,57 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . */ -using PlayerRoles; using System; using System.Collections.Generic; +using PlayerRoles; + +namespace UncomplicatedCustomRoles.API.Features.CustomModules; -namespace UncomplicatedCustomRoles.API.Features.CustomModules +internal class ChangeAppearanceOnKill : CustomModule { - internal class ChangeAppearanceOnKill : CustomModule + public override List RequiredArgs => ["new_appearance"]; + + public RoleTypeId NewAppearance => + Enum.TryParse(TryGetStringValue("new_appearance", "None"), true, out RoleTypeId role) ? role : RoleTypeId.None; + + public uint Duration => TryGetCastedValue("duration"); + + public bool Forever => TryGetCastedValue("forever", false); + + public bool AlreadyChanged { get; internal set; } = false; + + public override bool Validate(out string error) { - public override List RequiredArgs => new() + var raw = TryGetStringValue("new_appearance"); + if (NewAppearance is RoleTypeId.None) { - "new_appearance", - "duration", - "forever" - }; - - public RoleTypeId NewAppearance => Enum.TryParse(TryGetStringValue("new_appearance", "None"), out RoleTypeId role) ? role : RoleTypeId.None; + error = + $"'new_appearance' value '{raw}' is not a valid role. Examples: Scientist, ClassD, NtfSergeant, Scp0492."; + return false; + } - public uint Duration => Convert.ToUInt32(TryGetValue("duration", 0)); + if (Args.TryGetValue("duration", out var rawDuration) && rawDuration is not null + && !uint.TryParse(rawDuration.ToString(), out _)) + { + error = $"'duration' must be a whole number of seconds (0 or greater), got '{rawDuration}'."; + return false; + } - public bool Forever => Convert.ToBoolean(TryGetValue("forever", false)); + if (Args.TryGetValue("forever", out var rawForever) && rawForever is not null + && !bool.TryParse(rawForever.ToString(), out _)) + { + error = $"'forever' must be true or false, got '{rawForever}'."; + return false; + } - public bool AlreadyChanged { get; internal set; } = false; + error = null; + return true; } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/API/Features/CustomModules/ColorfulNickname.cs b/UncomplicatedCustomRoles/API/Features/CustomModules/ColorfulNickname.cs index 4d29c89..1adb469 100644 --- a/UncomplicatedCustomRoles/API/Features/CustomModules/ColorfulNickname.cs +++ b/UncomplicatedCustomRoles/API/Features/CustomModules/ColorfulNickname.cs @@ -8,27 +8,44 @@ * If not, see . */ +using System; using System.Collections.Generic; +using System.Linq; using MEC; -namespace UncomplicatedCustomRoles.API.Features.CustomModules +namespace UncomplicatedCustomRoles.API.Features.CustomModules; + +public class ColorfulNickname : CustomModule { - public class ColorfulNickname : CustomModule + public override List RequiredArgs => ["color"]; + + internal string Color { - public override List RequiredArgs => new() + get { - "color" - }; - - internal string Color => TryGetStringValue("color", string.Empty); + var raw = TryGetStringValue("color", string.Empty).TrimStart('#'); + return Misc.AcceptedColours.FirstOrDefault(c => + string.Equals(c, raw, StringComparison.OrdinalIgnoreCase)) ?? raw; + } + } - public override void OnAdded() + public override bool Validate(out string error) + { + var raw = TryGetStringValue("color", string.Empty).TrimStart('#'); + if (!Misc.AcceptedColours.Any(c => string.Equals(c, raw, StringComparison.OrdinalIgnoreCase))) { - Timing.CallDelayed(Timing.WaitForOneFrame, () => - { - CustomRole.CustomInfo.UpdateInfo(CustomRole.Player); - }); - base.OnAdded(); + error = + $"'color' '{raw}' is not a color the game allows for nicknames. Allowed colors: {string.Join(", ", Misc.AcceptedColours)}."; + return false; } + + error = null; + return true; + } + + public override void OnAdded() + { + Timing.CallDelayed(Timing.WaitForOneFrame, () => { CustomRole.CustomInfo.UpdateInfo(CustomRole.Player); }); + base.OnAdded(); } } \ No newline at end of file diff --git a/UncomplicatedCustomRoles/API/Features/CustomModules/ColorfulRaName.cs b/UncomplicatedCustomRoles/API/Features/CustomModules/ColorfulRaName.cs index f063e2e..4e22288 100644 --- a/UncomplicatedCustomRoles/API/Features/CustomModules/ColorfulRaName.cs +++ b/UncomplicatedCustomRoles/API/Features/CustomModules/ColorfulRaName.cs @@ -9,16 +9,28 @@ */ using System.Collections.Generic; +using UnityEngine; -namespace UncomplicatedCustomRoles.API.Features.CustomModules +namespace UncomplicatedCustomRoles.API.Features.CustomModules; + +public class ColorfulRaName : CustomModule { - public class ColorfulRaName : CustomModule + public override List RequiredArgs => ["color"]; + + internal string Color => TryGetStringValue("color", string.Empty); + + public override bool Validate(out string error) { - public override List RequiredArgs => new() + var raw = TryGetStringValue("color", string.Empty); + var hex = raw.StartsWith("#") ? raw : "#" + raw; + + if (!ColorUtility.TryParseHtmlString(hex, out _)) { - "color" - }; + error = $"'color' '{raw}' is not a valid hex color. Use a hex value like FF0000 or #FF0000."; + return false; + } - internal string Color => TryGetStringValue("color", string.Empty); + error = null; + return true; } } \ No newline at end of file diff --git a/UncomplicatedCustomRoles/API/Features/CustomModules/CustomInfoOrder.cs b/UncomplicatedCustomRoles/API/Features/CustomModules/CustomInfoOrder.cs index d249fb4..ce591ac 100644 --- a/UncomplicatedCustomRoles/API/Features/CustomModules/CustomInfoOrder.cs +++ b/UncomplicatedCustomRoles/API/Features/CustomModules/CustomInfoOrder.cs @@ -8,27 +8,49 @@ * If not, see . */ -using System.Collections.Generic; +using System; +using System.Linq; +using System.Text.RegularExpressions; using MEC; +using UncomplicatedCustomRoles.Manager; -namespace UncomplicatedCustomRoles.API.Features.CustomModules +namespace UncomplicatedCustomRoles.API.Features.CustomModules; + +public class CustomInfoOrder : CustomModule { - public class CustomInfoOrder : CustomModule + private static readonly string[] KnownTokens = ["custominfo", "nickname", "rolename"]; + + private static readonly Regex TokenRegex = new("%([a-zA-Z_]+)%", RegexOptions.Compiled); + + internal string Order => TryGetStringValue("order", "%custominfo%%nickname%%rolename%"); + + public override bool Validate(out string error) { - public override List RequiredArgs => new() + var tokens = TokenRegex.Matches(Order).Cast().Select(m => m.Groups[1].Value).ToList(); + + var unknown = tokens + .Where(t => !KnownTokens.Contains(t, StringComparer.OrdinalIgnoreCase)) + .Distinct() + .ToList(); + + if (unknown.Count > 0) + LogManager.Warn( + $"[CustomModule] CustomInfoOrder 'order' contains unknown token(s): {string.Join(", ", unknown.Select(t => $"%{t}%"))}; they will be shown as-is. Valid tokens: %custominfo%, %nickname%, %rolename%."); + + if (!tokens.Any(t => KnownTokens.Contains(t, StringComparer.OrdinalIgnoreCase))) { - "order" - }; - - internal string Order => TryGetStringValue("order", "%custominfo%%nickname%%rolename%"); - - public override void OnAdded() - { - Timing.CallDelayed(Timing.WaitForOneFrame, () => - { - CustomRole.CustomInfo.UpdateInfo(CustomRole.Player); - }); - base.OnAdded(); + error = + "'order' must contain at least one of %custominfo%, %nickname% or %rolename%; otherwise the custom info would show static text only."; + return false; } + + error = null; + return true; + } + + public override void OnAdded() + { + Timing.CallDelayed(Timing.WaitForOneFrame, () => { CustomRole.CustomInfo.UpdateInfo(CustomRole.Player); }); + base.OnAdded(); } } \ No newline at end of file diff --git a/UncomplicatedCustomRoles/API/Features/CustomModules/CustomKeycard.cs b/UncomplicatedCustomRoles/API/Features/CustomModules/CustomKeycard.cs index 32c3621..328037b 100644 --- a/UncomplicatedCustomRoles/API/Features/CustomModules/CustomKeycard.cs +++ b/UncomplicatedCustomRoles/API/Features/CustomModules/CustomKeycard.cs @@ -23,10 +23,6 @@ namespace UncomplicatedCustomRoles.API.Features.CustomModules; public class CustomKeycard : CustomModule { - public override List RequiredArgs => ["KeycardType"]; - - private KeycardItem _keycardItem; - private static readonly Dictionary KeycardTypeAliases = new(StringComparer.OrdinalIgnoreCase) { { "Management", ItemType.KeycardCustomManagement }, @@ -34,27 +30,39 @@ public class CustomKeycard : CustomModule { "MetalCase", ItemType.KeycardCustomMetalCase }, { "Site02", ItemType.KeycardCustomSite02 }, { "Site", ItemType.KeycardCustomSite02 }, - { "TaskForce", ItemType.KeycardCustomTaskForce }, + { "TaskForce", ItemType.KeycardCustomTaskForce } }; + private static readonly string ValidKeycardTypes = + string.Join(", ", KeycardTypeAliases.Keys.OrderBy(k => k)); + + private KeycardItem _keycardItem; + public override List RequiredArgs => ["KeycardType"]; + internal ItemType KeycardType { get { - string raw = TryGetStringValue("KeycardType")?.Trim(); + var raw = TryGetStringValue("KeycardType")?.Trim(); if (string.IsNullOrEmpty(raw)) return ItemType.None; - if (KeycardTypeAliases.TryGetValue(raw, out ItemType alias)) + if (KeycardTypeAliases.TryGetValue(raw, out var alias)) return alias; return Enum.TryParse(raw, true, out ItemType parsed) ? parsed : ItemType.None; } } - internal string ItemName => PlaceholderManager.ApplyPlaceholders(TryGetStringValue("ItemName", "Custom Keycard"), Player, CustomRole.Role); - internal string HolderName => PlaceholderManager.ApplyPlaceholders(TryGetStringValue("HolderName", "Unknown"), Player, CustomRole.Role); - internal string CardLabel => PlaceholderManager.ApplyPlaceholders(TryGetStringValue("CardLabel", string.Empty), Player, CustomRole.Role); + internal string ItemName => + PlaceholderManager.ApplyPlaceholders(TryGetStringValue("ItemName", "Custom Keycard"), Player, CustomRole.Role); + + internal string HolderName => + PlaceholderManager.ApplyPlaceholders(TryGetStringValue("HolderName", "Unknown"), Player, CustomRole.Role); + + internal string CardLabel => + PlaceholderManager.ApplyPlaceholders(TryGetStringValue("CardLabel", string.Empty), Player, CustomRole.Role); + internal KeycardLevels Permissions => BuildPermissions(); internal Color KeycardColor => ParseColor("KeycardColor", Color.white); internal Color PermissionsColor => ParseColor("PermissionsColor", Color.white); @@ -63,36 +71,102 @@ internal ItemType KeycardType internal string SerialLabel => TryGetStringValue("SerialLabel", "000000000000"); internal int RankIndex => TryGetCastedValue("RankIndex", 0); - private static readonly string ValidKeycardTypes = - string.Join(", ", KeycardTypeAliases.Keys.OrderBy(k => k)); - - public override void OnAdded() + public override bool Validate(out string error) { - Timing.CallDelayed(Timing.WaitForOneFrame, () => + if (KeycardType == ItemType.None) + { + error = + $"'KeycardType' '{TryGetStringValue("KeycardType")}' is not a valid keycard. Valid values: {ValidKeycardTypes}."; + return false; + } + + if (!KeycardType.TryGetTemplate(out var template) || + !template.Customizable) + { + error = $"'{KeycardType}' is not a customizable keycard type. Valid values: {ValidKeycardTypes}."; + return false; + } + + foreach (var level in new[] { "ContainmentLevel", "ArmoryLevel", "AdminLevel" }) + if (HasArg(level)) + { + var raw = TryGetStringValue(level); + if (!int.TryParse(raw, out var value) || value is < 0 or > 3) + { + error = $"'{level}' must be a whole number between 0 and 3, got '{raw}'."; + return false; + } + } + + if (HasArg("WearLevel") && !byte.TryParse(TryGetStringValue("WearLevel"), out _)) { - if (KeycardType == ItemType.None) + error = $"'WearLevel' must be a whole number between 0 and 255, got '{TryGetStringValue("WearLevel")}'."; + return false; + } + + foreach (var colorParam in new[] { "KeycardColor", "PermissionsColor", "LabelColor" }) + if (HasArg(colorParam) && !TryParseColor(TryGetStringValue(colorParam))) { - LogManager.Error($"[CustomKeycard] 'KeycardType' is missing or invalid for player {Player?.Nickname}. Valid values: {ValidKeycardTypes}"); - return; + error = + $"'{colorParam}' '{TryGetStringValue(colorParam)}' is not a valid hex color. Use a value like #FF0000."; + return false; } - if (!KeycardType.TryGetTemplate(out var template) || - !template.Customizable) + if (HasArg("Permissions")) + { + var joined = JoinArg("Permissions"); + if (!string.IsNullOrWhiteSpace(joined) && + !Enum.TryParse(joined.Replace(" ", string.Empty), true, out DoorPermissionFlags _)) { - LogManager.Error($"[CustomKeycard] '{KeycardType}' is not a customizable keycard type for player {Player?.Nickname}. Valid values: {ValidKeycardTypes}"); - return; + error = + $"'Permissions' value '{joined}' contains invalid door permission flag(s). Valid flags: {string.Join(", ", Enum.GetNames(typeof(DoorPermissionFlags)))}."; + return false; } + } + + error = null; + return true; + } + + private static bool TryParseColor(string raw) + { + if (string.IsNullOrEmpty(raw)) + return false; + + if (!raw.StartsWith("#")) + raw = "#" + raw; + + return ColorUtility.TryParseHtmlString(raw, out _); + } + + private string JoinArg(string param) + { + if (Args is null || !Args.TryGetValue(param, out var raw) || raw is null) + return string.Empty; + + return raw is string s + ? s + : raw is IEnumerable enumerable + ? string.Join(",", enumerable.Cast().Where(o => o is not null).Select(o => o.ToString())) + : raw.ToString(); + } + public override void OnAdded() + { + Timing.CallDelayed(Timing.WaitForOneFrame, () => + { _keycardItem = KeycardType switch { ItemType.KeycardCustomManagement => KeycardItem.CreateCustomKeycardManagement( Player, ItemName, CardLabel, Permissions, KeycardColor, PermissionsColor, LabelColor), ItemType.KeycardCustomMetalCase => KeycardItem.CreateCustomKeycardMetal( - Player, ItemName, HolderName, CardLabel, Permissions, KeycardColor, PermissionsColor, LabelColor, WearLevel, SerialLabel), + Player, ItemName, HolderName, CardLabel, Permissions, KeycardColor, PermissionsColor, LabelColor, + WearLevel, SerialLabel), ItemType.KeycardCustomSite02 => KeycardItem.CreateCustomKeycardSite02( - Player, ItemName, HolderName, CardLabel, Permissions, KeycardColor, PermissionsColor, LabelColor, WearLevel), + Player, ItemName, HolderName, CardLabel, Permissions, KeycardColor, PermissionsColor, LabelColor, + WearLevel), ItemType.KeycardCustomTaskForce => KeycardItem.CreateCustomKeycardTaskForce( Player, ItemName, HolderName, Permissions, KeycardColor, PermissionsColor, SerialLabel, RankIndex), @@ -101,21 +175,22 @@ public override void OnAdded() }; if (_keycardItem is null) - LogManager.Error($"[CustomKeycard] Failed to create keycard of type '{KeycardType}' for player {Player?.Nickname}. This is likely a bug, please report it."); + LogManager.Error( + $"[CustomKeycard] Failed to create keycard of type '{KeycardType}' for player {Player?.Nickname}. This is likely a bug, please report it."); }); base.OnAdded(); } - + private KeycardLevels BuildPermissions() { - bool hasLevels = HasArg("ContainmentLevel") || HasArg("ArmoryLevel") || HasArg("AdminLevel"); + var hasLevels = HasArg("ContainmentLevel") || HasArg("ArmoryLevel") || HasArg("AdminLevel"); KeycardLevels levels = new( TryGetCastedValue("ContainmentLevel", 0), TryGetCastedValue("ArmoryLevel", 0), TryGetCastedValue("AdminLevel", 0)); - DoorPermissionFlags rawFlags = ParseFlags("Permissions"); + var rawFlags = ParseFlags("Permissions"); if (!hasLevels && rawFlags == DoorPermissionFlags.None) return levels; @@ -123,23 +198,22 @@ private KeycardLevels BuildPermissions() return new KeycardLevels(levels.Permissions | rawFlags); } - private bool HasArg(string param) => Args is not null && Args.ContainsKey(param); + private bool HasArg(string param) + { + return Args is not null && Args.ContainsKey(param); + } private DoorPermissionFlags ParseFlags(string param) { - if (Args is null || !Args.TryGetValue(param, out object raw) || raw is null) - return DoorPermissionFlags.None; - - string joined = raw as string ?? (raw is IEnumerable enumerable - ? string.Join(",", enumerable.Cast().Where(o => o is not null).Select(o => o.ToString())) - : raw.ToString()); + var joined = JoinArg(param); if (string.IsNullOrWhiteSpace(joined)) return DoorPermissionFlags.None; if (!Enum.TryParse(joined.Replace(" ", string.Empty), true, out DoorPermissionFlags result)) { - LogManager.Warn($"[CustomKeycard] Invalid value '{joined}' for '{param}'. Valid flags: {string.Join(", ", Enum.GetNames(typeof(DoorPermissionFlags)))}. Ignoring it."); + LogManager.Warn( + $"[CustomKeycard] Invalid value '{joined}' for '{param}'. Valid flags: {string.Join(", ", Enum.GetNames(typeof(DoorPermissionFlags)))}. Ignoring it."); return DoorPermissionFlags.None; } @@ -148,19 +222,20 @@ private DoorPermissionFlags ParseFlags(string param) private Color ParseColor(string param, Color def) { - string raw = TryGetStringValue(param); + var raw = TryGetStringValue(param); if (raw is null) return def; if (!raw.StartsWith("#")) raw = "#" + raw; - if (!ColorUtility.TryParseHtmlString(raw, out Color color)) + if (!ColorUtility.TryParseHtmlString(raw, out var color)) { - LogManager.Warn($"[CustomKeycard] Invalid color '{TryGetStringValue(param)}' for '{param}'. Expected a hex color like #FF0000. Using default (white)."); + LogManager.Warn( + $"[CustomKeycard] Invalid color '{TryGetStringValue(param)}' for '{param}'. Expected a hex color like #FF0000. Using default (white)."); return def; } return color; } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/API/Features/CustomModules/CustomModule.cs b/UncomplicatedCustomRoles/API/Features/CustomModules/CustomModule.cs index df994da..ff484ea 100644 --- a/UncomplicatedCustomRoles/API/Features/CustomModules/CustomModule.cs +++ b/UncomplicatedCustomRoles/API/Features/CustomModules/CustomModule.cs @@ -1,290 +1,376 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . */ -using LabApi.Events.Arguments.Interfaces; using System; using System.Collections; using System.Collections.Generic; using System.Linq; +using LabApi.Events.Arguments.Interfaces; using LabApi.Features.Wrappers; -using UncomplicatedCustomRoles.Extensions; using UncomplicatedCustomRoles.Manager; -namespace UncomplicatedCustomRoles.API.Features.CustomModules +namespace UncomplicatedCustomRoles.API.Features.CustomModules; + +public abstract class CustomModule { - public abstract class CustomModule + /// + /// Gets the display name of the given + /// + /// Default one is the class' name + public virtual string Name => GetType().Name; + + /// + /// Gets the list of events that this will listen for. + /// + /// The will be invoked only for the given events! + public virtual List TriggerOnEvents { get; } = []; + + /// + /// Gets the list of required argument names for the current + /// + public virtual List RequiredArgs { get; } = []; + + /// + /// Gets the args of the current + /// + /// Every value is a + public Dictionary Args { get; private set; } + + /// + /// Gets the args of the current with the value converted as string + /// + public Dictionary StringArgs { - /// - /// Gets the display name of the given - /// - /// Default one is the class' name - public virtual string Name + get { - get - { - return GetType().Name; - } + Dictionary result = new(StringComparer.OrdinalIgnoreCase); + foreach (var kvp in Args) + result[kvp.Key] = kvp.Value?.ToString(); + return result; } + } + + /// + /// Gets the instance of the in which the current is + /// embedded + /// + public SummonedCustomRole CustomRole { get; private set; } + + /// + /// Gets the instance of the in which the current + /// is embedded + /// + public Player Player => CustomRole.Player; + + internal void Initialize(SummonedCustomRole summonedCustomRole, Dictionary args) + { + CustomRole = summonedCustomRole; + Args = args is null + ? new Dictionary(StringComparer.OrdinalIgnoreCase) + : new Dictionary(args, StringComparer.OrdinalIgnoreCase); + } + + /// + /// The added event function + /// + /// Invoked when the has been added to the + public virtual void OnAdded() + { + } - /// - /// Gets the list of events that this will listen for. - /// - /// The will be invoked only for the given events! - public virtual List TriggerOnEvents { get; } = new(); - - /// - /// Gets the list of required argument names for the current - /// - public virtual List RequiredArgs { get; } = new(); - - /// - /// Gets the args of the current - /// - /// Every value is a - public Dictionary Args { get; private set; } - - /// - /// Gets the args of the current with the value converted as string - /// - public Dictionary StringArgs => Args.ConvertToString(); - - /// - /// Gets the instance of the in which the current is embedded - /// - public SummonedCustomRole CustomRole { get; private set; } - - /// - /// Gets the instance of the in which the current is embedded - /// - public Player Player => CustomRole.Player; - - internal void Initialize(SummonedCustomRole summonedCustomRole, Dictionary args) + /// + /// The removed event function + /// + /// + /// Invoked when the has been removed from the + /// + public virtual void OnRemoved() + { + } + + /// + /// The generic event function + /// + /// + /// + /// Invoked only for the events listed in + public virtual bool OnEvent(string name, IPlayerEvent ev) + { + return true; + } + + public virtual bool Validate(out string error) + { + error = null; + return true; + } + + /// + /// A generic function + /// + /// This won't be invoked by UCR + public virtual void Execute() + { + } + + /// + /// Try to get a generic value from the and if not present just return a + /// default value. + /// + /// + /// + /// + public object TryGetValue(string param, object def = null) + { + return Args.TryGetValue(param, out var value) ? value : def; + } + + /// + /// Try to get a value from the and if not present just return a default value. + /// + /// + /// + /// + public string TryGetStringValue(string param, string def = null) + { + return StringArgs.TryGetValue(param, out var value) ? value : def; + } + + /// + /// Try to get a value from the and if not present just return a default value, with the value + /// converted to the given type . + /// + /// + /// + /// + /// + public T TryGetCastedValue(string param, T def = default) + { + if (!Args.TryGetValue(param, out var value)) + return def; + + try { - CustomRole = summonedCustomRole; - Args = args; + return (T)Convert.ChangeType(value, typeof(T)); } - - /// - /// The added event function - /// - /// Invoked when the has been added to the - public virtual void OnAdded() - { } - - /// - /// The removed event function - /// - /// Invoked when the has been removed from the - public virtual void OnRemoved() - { } - - /// - /// The generic event function - /// - /// - /// - /// Invoked only for the events listed in - public virtual bool OnEvent(string name, IPlayerEvent ev) => true; - - /// - /// A generic function - /// - /// This won't be invoked by UCR - public virtual void Execute() - { } - - /// - /// Try to get a generic value from the and if not present just return a default value. - /// - /// - /// - /// - public object TryGetValue(string param, object def = null) => Args.TryGetValue(param, out object value) ? value : def; - - /// - /// Try to get a value from the and if not present just return a default value. - /// - /// - /// - /// - public string TryGetStringValue(string param, string def = null) => StringArgs.TryGetValue(param, out string value) ? value : def; - - /// - /// Try to get a value from the and if not present just return a default value, with the value converted to the given type . - /// - /// - /// - /// - /// - public T TryGetCastedValue(string param, T def = default) + catch { - if (!Args.TryGetValue(param, out object value)) - return def; - - try - { - return (T)Convert.ChangeType(value, typeof(T)); - } - catch - { - return def; - } + return def; } + } - /// - /// Try to get a value from the and if not present just return a default value, with the value converted to a list of the given type . - /// - /// - /// - /// - public List TryGetCastedListValue(string param) + /// + /// Try to get a value from the and if not present just return a default value, with the value + /// converted to a list of the given type . + /// + /// + /// + /// + public List TryGetCastedListValue(string param) + { + if (!Args.TryGetValue(param, out var value) || value is null) + return []; + switch (value) { - if (!Args.TryGetValue(param, out var value) || value is null) - return new List(); - switch (value) - { - case T t: - return new List { t }; - case List listT: - return listT; - case IEnumerable enumT: - return enumT.ToList(); - case IEnumerable nonGenericEnum: - var result = nonGenericEnum is ICollection col ? new List(col.Count) : new List(); - foreach (var o in nonGenericEnum) - if (TryConvertTo(o, out T converted)) - result.Add(converted); - return result; - default: - return TryConvertTo(value, out T single) ? new List { single } : new List(); - } + case T t: + return [t]; + case List listT: + return listT; + case IEnumerable enumT: + return enumT.ToList(); + case IEnumerable nonGenericEnum: + var result = nonGenericEnum is ICollection col ? new List(col.Count) : new List(); + foreach (var o in nonGenericEnum) + if (TryConvertTo(o, out T converted)) + result.Add(converted); + return result; + default: + return TryConvertTo(value, out T single) ? [single] : []; } + } - private static bool TryConvertTo(object o, out T result) + private static bool TryConvertTo(object o, out T result) + { + try { - try - { - result = ConvertTo(o); - return true; - } - catch - { - result = default; - return false; - } + result = ConvertTo(o); + return true; + } + catch + { + result = default; + return false; } + } + + protected List GetRawListEntries(string param) + { + if (Args is null || !Args.TryGetValue(param, out var value) || value is null) + return []; + + if (value is string s) + return [s]; - private static T ConvertTo(object o) + if (value is IEnumerable enumerable) { - var type = typeof(T); - if (!type.IsEnum) - return (T)Convert.ChangeType(o, type); - if (o is string s) - return (T)Enum.Parse(type, s, true); - return (T)Enum.ToObject(type, Convert.ChangeType(o, Enum.GetUnderlyingType(type))); + List result = []; + foreach (var o in enumerable) + if (o is not null) + result.Add(o.ToString()); + return result; } - /// - /// Logs an error message indicating that the custom module failed to load or had an issue. - /// - public void ThrowError(string message) => LogManager.Error($"[CustomModule] Failed to load CustomModule '{Name}': {message}"); + return [value.ToString()]; + } + + protected List GetInvalidEnumEntries(string param) where T : struct + { + List invalid = []; + foreach (var raw in GetRawListEntries(param)) + if (!Enum.TryParse(raw, true, out T _)) + invalid.Add(raw); + return invalid; + } + + private static T ConvertTo(object o) + { + var type = typeof(T); + if (!type.IsEnum) + return (T)Convert.ChangeType(o, type); + if (o is string s) + return (T)Enum.Parse(type, s, true); + return (T)Enum.ToObject(type, Convert.ChangeType(o, Enum.GetUnderlyingType(type))); + } + + /// + /// Logs an error message indicating that the custom module failed to load or had an issue. + /// + public void ThrowError(string message) + { + LogManager.Error($"[CustomModule] Failed to load CustomModule '{Name}': {message}"); + } #nullable enable - internal static List Load(List modules, SummonedCustomRole summonedCustomRole) - { - LogManager.Silent($"[CM Loader] Initialize loading for {summonedCustomRole}\nPreloaded {YamlFlagsHandler.Modules.Length} modules..."); + internal static List Load(List modules, SummonedCustomRole summonedCustomRole) + { + LogManager.Silent( + $"[CM Loader] Initialize loading for {summonedCustomRole}\nPreloaded {YamlFlagsHandler.Modules.Length} modules..."); - Dictionary?> data = YamlFlagsHandler.Decode(modules) ?? new(); + var data = YamlFlagsHandler.Decode(modules) ?? new Dictionary?>(); - List mods = new(); + List mods = []; - foreach (KeyValuePair?> module in data) - if (InitializeCustomModule(module.Key, module.Value, YamlFlagsHandler.Modules, summonedCustomRole) is CustomModule mod) - mods.Add(mod); + foreach (var module in data) + if (InitializeCustomModule(module.Key, module.Value, YamlFlagsHandler.Modules, summonedCustomRole) is + CustomModule mod) + mods.Add(mod); - LogManager.Debug($"Successfully loaded {mods.Count} CustomModules for player {summonedCustomRole.Player.Nickname}!"); + LogManager.Debug( + $"Successfully loaded {mods.Count} CustomModules for player {summonedCustomRole.Player.Nickname}!"); - return mods; + return mods; + } + + internal static CustomModule? FastAdd(Type type, SummonedCustomRole role, Dictionary? args = null) + { + if (Activator.CreateInstance(type) is not CustomModule module) + { + LogManager.Error( + $"Failed to enable CustomModule '{type?.Name}'!\nError: ERR_CUSTOM_MODULE_NULLREFERENCE_OR_NOTMODULE", + "CM0003"); + return null; } - internal static CustomModule? FastAdd(Type type, SummonedCustomRole role, Dictionary? args = null) + module.Initialize(role, args ?? new Dictionary()); + + if (!ValidateModule(module, type?.Name ?? module.Name, role)) + return null; + + module.OnAdded(); // Invoke added event + + return module; + } + + private static CustomModule? InitializeCustomModule(string name, Dictionary? args, Type[] types, + SummonedCustomRole summonedCustomRole) + { + try { + LogManager.Silent($"[CM Loader] Initialize loading module '{name}' for {summonedCustomRole}"); + + var type = types.FirstOrDefault(t => string.Equals(t.Name, name, StringComparison.OrdinalIgnoreCase)); + + if (type is null) + { + LogManager.Error( + $"[CM Loader] Unknown CustomModule '{name}' on role {RoleLabel(summonedCustomRole)} - it will be ignored.\n" + + $"Available flags: {string.Join(", ", types.Select(t => t.Name).OrderBy(n => n))}", "CM0001"); + return null; + } + if (Activator.CreateInstance(type) is not CustomModule module) { - LogManager.Error($"Failed to enable CustomModule '{type?.Name}'!\nError: ERR_CUSTOM_MODULE_NULLREFERENCE_OR_NOTMODULE", "CM0003"); + LogManager.Error( + $"[CM Loader] Failed to instantiate CustomModule '{type.Name}' on role {RoleLabel(summonedCustomRole)}.", + "CM0002"); return null; } - module.Initialize(role, args ?? new()); + module.Initialize(summonedCustomRole, args ?? new Dictionary()); + + if (!ValidateModule(module, type.Name, summonedCustomRole)) + return null; + module.OnAdded(); // Invoke added event + LogManager.Silent($"[CM Loader] CustomModule '{name}' successfully enabled for {summonedCustomRole}!"); + return module; } - - private static CustomModule? InitializeCustomModule(string name, Dictionary? args, Type[] types, SummonedCustomRole summonedCustomRole) + catch (Exception e) { - try - { - LogManager.Silent($"[CM Loader] Initialize loading module '{name}' for {summonedCustomRole}"); - - Type type = types.FirstOrDefault(t => t.Name == name); - - if (type is null) - { - LogManager.Error($"[CM Loader] Failed to enable CustomModule '{name}'!\nError: ERR_CUSTOM_MODULE_NOT_FOUND", "CM0001"); - return null; - } - - if (Activator.CreateInstance(type) is not CustomModule module) - { - LogManager.Error($"[CM Loader] Failed to enable CustomModule '{name}'!\nError: ERR_CUSTOM_MODULE_NULLREFERENCE_OR_NOTMODULE", "CM0002"); - return null; - } + LogManager.Error( + $"[CM Loader] Unexpected error while enabling CustomModule '{name}' on role {RoleLabel(summonedCustomRole)}:\n{e}"); - module.Initialize(summonedCustomRole, args ?? new()); - module.OnAdded(); // Invoke added event - - LogManager.Silent($"[CM Loader] CustomModule '{name}' successfully enabled for {summonedCustomRole}!"); + return null; + } + } - return module; - } - catch (Exception e) + private static bool ValidateModule(CustomModule module, string name, SummonedCustomRole role) + { + if (module.RequiredArgs is { Count: > 0 }) + { + List missing = module.RequiredArgs.Where(arg => !module.Args.ContainsKey(arg)).ToList(); + if (missing.Count > 0) { - LogManager.Error(e.ToString()); - - return null; + LogManager.Error( + $"[CM Loader] CustomModule '{name}' on role {RoleLabel(role)} is missing required setting(s): {string.Join(", ", missing)}.\n" + + $"Provided setting(s): {(module.Args.Count == 0 ? "(none)" : string.Join(", ", module.Args.Keys))}.\n" + + "This flag will be skipped.", "CM0004"); + return false; } } -/* internal static List ConvertToList(object items) + if (!module.Validate(out var error)) { - switch (items) - { - case null: - return new List(); - case string: - return new List { items.ToString() }; - case List listStr: - return listStr; - case IEnumerable enumStr: - return enumStr.ToList(); - case System.Collections.IEnumerable nonGenericEnum: - { - var result = new List(); - foreach (var o in nonGenericEnum) - result.Add(o?.ToString() ?? string.Empty); - return result; - } - default: - return new List { items.ToString() }; - } - }*/ + LogManager.Error( + $"[CM Loader] CustomModule '{name}' on role {RoleLabel(role)} has an invalid setting: {error}\n" + + "This flag will be skipped.", "CM0005"); + return false; + } + + return true; + } + + private static string RoleLabel(SummonedCustomRole role) + { + return role?.Role is null ? "?" : $"{role.Role.Name} ({role.Role.Id})"; } } \ No newline at end of file diff --git a/UncomplicatedCustomRoles/API/Features/CustomModules/CustomPermissions.cs b/UncomplicatedCustomRoles/API/Features/CustomModules/CustomPermissions.cs index 983e1d9..eda4bc7 100644 --- a/UncomplicatedCustomRoles/API/Features/CustomModules/CustomPermissions.cs +++ b/UncomplicatedCustomRoles/API/Features/CustomModules/CustomPermissions.cs @@ -8,38 +8,43 @@ * If not, see . */ +using System; using System.Collections.Generic; using LabApi.Features.Permissions; -namespace UncomplicatedCustomRoles.API.Features.CustomModules +namespace UncomplicatedCustomRoles.API.Features.CustomModules; + +public class CustomPermissions : CustomModule { - public class CustomPermissions : CustomModule - { - public override List RequiredArgs => new() - { - "permissions" - }; + public override List RequiredArgs => ["permissions"]; - private string[] Permissions => StringArgs.TryGetValue("permissions", out string permissions) ? permissions.Replace(" ", string.Empty).Split(',') : new string[] { }; - - public override void OnAdded() - { - var player = CustomRole.Player; - foreach (var permission in Permissions) - { - player?.AddPermissions(permission); - } - base.OnAdded(); - } + private string[] Permissions => StringArgs.TryGetValue("permissions", out var permissions) + ? permissions.Replace(" ", string.Empty).Split([','], StringSplitOptions.RemoveEmptyEntries) + : []; - public override void OnRemoved() + public override bool Validate(out string error) + { + if (Permissions.Length == 0) { - var player = CustomRole.Player; - foreach (var permission in Permissions) - { - player?.RemovePermissions(permission); - } - base.OnRemoved(); + error = "'permissions' must list at least one permission node, e.g. 'myplugin.command' or 'a.b, c.d'."; + return false; } + + error = null; + return true; + } + + public override void OnAdded() + { + var player = CustomRole.Player; + foreach (var permission in Permissions) player?.AddPermissions(permission); + base.OnAdded(); + } + + public override void OnRemoved() + { + var player = CustomRole.Player; + foreach (var permission in Permissions) player?.RemovePermissions(permission); + base.OnRemoved(); } } \ No newline at end of file diff --git a/UncomplicatedCustomRoles/API/Features/CustomModules/CustomScpAnnouncer.cs b/UncomplicatedCustomRoles/API/Features/CustomModules/CustomScpAnnouncer.cs index ee7a73f..487ffeb 100644 --- a/UncomplicatedCustomRoles/API/Features/CustomModules/CustomScpAnnouncer.cs +++ b/UncomplicatedCustomRoles/API/Features/CustomModules/CustomScpAnnouncer.cs @@ -10,15 +10,11 @@ using System.Collections.Generic; -namespace UncomplicatedCustomRoles.API.Features.CustomModules +namespace UncomplicatedCustomRoles.API.Features.CustomModules; + +public class CustomScpAnnouncer : CustomModule { - public class CustomScpAnnouncer : CustomModule - { - public override List RequiredArgs => new() - { - "name" - }; + public override List RequiredArgs => ["name"]; - internal string RoleName => TryGetStringValue("name", "SCP-404"); - } + internal string RoleName => TryGetStringValue("name", "SCP-404"); } \ No newline at end of file diff --git a/UncomplicatedCustomRoles/API/Features/CustomModules/DamageResistance.cs b/UncomplicatedCustomRoles/API/Features/CustomModules/DamageResistance.cs index 7f115d5..80510b9 100644 --- a/UncomplicatedCustomRoles/API/Features/CustomModules/DamageResistance.cs +++ b/UncomplicatedCustomRoles/API/Features/CustomModules/DamageResistance.cs @@ -8,6 +8,8 @@ * If not, see . */ +using System; +using System.Collections; using System.Collections.Generic; using System.Linq; using InventorySystem.Items.Scp1509; @@ -18,186 +20,226 @@ using PlayerStatsSystem; using UncomplicatedCustomRoles.API.Enums; -namespace UncomplicatedCustomRoles.API.Features.CustomModules +namespace UncomplicatedCustomRoles.API.Features.CustomModules; + +internal class DamageResistance : CustomModule { - internal class DamageResistance : CustomModule + // ----------------------------------------------------------------------- + // + // Copyright (c) ExMod Team. All rights reserved. + // Licensed under the CC BY-SA 3.0 license. + // + // ----------------------------------------------------------------------- + + private static readonly Dictionary ItemConversion = new() { - public override List RequiredArgs => new() - { - "damages" - }; + { ItemType.GunCrossvec, DamageType.Crossvec }, + { ItemType.GunLogicer, DamageType.Logicer }, + { ItemType.GunRevolver, DamageType.Revolver }, + { ItemType.GunShotgun, DamageType.Shotgun }, + { ItemType.GunAK, DamageType.AK }, + { ItemType.GunCOM15, DamageType.Com15 }, + { ItemType.GunCom45, DamageType.Com45 }, + { ItemType.GunCOM18, DamageType.Com18 }, + { ItemType.GunFSP9, DamageType.Fsp9 }, + { ItemType.GunE11SR, DamageType.E11Sr }, + { ItemType.MicroHID, DamageType.MicroHid }, + { ItemType.ParticleDisruptor, DamageType.ParticleDisruptor }, + { ItemType.Jailbird, DamageType.Jailbird }, + { ItemType.GunFRMG0, DamageType.Frmg0 }, + { ItemType.GunA7, DamageType.A7 }, + { ItemType.GunSCP127, DamageType.Scp127 } + }; + + private static readonly Dictionary TranslationConversion = new() + { + { DeathTranslations.Asphyxiated, DamageType.Asphyxiation }, + { DeathTranslations.Bleeding, DamageType.Bleeding }, + { DeathTranslations.Crushed, DamageType.Crushed }, + { DeathTranslations.Decontamination, DamageType.Decontamination }, + { DeathTranslations.Explosion, DamageType.Explosion }, + { DeathTranslations.Falldown, DamageType.Falldown }, + { DeathTranslations.Poisoned, DamageType.Poison }, + { DeathTranslations.Recontained, DamageType.Recontainment }, + { DeathTranslations.Scp049, DamageType.Scp049 }, + { DeathTranslations.Scp096, DamageType.Scp096 }, + { DeathTranslations.Scp173, DamageType.Scp173 }, + { DeathTranslations.Scp207, DamageType.Scp207 }, + { DeathTranslations.Scp939Lunge, DamageType.Scp939 }, + { DeathTranslations.Scp939Other, DamageType.Scp939 }, + { DeathTranslations.Scp3114Slap, DamageType.Scp3114 }, + { DeathTranslations.Tesla, DamageType.Tesla }, + { DeathTranslations.Unknown, DamageType.Unknown }, + { DeathTranslations.Warhead, DamageType.Warhead }, + { DeathTranslations.Zombie, DamageType.Scp0492 }, + { DeathTranslations.BulletWounds, DamageType.Firearm }, + { DeathTranslations.PocketDecay, DamageType.PocketDimension }, + { DeathTranslations.SeveredHands, DamageType.SeveredHands }, + { DeathTranslations.FriendlyFireDetector, DamageType.FriendlyFireDetector }, + { DeathTranslations.UsedAs106Bait, DamageType.FemurBreaker }, + { DeathTranslations.MicroHID, DamageType.MicroHid }, + { DeathTranslations.Hypothermia, DamageType.Hypothermia }, + { DeathTranslations.MarshmallowMan, DamageType.Marshmallow }, + { DeathTranslations.Scp1344, DamageType.SeveredEyes }, + { DeathTranslations.Scp1509, DamageType.Scp1509 } + }; + + private static readonly Dictionary TranslationIdConversion = + TranslationConversion.ToDictionary(x => x.Key.Id, x => x.Value); + + private Dictionary _damageTypes; + public override List RequiredArgs => ["damages"]; + + public override List TriggerOnEvents => ["Hurting"]; + + public override bool Validate(out string error) + { + return ParseDamages(out error) is not null; + } - public override List TriggerOnEvents => new() - { - "Hurting" - }; + public override void OnAdded() + { + _damageTypes = ParseDamages(out _); + } - private Dictionary _damageTypes; + private Dictionary ParseDamages(out string error) + { + error = null; - public override void OnAdded() + if (!Args.TryGetValue("damages", out var raw) || raw is null) { - _damageTypes = TryGetValue("damages", new Dictionary()) as Dictionary; - - if (_damageTypes is null) - ThrowError($"DamageResistance CustomFlag/CustomModule expected a Dictionary in 'damages', got a {TryGetValue("damages", null)?.GetType().FullName}"); + error = "'damages' is missing. Provide a mapping like 'Firearm: 50' (50% less firearm damage)."; + return null; } - public override bool OnEvent(string name, IPlayerEvent ev) + if (raw is Dictionary typed) + return typed; + + if (raw is not IDictionary map) { - if (_damageTypes is null) - return true; + error = + $"'damages' must be a mapping of DamageType: reduction%, e.g. 'Firearm: 50'. Got a {raw.GetType().Name}."; + return null; + } - if (ev is not PlayerHurtingEventArgs hurting) - return true; + Dictionary result = new(); + foreach (DictionaryEntry entry in map) + { + var key = entry.Key?.ToString(); + if (!Enum.TryParse(key, true, out DamageType damageType)) + { + error = + $"'{key}' is not a valid DamageType. Valid values: {string.Join(", ", Enum.GetNames(typeof(DamageType)))}."; + return null; + } - if (hurting.DamageHandler is not StandardDamageHandler standardDamageHandler) - return true; - - DamageType damageType = GetDamageType(hurting.DamageHandler); - if (_damageTypes.TryGetValue(damageType, out uint reduction)) + if (!uint.TryParse(entry.Value?.ToString(), out var reduction) || reduction > 100) { - standardDamageHandler.Damage *= (100f - reduction) / 100f; + error = $"the reduction for '{key}' must be a whole number between 0 and 100, got '{entry.Value}'."; + return null; } - return true; + result[damageType] = reduction; } - public override void OnRemoved() - { - _damageTypes = null; - } - - // ----------------------------------------------------------------------- - // - // Copyright (c) ExMod Team. All rights reserved. - // Licensed under the CC BY-SA 3.0 license. - // - // ----------------------------------------------------------------------- - - private static readonly Dictionary ItemConversion = new() - { - { ItemType.GunCrossvec, DamageType.Crossvec }, - { ItemType.GunLogicer, DamageType.Logicer }, - { ItemType.GunRevolver, DamageType.Revolver }, - { ItemType.GunShotgun, DamageType.Shotgun }, - { ItemType.GunAK, DamageType.AK }, - { ItemType.GunCOM15, DamageType.Com15 }, - { ItemType.GunCom45, DamageType.Com45 }, - { ItemType.GunCOM18, DamageType.Com18 }, - { ItemType.GunFSP9, DamageType.Fsp9 }, - { ItemType.GunE11SR, DamageType.E11Sr }, - { ItemType.MicroHID, DamageType.MicroHid }, - { ItemType.ParticleDisruptor, DamageType.ParticleDisruptor }, - { ItemType.Jailbird, DamageType.Jailbird }, - { ItemType.GunFRMG0, DamageType.Frmg0 }, - { ItemType.GunA7, DamageType.A7 }, - { ItemType.GunSCP127, DamageType.Scp127 }, - }; - - private static readonly Dictionary TranslationConversion = new() - { - { DeathTranslations.Asphyxiated, DamageType.Asphyxiation }, - { DeathTranslations.Bleeding, DamageType.Bleeding }, - { DeathTranslations.Crushed, DamageType.Crushed }, - { DeathTranslations.Decontamination, DamageType.Decontamination }, - { DeathTranslations.Explosion, DamageType.Explosion }, - { DeathTranslations.Falldown, DamageType.Falldown }, - { DeathTranslations.Poisoned, DamageType.Poison }, - { DeathTranslations.Recontained, DamageType.Recontainment }, - { DeathTranslations.Scp049, DamageType.Scp049 }, - { DeathTranslations.Scp096, DamageType.Scp096 }, - { DeathTranslations.Scp173, DamageType.Scp173 }, - { DeathTranslations.Scp207, DamageType.Scp207 }, - { DeathTranslations.Scp939Lunge, DamageType.Scp939 }, - { DeathTranslations.Scp939Other, DamageType.Scp939 }, - { DeathTranslations.Scp3114Slap, DamageType.Scp3114 }, - { DeathTranslations.Tesla, DamageType.Tesla }, - { DeathTranslations.Unknown, DamageType.Unknown }, - { DeathTranslations.Warhead, DamageType.Warhead }, - { DeathTranslations.Zombie, DamageType.Scp0492 }, - { DeathTranslations.BulletWounds, DamageType.Firearm }, - { DeathTranslations.PocketDecay, DamageType.PocketDimension }, - { DeathTranslations.SeveredHands, DamageType.SeveredHands }, - { DeathTranslations.FriendlyFireDetector, DamageType.FriendlyFireDetector }, - { DeathTranslations.UsedAs106Bait, DamageType.FemurBreaker }, - { DeathTranslations.MicroHID, DamageType.MicroHid }, - { DeathTranslations.Hypothermia, DamageType.Hypothermia }, - { DeathTranslations.MarshmallowMan, DamageType.Marshmallow }, - { DeathTranslations.Scp1344, DamageType.SeveredEyes }, - { DeathTranslations.Scp1509, DamageType.Scp1509 }, - }; - - private static readonly Dictionary TranslationIdConversion = TranslationConversion.ToDictionary(x => x.Key.Id, x => x.Value); - - private static DamageType GetDamageType(DamageHandlerBase damageHandlerBase) + return result; + } + + public override bool OnEvent(string name, IPlayerEvent ev) + { + if (_damageTypes is null) + return true; + + if (ev is not PlayerHurtingEventArgs hurting) + return true; + + if (hurting.DamageHandler is not StandardDamageHandler standardDamageHandler) + return true; + + var damageType = GetDamageType(hurting.DamageHandler); + if (_damageTypes.TryGetValue(damageType, out var reduction)) + standardDamageHandler.Damage *= (100f - reduction) / 100f; + + return true; + } + + public override void OnRemoved() + { + _damageTypes = null; + } + + private static DamageType GetDamageType(DamageHandlerBase damageHandlerBase) + { + switch (damageHandlerBase) { - switch (damageHandlerBase) + case CustomReasonDamageHandler: + return DamageType.Custom; + case WarheadDamageHandler: + return DamageType.Warhead; + case ExplosionDamageHandler: + return DamageType.Explosion; + case Scp018DamageHandler: + return DamageType.Scp018; + case RecontainmentDamageHandler: + return DamageType.Recontainment; + case Scp096DamageHandler: + return DamageType.Scp096; + case MicroHidDamageHandler: + return DamageType.MicroHid; + case DisruptorDamageHandler: + return DamageType.ParticleDisruptor; + case Scp1507DamageHandler: + return DamageType.Scp1507; + case Scp956DamageHandler: + return DamageType.Scp956; + case SnowballDamageHandler: + return DamageType.SnowBall; + case GrayCandyDamageHandler: + return DamageType.GrayCandy; + case Scp1509DamageHandler: + return DamageType.Scp1509; + case Scp049DamageHandler scp049DamageHandler: + return scp049DamageHandler.DamageSubType switch + { + Scp049DamageHandler.AttackType.CardiacArrest => DamageType.CardiacArrest, + Scp049DamageHandler.AttackType.Instakill => DamageType.Scp049, + Scp049DamageHandler.AttackType.Scp0492 => DamageType.Scp0492, + _ => DamageType.Unknown + }; + case Scp3114DamageHandler scp3114DamageHandler: + return scp3114DamageHandler.Subtype switch + { + Scp3114DamageHandler.HandlerType.Strangulation => DamageType.Strangled, + Scp3114DamageHandler.HandlerType.SkinSteal => DamageType.Scp3114, + Scp3114DamageHandler.HandlerType.Slap => DamageType.Scp3114, + _ => DamageType.Unknown + }; + case FirearmDamageHandler firearmDamageHandler: + return ItemConversion.TryGetValue(firearmDamageHandler.WeaponType, out var value) + ? value + : DamageType.Firearm; + + case ScpDamageHandler scpDamageHandler: { - case CustomReasonDamageHandler: - return DamageType.Custom; - case WarheadDamageHandler: - return DamageType.Warhead; - case ExplosionDamageHandler: - return DamageType.Explosion; - case Scp018DamageHandler: - return DamageType.Scp018; - case RecontainmentDamageHandler: - return DamageType.Recontainment; - case Scp096DamageHandler: - return DamageType.Scp096; - case MicroHidDamageHandler: - return DamageType.MicroHid; - case DisruptorDamageHandler: - return DamageType.ParticleDisruptor; - case Scp1507DamageHandler: - return DamageType.Scp1507; - case Scp956DamageHandler: - return DamageType.Scp956; - case SnowballDamageHandler: - return DamageType.SnowBall; - case GrayCandyDamageHandler: - return DamageType.GrayCandy; - case Scp1509DamageHandler: - return DamageType.Scp1509; - case Scp049DamageHandler scp049DamageHandler: - return scp049DamageHandler.DamageSubType switch - { - Scp049DamageHandler.AttackType.CardiacArrest => DamageType.CardiacArrest, - Scp049DamageHandler.AttackType.Instakill => DamageType.Scp049, - Scp049DamageHandler.AttackType.Scp0492 => DamageType.Scp0492, - _ => DamageType.Unknown, - }; - case Scp3114DamageHandler scp3114DamageHandler: - return scp3114DamageHandler.Subtype switch - { - Scp3114DamageHandler.HandlerType.Strangulation => DamageType.Strangled, - Scp3114DamageHandler.HandlerType.SkinSteal => DamageType.Scp3114, - Scp3114DamageHandler.HandlerType.Slap => DamageType.Scp3114, - _ => DamageType.Unknown, - }; - case FirearmDamageHandler firearmDamageHandler: - return ItemConversion.TryGetValue(firearmDamageHandler.WeaponType, out var value) ? value : DamageType.Firearm; - - case ScpDamageHandler scpDamageHandler: - { - DeathTranslation translation = DeathTranslations.TranslationsById[scpDamageHandler._translationId]; - if (translation.Id == DeathTranslations.PocketDecay.Id) - return DamageType.Scp106; - - return TranslationIdConversion.TryGetValue(translation.Id, out var value1) - ? value1 - : DamageType.Scp; - } - - case UniversalDamageHandler universal: - { - DeathTranslation translation = DeathTranslations.TranslationsById[universal.TranslationId]; - - return TranslationIdConversion.TryGetValue(translation.Id, out var damageType) ? damageType : DamageType.Unknown; - } + var translation = DeathTranslations.TranslationsById[scpDamageHandler._translationId]; + if (translation.Id == DeathTranslations.PocketDecay.Id) + return DamageType.Scp106; + + return TranslationIdConversion.TryGetValue(translation.Id, out var value1) + ? value1 + : DamageType.Scp; } - return DamageType.Unknown; + case UniversalDamageHandler universal: + { + var translation = DeathTranslations.TranslationsById[universal.TranslationId]; + + return TranslationIdConversion.TryGetValue(translation.Id, out var damageType) + ? damageType + : DamageType.Unknown; + } } + + return DamageType.Unknown; } } \ No newline at end of file diff --git a/UncomplicatedCustomRoles/API/Features/CustomModules/DoNotTrigger096.cs b/UncomplicatedCustomRoles/API/Features/CustomModules/DoNotTrigger096.cs index 141653d..b8460e1 100644 --- a/UncomplicatedCustomRoles/API/Features/CustomModules/DoNotTrigger096.cs +++ b/UncomplicatedCustomRoles/API/Features/CustomModules/DoNotTrigger096.cs @@ -1,15 +1,15 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . */ -namespace UncomplicatedCustomRoles.API.Features.CustomModules +namespace UncomplicatedCustomRoles.API.Features.CustomModules; + +public class DoNotTrigger096 : CustomModule { - public class DoNotTrigger096 : CustomModule - { } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/API/Features/CustomModules/DoNotTriggerTeslaGates.cs b/UncomplicatedCustomRoles/API/Features/CustomModules/DoNotTriggerTeslaGates.cs index 0024b46..a416d83 100644 --- a/UncomplicatedCustomRoles/API/Features/CustomModules/DoNotTriggerTeslaGates.cs +++ b/UncomplicatedCustomRoles/API/Features/CustomModules/DoNotTriggerTeslaGates.cs @@ -1,25 +1,24 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . */ -using LabApi.Events.Arguments.Interfaces; using System.Collections.Generic; +using LabApi.Events.Arguments.Interfaces; + +namespace UncomplicatedCustomRoles.API.Features.CustomModules; -namespace UncomplicatedCustomRoles.API.Features.CustomModules +public class DoNotTriggerTeslaGates : CustomModule { - public class DoNotTriggerTeslaGates : CustomModule - { - public override List TriggerOnEvents => new() - { - "TriggeringTesla" - }; + public override List TriggerOnEvents => ["TriggeringTesla"]; - public override bool OnEvent(string name, IPlayerEvent ev) => false; + public override bool OnEvent(string name, IPlayerEvent ev) + { + return false; } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/API/Features/CustomModules/DropItemOnDeath.cs b/UncomplicatedCustomRoles/API/Features/CustomModules/DropItemOnDeath.cs index f1069cc..b9b2e4b 100644 --- a/UncomplicatedCustomRoles/API/Features/CustomModules/DropItemOnDeath.cs +++ b/UncomplicatedCustomRoles/API/Features/CustomModules/DropItemOnDeath.cs @@ -8,30 +8,43 @@ * If not, see . */ -using LabApi.Features.Wrappers; -using MEC; using System; using System.Collections.Generic; +using LabApi.Features.Wrappers; +using MEC; + +namespace UncomplicatedCustomRoles.API.Features.CustomModules; -namespace UncomplicatedCustomRoles.API.Features.CustomModules +public class DropItemOnDeath : CustomModule { - public class DropItemOnDeath : CustomModule - { - public override List RequiredArgs => new() - { - "item" - }; + public override List RequiredArgs => ["item"]; - public ItemType? Item => StringArgs.TryGetValue("item", out string rawItem) && Enum.TryParse(rawItem, out ItemType item) && item is not ItemType.None ? item : null; + public ItemType? Item => + StringArgs.TryGetValue("item", out var rawItem) && Enum.TryParse(rawItem, true, out ItemType item) && + item is not ItemType.None + ? item + : null; - public override void OnRemoved() + public override bool Validate(out string error) + { + if (Item is null) { - if (Item is ItemType item) - Timing.CallDelayed(0.5f, () => - { - var pickup = Pickup.Create(item, CustomRole.Player.Position); - pickup?.Spawn(); - }); + error = + $"'item' value '{TryGetStringValue("item")}' is not a valid ItemType. Examples: Medkit, KeycardScientist, GunCOM15, Coin."; + return false; } + + error = null; + return true; + } + + public override void OnRemoved() + { + if (Item is ItemType item) + Timing.CallDelayed(0.5f, () => + { + var pickup = Pickup.Create(item, CustomRole.Player.Position); + pickup?.Spawn(); + }); } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/API/Features/CustomModules/DropNothingOnDeath.cs b/UncomplicatedCustomRoles/API/Features/CustomModules/DropNothingOnDeath.cs index d795f5d..c2733f5 100644 --- a/UncomplicatedCustomRoles/API/Features/CustomModules/DropNothingOnDeath.cs +++ b/UncomplicatedCustomRoles/API/Features/CustomModules/DropNothingOnDeath.cs @@ -1,16 +1,15 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . */ -namespace UncomplicatedCustomRoles.API.Features.CustomModules +namespace UncomplicatedCustomRoles.API.Features.CustomModules; + +public class DropNothingOnDeath : CustomModule { - public class DropNothingOnDeath : CustomModule - { - } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/API/Features/CustomModules/FullCandyBag.cs b/UncomplicatedCustomRoles/API/Features/CustomModules/FullCandyBag.cs index 5470dbf..9e1c8ec 100644 --- a/UncomplicatedCustomRoles/API/Features/CustomModules/FullCandyBag.cs +++ b/UncomplicatedCustomRoles/API/Features/CustomModules/FullCandyBag.cs @@ -8,25 +8,43 @@ * If not, see . */ -using InventorySystem.Items.Usables.Scp330; +using System; using System.Collections.Generic; using InventorySystem.Items; +using InventorySystem.Items.Usables.Scp330; + +namespace UncomplicatedCustomRoles.API.Features.CustomModules; -namespace UncomplicatedCustomRoles.API.Features.CustomModules +public class FullCandyBag : CustomModule { - public class FullCandyBag : CustomModule + public override List RequiredArgs => ["candies"]; + + internal List Kinds => TryGetCastedListValue("candies"); + + public override bool Validate(out string error) { - public override List RequiredArgs => new() + var invalid = GetInvalidEnumEntries("candies"); + if (invalid.Count > 0) { - "candies" - }; - - internal List Kinds => TryGetCastedListValue("candies"); + error = + $"'candies' contains invalid candy value(s): {string.Join(", ", invalid)}. Valid values: {string.Join(", ", Enum.GetNames(typeof(CandyKindID)))}."; + return false; + } - public override void OnAdded() + if (Kinds.Count == 0) { - foreach (CandyKindID kind in Kinds) - CustomRole.Player.GiveCandy(kind, ItemAddReason.AdminCommand); + error = + $"'candies' must list at least one valid candy. Valid values: {string.Join(", ", Enum.GetNames(typeof(CandyKindID)))}."; + return false; } + + error = null; + return true; + } + + public override void OnAdded() + { + foreach (var kind in Kinds) + CustomRole.Player.GiveCandy(kind, ItemAddReason.AdminCommand); } } \ No newline at end of file diff --git a/UncomplicatedCustomRoles/API/Features/CustomModules/ItemBan.cs b/UncomplicatedCustomRoles/API/Features/CustomModules/ItemBan.cs index 564bfc3..5190e4d 100644 --- a/UncomplicatedCustomRoles/API/Features/CustomModules/ItemBan.cs +++ b/UncomplicatedCustomRoles/API/Features/CustomModules/ItemBan.cs @@ -1,8 +1,8 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . @@ -10,15 +10,31 @@ using System.Collections.Generic; -namespace UncomplicatedCustomRoles.API.Features.CustomModules +namespace UncomplicatedCustomRoles.API.Features.CustomModules; + +public class ItemBan : CustomModule { - public class ItemBan : CustomModule + public override List RequiredArgs => ["item_type"]; + + public List Items => TryGetCastedListValue("item_type"); + + public override bool Validate(out string error) { - public override List RequiredArgs => new() + var invalid = GetInvalidEnumEntries("item_type"); + if (invalid.Count > 0) + { + error = + $"'item_type' contains invalid ItemType value(s): {string.Join(", ", invalid)}. Examples: GunAK, Medkit, KeycardO5."; + return false; + } + + if (Items.Count == 0) { - "item_type" - }; + error = "'item_type' must list at least one valid ItemType (e.g. GunAK, Medkit, KeycardO5)."; + return false; + } - public List Items => TryGetCastedListValue("item_type"); + error = null; + return true; } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/API/Features/CustomModules/KeepInventoryOnEscape.cs b/UncomplicatedCustomRoles/API/Features/CustomModules/KeepInventoryOnEscape.cs index 7e85cf4..6572f54 100644 --- a/UncomplicatedCustomRoles/API/Features/CustomModules/KeepInventoryOnEscape.cs +++ b/UncomplicatedCustomRoles/API/Features/CustomModules/KeepInventoryOnEscape.cs @@ -8,10 +8,21 @@ * If not, see . */ -namespace UncomplicatedCustomRoles.API.Features.CustomModules +namespace UncomplicatedCustomRoles.API.Features.CustomModules; + +internal class KeepInventoryOnEscape : CustomModule { - internal class KeepInventoryOnEscape : CustomModule + public bool DropItems => TryGetCastedValue("drop", true); + + public override bool Validate(out string error) { - public bool DropItems => TryGetValue("drop", true) is not bool drop || drop; + if (Args.TryGetValue("drop", out var raw) && raw is not null && !bool.TryParse(raw.ToString(), out _)) + { + error = $"'drop' must be true or false, got '{raw}'."; + return false; + } + + error = null; + return true; } } \ No newline at end of file diff --git a/UncomplicatedCustomRoles/API/Features/CustomModules/LifeStealer.cs b/UncomplicatedCustomRoles/API/Features/CustomModules/LifeStealer.cs index 60f0e93..dc9d7f4 100644 --- a/UncomplicatedCustomRoles/API/Features/CustomModules/LifeStealer.cs +++ b/UncomplicatedCustomRoles/API/Features/CustomModules/LifeStealer.cs @@ -1,8 +1,8 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . @@ -10,15 +10,32 @@ using System.Collections.Generic; -namespace UncomplicatedCustomRoles.API.Features.CustomModules +namespace UncomplicatedCustomRoles.API.Features.CustomModules; + +public class LifeStealer : CustomModule { - public class LifeStealer : CustomModule + public override List RequiredArgs => ["percentage"]; + + public int Percentage => StringArgs.TryGetValue("percentage", out var perc) && int.TryParse(perc, out var numPerc) + ? numPerc + : 0; + + public override bool Validate(out string error) { - public override List RequiredArgs => new() + var raw = TryGetStringValue("percentage"); + if (!int.TryParse(raw, out var perc)) + { + error = $"'percentage' must be a whole number between 0 and 100 (e.g. 75 for 75%), got '{raw}'."; + return false; + } + + if (perc is < 0 or > 100) { - "percentage" - }; + error = $"'percentage' must be between 0 and 100, got {perc}."; + return false; + } - public int Percentage => StringArgs.TryGetValue("percentage", out string perc) && int.TryParse(perc, out int numPerc) ? numPerc : 0; // NOTE: Percentage MUST be an int so like 75 is 75% (0.75) + error = null; + return true; } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/API/Features/CustomModules/NoUnitName.cs b/UncomplicatedCustomRoles/API/Features/CustomModules/NoUnitName.cs index f4997f5..cd833f0 100644 --- a/UncomplicatedCustomRoles/API/Features/CustomModules/NoUnitName.cs +++ b/UncomplicatedCustomRoles/API/Features/CustomModules/NoUnitName.cs @@ -1,15 +1,15 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . */ -namespace UncomplicatedCustomRoles.API.Features.CustomModules +namespace UncomplicatedCustomRoles.API.Features.CustomModules; + +public class NoUnitName : CustomModule { - public class NoUnitName : CustomModule - { } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/API/Features/CustomModules/NotAffectedByAppearance.cs b/UncomplicatedCustomRoles/API/Features/CustomModules/NotAffectedByAppearance.cs index 6110b9a..da180f6 100644 --- a/UncomplicatedCustomRoles/API/Features/CustomModules/NotAffectedByAppearance.cs +++ b/UncomplicatedCustomRoles/API/Features/CustomModules/NotAffectedByAppearance.cs @@ -1,15 +1,15 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . */ -namespace UncomplicatedCustomRoles.API.Features.CustomModules +namespace UncomplicatedCustomRoles.API.Features.CustomModules; + +public class NotAffectedByAppearance : CustomModule { - public class NotAffectedByAppearance : CustomModule - { } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/API/Features/CustomModules/PacifismUntilDamage.cs b/UncomplicatedCustomRoles/API/Features/CustomModules/PacifismUntilDamage.cs index ca5a97a..e273c11 100644 --- a/UncomplicatedCustomRoles/API/Features/CustomModules/PacifismUntilDamage.cs +++ b/UncomplicatedCustomRoles/API/Features/CustomModules/PacifismUntilDamage.cs @@ -1,15 +1,15 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . */ -namespace UncomplicatedCustomRoles.API.Features.CustomModules +namespace UncomplicatedCustomRoles.API.Features.CustomModules; + +public class PacifismUntilDamage : CustomModule { - public class PacifismUntilDamage : CustomModule - { } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/API/Features/CustomModules/Schematic.cs b/UncomplicatedCustomRoles/API/Features/CustomModules/Schematic.cs index 787ce8e..774f2de 100644 --- a/UncomplicatedCustomRoles/API/Features/CustomModules/Schematic.cs +++ b/UncomplicatedCustomRoles/API/Features/CustomModules/Schematic.cs @@ -10,36 +10,45 @@ using System.Collections.Generic; using UncomplicatedCustomRoles.API.Features.Controllers; +using UnityEngine; -namespace UncomplicatedCustomRoles.API.Features.CustomModules +namespace UncomplicatedCustomRoles.API.Features.CustomModules; + +internal class Schematic : CustomModule { - internal class Schematic : CustomModule + public override List RequiredArgs => ["name"]; + + private string TargetName => TryGetStringValue("name"); + + public override bool Validate(out string error) { - public override List RequiredArgs => new() + if (string.IsNullOrWhiteSpace(TargetName)) { - "name" - }; + error = "'name' must be the name of a schematic to spawn; it cannot be empty."; + return false; + } - private string TargetName => TryGetStringValue("name"); + error = null; + return true; + } - public override void OnAdded() + public override void OnAdded() + { + if (TargetName is null) { - if (TargetName is null) - { - ThrowError("Argument 'name' not found!"); - return; - } - - SchematicController controller = CustomRole.Player.GameObject.AddComponent(); - controller.Init(TargetName); + ThrowError("Argument 'name' not found!"); + return; } - public override void OnRemoved() - { - if (TargetName is null) - return; + var controller = CustomRole.Player.GameObject.AddComponent(); + controller.Init(TargetName); + } - UnityEngine.Object.Destroy(CustomRole.Player.GameObject.GetComponent()); - } + public override void OnRemoved() + { + if (TargetName is null) + return; + + Object.Destroy(CustomRole.Player.GameObject.GetComponent()); } } \ No newline at end of file diff --git a/UncomplicatedCustomRoles/API/Features/CustomModules/SilentAnnouncer.cs b/UncomplicatedCustomRoles/API/Features/CustomModules/SilentAnnouncer.cs index 755143a..9858483 100644 --- a/UncomplicatedCustomRoles/API/Features/CustomModules/SilentAnnouncer.cs +++ b/UncomplicatedCustomRoles/API/Features/CustomModules/SilentAnnouncer.cs @@ -1,15 +1,15 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . */ -namespace UncomplicatedCustomRoles.API.Features.CustomModules +namespace UncomplicatedCustomRoles.API.Features.CustomModules; + +public class SilentAnnouncer : CustomModule { - public class SilentAnnouncer : CustomModule - { } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/API/Features/CustomModules/SilentWalker.cs b/UncomplicatedCustomRoles/API/Features/CustomModules/SilentWalker.cs index 1a1e65a..ddbccd6 100644 --- a/UncomplicatedCustomRoles/API/Features/CustomModules/SilentWalker.cs +++ b/UncomplicatedCustomRoles/API/Features/CustomModules/SilentWalker.cs @@ -1,15 +1,15 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . */ -namespace UncomplicatedCustomRoles.API.Features.CustomModules +namespace UncomplicatedCustomRoles.API.Features.CustomModules; + +public class SilentWalker : CustomModule { - public class SilentWalker : CustomModule - { } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/API/Features/CustomModules/TutorialRagdoll.cs b/UncomplicatedCustomRoles/API/Features/CustomModules/TutorialRagdoll.cs index aaf6a55..71297ac 100644 --- a/UncomplicatedCustomRoles/API/Features/CustomModules/TutorialRagdoll.cs +++ b/UncomplicatedCustomRoles/API/Features/CustomModules/TutorialRagdoll.cs @@ -1,15 +1,15 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . */ -namespace UncomplicatedCustomRoles.API.Features.CustomModules +namespace UncomplicatedCustomRoles.API.Features.CustomModules; + +public class TutorialRagdoll : CustomModule { - public class TutorialRagdoll : CustomModule - { } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/API/Features/CustomModules/Wardrobe.cs b/UncomplicatedCustomRoles/API/Features/CustomModules/Wardrobe.cs index 18a34f3..34508a9 100644 --- a/UncomplicatedCustomRoles/API/Features/CustomModules/Wardrobe.cs +++ b/UncomplicatedCustomRoles/API/Features/CustomModules/Wardrobe.cs @@ -11,37 +11,45 @@ using System.Collections.Generic; using UncomplicatedCustomRoles.Integrations; -namespace UncomplicatedCustomRoles.API.Features.CustomModules +namespace UncomplicatedCustomRoles.API.Features.CustomModules; + +internal class Wardrobe : CustomModule { - internal class Wardrobe : CustomModule + public override List RequiredArgs => ["name"]; + + private string TargetName => TryGetStringValue("name"); + + public override bool Validate(out string error) { - public override List RequiredArgs => new() + if (string.IsNullOrWhiteSpace(TargetName)) { - "name" - }; + error = "'name' must be the name of an SLWardrobe suit; it cannot be empty."; + return false; + } - private string TargetName => TryGetStringValue("name"); + error = null; + return true; + } - public override void OnAdded() + public override void OnAdded() + { + if (TargetName is null) { - if (TargetName is null) - { - ThrowError("Argument 'name' not found!"); - return; - } + ThrowError("Argument 'name' not found!"); + return; + } - if (SLWardobe.PluginInstance is null) - ThrowError("Plugin 'SLWardrobe' not found!\nMake sure it's installed and enabled to use that flag!"); + if (SLWardobe.PluginInstance is null) + ThrowError("Plugin 'SLWardrobe' not found!\nMake sure it's installed and enabled to use that flag!"); - SLWardobe.ApplySuit(CustomRole.Player, TargetName); - } + SLWardobe.ApplySuit(CustomRole.Player, TargetName); + } - public override void OnRemoved() - { - if (TargetName is null) - return; - - SLWardobe.RemoveSuit(CustomRole.Player); - } + public override void OnRemoved() + { + if (TargetName is null) + return; + + SLWardobe.RemoveSuit(CustomRole.Player); } } \ No newline at end of file diff --git a/UncomplicatedCustomRoles/API/Features/CustomRole.cs b/UncomplicatedCustomRoles/API/Features/CustomRole.cs index 8c91f10..04bb05a 100644 --- a/UncomplicatedCustomRoles/API/Features/CustomRole.cs +++ b/UncomplicatedCustomRoles/API/Features/CustomRole.cs @@ -1,18 +1,17 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . */ using System.Collections.Concurrent; -using PlayerRoles; using System.Collections.Generic; -using System.Linq; using System.Text.RegularExpressions; +using PlayerRoles; using UncomplicatedCustomRoles.API.Enums; using UncomplicatedCustomRoles.API.Features.Behaviour; using UncomplicatedCustomRoles.API.Interfaces; @@ -20,316 +19,297 @@ using UncomplicatedCustomRoles.Manager; using UnityEngine; -namespace UncomplicatedCustomRoles.API.Features -{ +namespace UncomplicatedCustomRoles.API.Features; #nullable enable - public class CustomRole : ICustomRole +public class CustomRole : ICustomRole +{ + /// + /// A more easy-to-use dictionary to store every registered + /// + internal static ConcurrentDictionary CustomRoles { get; set; } = new(); + + /// + /// Get a list of every registered. + /// + public static ICollection List => CustomRoles.Values; + + /// + /// Gets a list of every not loaded custom role. + /// The data is the Id, the role path, the error type and the error name + /// + internal static List NotLoadedRoles { get; } = []; + + /// + /// Gets a list of every outdated loaded roles. + /// The data is the CustomRole, the plugin Version and the role path + /// + internal static List OutdatedRoles { get; } = []; + + /// + /// Gets or sets the unique Id + /// + public virtual int Id { get; set; } = 1; + + /// + /// Gets or sets the name of the custom role.

+ /// Thisn won't be shown to players, just a thing to help you recognize better your custom roles. + ///
+ public virtual string Name { get; set; } = "Janitor"; + + /// + /// Gets or sets whether the name should be hidden in favor of the + /// + public virtual bool OverrideRoleName { get; set; } = false; + + /// + /// Gets or sets the nickname that will be set to the player if not null. + /// + public virtual string? Nickname { get; set; } = "D-%dnumber%"; + + /// + /// Gets or sets the CustomInfo that will be give to the player.

+ /// Will be visible only to other players + ///
+ public virtual string CustomInfo { get; set; } = "Janitor"; + + /// + /// Gets or sets the badge name + /// + public virtual string BadgeName { get; set; } = "Janitor"; + + /// + /// Gets or sets the badge color + /// + public virtual string BadgeColor { get; set; } = "pumpkin"; + + /// + /// Gets or sets the of the player + /// + public virtual RoleTypeId Role { get; set; } = RoleTypeId.ClassD; + + /// + /// Gets or sets the of the player + /// + public virtual Team? Team { get; set; } = null; + + /// + /// Gets or sets the the Role Appeareance for the player.

+ /// If it's equal to then won't be applied + ///
+ public virtual RoleTypeId RoleAppearance { get; set; } = RoleTypeId.ClassD; + + /// + /// Gets or sets the (s) that will be "friends" with this custom role + /// + public virtual List IsFriendOf { get; set; } = []; + + /// + /// Gets or sets the + /// + public virtual HealthBehaviour Health { get; set; } = new(); + + /// + /// Gets or sets the + /// + public virtual AhpBehaviour Ahp { get; set; } = new(); + + /// + /// Gets or sets the + /// + public virtual HumeShieldBehaviour HumeShield { get; set; } = new(); + + /// + /// Gets or sets the + /// + public virtual List? Effects { get; set; } = []; + + /// + /// Gets or sets the + /// + public virtual StaminaBehaviour Stamina { get; set; } = new(); + + /// + /// Gets or sets the maximum number of candies that can be took by the player without losing hands + /// + public virtual int MaxScp330Candies { get; set; } = 2; + + /// + /// Gets or sets whether the player can escape or not + /// + public virtual bool CanEscape { get; set; } = true; + + /// + /// Gets or sets the role after escape + /// + public virtual Dictionary RoleAfterEscape { get; set; } = new() { - /// - /// A more easy-to-use dictionary to store every registered - /// - internal static ConcurrentDictionary CustomRoles { get; set; } = new(); - - /// - /// Get a list of every registered. - /// - public static ICollection List => CustomRoles.Values; - - /// - /// Gets a list of every not loaded custom role. - /// The data is the Id, the role path, the error type and the error name - /// - internal static List NotLoadedRoles { get; } = new(); - - /// - /// Gets a list of every outdated loaded roles. - /// The data is the CustomRole, the plugin Version and the role path - /// - internal static List OutdatedRoles { get; } = new(); - - /// - /// Gets or sets the unique Id - /// - public virtual int Id { get; set; } = 1; - - /// - /// Gets or sets the name of the custom role.

- /// Thisn won't be shown to players, just a thing to help you recognize better your custom roles. - ///
- public virtual string Name { get; set; } = "Janitor"; - - /// - /// Gets or sets whether the name should be hidden in favor of the - /// - public virtual bool OverrideRoleName { get; set; } = false; - - /// - /// Gets or sets the nickname that will be set to the player if not null. - /// - public virtual string? Nickname { get; set; } = "D-%dnumber%"; - - /// - /// Gets or sets the CustomInfo that will be give to the player.

- /// Will be visible only to other players - ///
- public virtual string CustomInfo { get; set; } = "Janitor"; - - /// - /// Gets or sets the badge name - /// - public virtual string BadgeName { get; set; } = "Janitor"; - - /// - /// Gets or sets the badge color - /// - public virtual string BadgeColor { get; set; } = "pumpkin"; - - /// - /// Gets or sets the of the player - /// - public virtual RoleTypeId Role { get; set; } = RoleTypeId.ClassD; - - /// - /// Gets or sets the of the player - /// - public virtual Team? Team { get; set; } = null; - - /// - /// Gets or sets the the Role Appeareance for the player.

- /// If it's equal to then won't be applied - ///
- public virtual RoleTypeId RoleAppearance { get; set; } = RoleTypeId.ClassD; - - /// - /// Gets or sets the (s) that will be "friends" with this custom role - /// - public virtual List IsFriendOf { get; set; } = new(); - - /// - /// Gets or sets the - /// - public virtual HealthBehaviour Health { get; set; } = new(); - - /// - /// Gets or sets the - /// - public virtual AhpBehaviour Ahp { get; set; } = new(); - - /// - /// Gets or sets the - /// - public virtual HumeShieldBehaviour HumeShield { get; set; } = new(); - - /// - /// Gets or sets the - /// - public virtual List? Effects { get; set; } = new(); - - /// - /// Gets or sets the - /// - public virtual StaminaBehaviour Stamina { get; set; } = new(); - - /// - /// Gets or sets the maximum number of candies that can be took by the player without losing hands - /// - public virtual int MaxScp330Candies { get; set; } = 2; - - /// - /// Gets or sets whether the player can escape or not - /// - public virtual bool CanEscape { get; set; } = true; - - /// - /// Gets or sets the role after escape - /// - public virtual Dictionary RoleAfterEscape { get; set; } = new() { - { - "default", - "InternalRole Spectator" - }, - { - "cuffed by InternalTeam ChaosInsurgency", - "InternalRole ClassD" - } - }; - - /// - /// Gets or sets the scale of the player - /// - public virtual Vector3 Scale { get; set; } = Vector3.one; - - /// - /// Gets or sets the broadcast that will be shown to the player when spawned - /// - public virtual string SpawnBroadcast { get; set; } = "You are a Janitor!\nClean the Light Containment Zone!"; - - /// - /// Gets or sets the broadcast duration - /// - public virtual ushort SpawnBroadcastDuration { get; set; } = 5; - - /// - /// Gets or sets the hint that will be shown to the player when spawned - /// - public virtual string SpawnHint { get; set; } = "This hint will be shown when you will spawn as a Janitor!"; - - /// - /// Gets or sets hint duration - /// - public virtual float SpawnHintDuration { get; set; } = 5; - - /// - /// Gets or sets the custom inventory limits to override the default ones - /// - public virtual Dictionary CustomInventoryLimits { get; set; } = new(); - - /// - /// Gets or sets the inventory of the player - /// - public virtual List Inventory { get; set; } = new() + "default", + "InternalRole Spectator" + }, { - ItemType.Flashlight, - ItemType.KeycardJanitor - }; - - /// - /// Gets or sets the custom items inventory of the player - /// - public virtual List CustomItemsInventory { get; set; } = new(); - - /// - /// Gets or sets the ammo inventory of the player - /// - public virtual Dictionary Ammo { get; set; } = new() + "cuffed by InternalTeam ChaosInsurgency", + "InternalRole ClassD" + } + }; + + /// + /// Gets or sets the scale of the player + /// + public virtual Vector3 Scale { get; set; } = Vector3.one; + + /// + /// Gets or sets the broadcast that will be shown to the player when spawned + /// + public virtual string SpawnBroadcast { get; set; } = + "You are a Janitor!\nClean the Light Containment Zone!"; + + /// + /// Gets or sets the broadcast duration + /// + public virtual ushort SpawnBroadcastDuration { get; set; } = 5; + + /// + /// Gets or sets the hint that will be shown to the player when spawned + /// + public virtual string SpawnHint { get; set; } = "This hint will be shown when you will spawn as a Janitor!"; + + /// + /// Gets or sets hint duration + /// + public virtual float SpawnHintDuration { get; set; } = 5; + + /// + /// Gets or sets the custom inventory limits to override the default ones + /// + public virtual Dictionary CustomInventoryLimits { get; set; } = new(); + + /// + /// Gets or sets the inventory of the player + /// + public virtual List Inventory { get; set; } = + [ + ItemType.Flashlight, + ItemType.KeycardJanitor + ]; + + /// + /// Gets or sets the custom items inventory of the player + /// + public virtual List CustomItemsInventory { get; set; } = []; + + /// + /// Gets or sets the ammo inventory of the player + /// + public virtual Dictionary Ammo { get; set; } = new() + { { - { - ItemType.Ammo9x19, - 10 - } - }; - - /// - /// Gets or sets the damage multiplier.

- /// This will increase - keep normal - or decrease the damage that this role will do - ///
- public virtual float DamageMultiplier { get; set; } = 1; - - /// - /// Gets or sets the - /// - public virtual SpawnBehaviour? SpawnSettings { get; set; } = new(); - - /// - /// Gets or sets the of the custom role - /// - public virtual List? CustomFlags { get; set; } = null; - - /// - /// Gets or sets whether the custom role should be evaluated during normal spawn events or not - /// - public virtual bool IgnoreSpawnSystem { get; set; } = false; - - /// - /// Invoked when the custom role is spawned - /// - /// - public virtual void OnSpawned(SummonedCustomRole role) - { } - - public override string ToString() => $"{Regex.Replace(Name, "(.*?)", "$1")} ({Id})"; + ItemType.Ammo9x19, + 10 + } + }; + + /// + /// Gets or sets the damage multiplier.

+ /// This will increase - keep normal - or decrease the damage that this role will do + ///
+ public virtual float DamageMultiplier { get; set; } = 1; + + /// + /// Gets or sets the + /// + public virtual SpawnBehaviour? SpawnSettings { get; set; } = new(); + + /// + /// Gets or sets the of the custom role + /// + public virtual List? CustomFlags { get; set; } = null; + + /// + /// Gets or sets whether the custom role should be evaluated during normal spawn events or not + /// + public virtual bool IgnoreSpawnSystem { get; set; } = false; + + /// + /// Invoked when the custom role is spawned + /// + /// + public virtual void OnSpawned(SummonedCustomRole role) + { + } + + public override string ToString() + { + return $"{Regex.Replace(Name, "(.*?)", "$1")} ({Id})"; + } #nullable disable - /// - /// Try to get a registered by it's Id. - /// - /// - /// - /// if the operation was successfull. - public static bool TryGet(int id, out ICustomRole customRole) + /// + /// Try to get a registered by it's Id. + /// + /// + /// + /// if the operation was successfull. + public static bool TryGet(int id, out ICustomRole customRole) + { + if (CustomRoles.ContainsKey(id)) { - if (CustomRoles.ContainsKey(id)) - { - customRole = CustomRoles[id]; - return true; - } - - customRole = null; - return false; + customRole = CustomRoles[id]; + return true; } - /// - /// Get a registered by it's Id - /// - /// - /// The with the given Id or if not found. - public static ICustomRole Get(int id) - { - if (TryGet(id, out ICustomRole customRole)) - return customRole; + customRole = null; + return false; + } - return null; - } + /// + /// Get a registered by it's Id + /// + /// + /// The with the given Id or if not found. + public static ICustomRole Get(int id) + { + if (TryGet(id, out var customRole)) + return customRole; - /// - /// Register a new instance. - /// - /// - public static LoadStatusType Register(ICustomRole customRole) => CompatibilityManager.RegisterCustomRole(customRole); - - /// - /// Unregister a registered . - /// - /// - public static void Unregister(ICustomRole customRole) - { - CustomRoles.TryRemove(customRole.Id, out _); - } + return null; + } - internal static bool Validate(ICustomRole role, out string error) - { - error = "Role seems to be null"; - - if (role is null) - return false; - - if (role.SpawnSettings is null) - { - error = $"Role has no spawn_settings"; - return false; - } - - if (role.SpawnSettings.Spawn is SpawnType.ZoneSpawn && !role.SpawnSettings.SpawnZones.Any()) - { - error = "If the SpawnType is ZoneSpawn the list SpawnZones shouldn't be empty"; - return false; - } - else if (role.SpawnSettings.Spawn is SpawnType.RoomsSpawn && !role.SpawnSettings.SpawnRooms.Any()) - { - error = "If the SpawnType is RoomsSpawn the list SpawnRooms shouldn't be empty"; - return false; - } - else if (role.SpawnSettings.Spawn is SpawnType.SpawnPointSpawn && (role.SpawnSettings.SpawnPoints is null || !role.SpawnSettings.SpawnPoints.Any())) - { - error = "If the SpawnType is SpawnPointSpawn the list SpawnPoints shouldn't be empty"; - return false; - } + /// + /// Register a new instance. + /// + /// + public static LoadStatusType Register(ICustomRole customRole) + { + return CompatibilityManager.RegisterCustomRole(customRole); + } - return true; - } + /// + /// Unregister a registered . + /// + /// + public static void Unregister(ICustomRole customRole) + { + CustomRoles.TryRemove(customRole.Id, out _); + } - internal static LoadStatusType InternalRegister(ICustomRole customRole) - { - if (!Validate(customRole, out string _)) - return LoadStatusType.ValidatorError; + internal static bool Validate(ICustomRole role, out string error) + { + return RoleValidator.IsValid(role, out error); + } - if (CustomRoles.TryAdd(customRole.Id, customRole)) - { - return LoadStatusType.Success; - } + internal static LoadStatusType InternalRegister(ICustomRole customRole) + { + RoleValidator.Validate(customRole, out var errors, out var warnings); - return LoadStatusType.SameId; - } + foreach (var warning in warnings) + LogManager.Warn($"[Role Validator] {customRole}: {warning}"); + + if (errors.Count > 0) + return LoadStatusType.ValidatorError; + + if (CustomRoles.TryAdd(customRole.Id, customRole)) return LoadStatusType.Success; + + return LoadStatusType.SameId; } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/API/Features/CustomRoleEventHandler.cs b/UncomplicatedCustomRoles/API/Features/CustomRoleEventHandler.cs index a2452b8..5d4321c 100644 --- a/UncomplicatedCustomRoles/API/Features/CustomRoleEventHandler.cs +++ b/UncomplicatedCustomRoles/API/Features/CustomRoleEventHandler.cs @@ -1,109 +1,115 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . */ -using LabApi.Events.Arguments.Interfaces; using System; using System.Collections.Generic; using System.Linq; using System.Reflection; +using LabApi.Events.Arguments.Interfaces; using UncomplicatedCustomRoles.API.Interfaces; using UncomplicatedCustomRoles.Manager; -namespace UncomplicatedCustomRoles.API.Features +namespace UncomplicatedCustomRoles.API.Features; + +public class CustomRoleEventHandler { - public class CustomRoleEventHandler + private static int _activeListeners; + + internal CustomRoleEventHandler(SummonedCustomRole summonedInstance) { - public SummonedCustomRole SummonedInstance { get; } + SummonedInstance = summonedInstance; + LoadListeners(); + _activeListeners += Listeners.Count; + } - public ICustomRole Role => SummonedInstance.Role; + public SummonedCustomRole SummonedInstance { get; } - public List Listeners { get; } = new(); - - private static int _activeListeners; + public ICustomRole Role => SummonedInstance.Role; - internal CustomRoleEventHandler(SummonedCustomRole summonedInstance) - { - SummonedInstance = summonedInstance; - LoadListeners(); - _activeListeners += Listeners.Count; - } - - internal void Unload() - { - _activeListeners -= Listeners.Count; - if (_activeListeners < 0) - _activeListeners = 0; - Listeners.Clear(); - } + public List Listeners { get; } = []; - private void LoadListeners() + internal void Unload() + { + _activeListeners -= Listeners.Count; + if (_activeListeners < 0) + _activeListeners = 0; + Listeners.Clear(); + } + + private void LoadListeners() + { + try { - try + if (Role is EventCustomRole customRoleEventsRole) { - if (Role is EventCustomRole customRoleEventsRole) + var baseType = typeof(EventCustomRole); + var declaredType = customRoleEventsRole.GetType(); + + foreach (var method in declaredType + .GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | + BindingFlags.DeclaredOnly).Where(m => + m.GetBaseDefinition().DeclaringType == baseType && !m.IsSpecialName && + m.Name is not "OnSpawned")) { - Type baseType = typeof(EventCustomRole); - Type declaredType = (customRoleEventsRole as EventCustomRole).GetType(); + var derivedMethod = declaredType.GetMethod(method.Name); + var isOverride = derivedMethod != null && derivedMethod.DeclaringType != baseType; - foreach (MethodInfo method in declaredType.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly).Where(m => m.GetBaseDefinition().DeclaringType == baseType && !m.IsSpecialName && m.Name is not "OnSpawned")) + if (isOverride && derivedMethod.GetParameters().Length > 0) { - MethodInfo derivedMethod = declaredType.GetMethod(method.Name); - bool isOverride = derivedMethod != null && derivedMethod.DeclaringType != baseType; - - if (isOverride && derivedMethod.GetParameters().Length > 0) - { - Listeners.Add(new(derivedMethod.GetParameters()[0].ParameterType, derivedMethod, customRoleEventsRole)); - LogManager.Debug($"Loaded listener for [Event]CustomRole {customRoleEventsRole}: EVENT={derivedMethod.GetParameters()[0].ParameterType}, METHOD={derivedMethod.Name}()"); - } + Listeners.Add(new Listener(derivedMethod.GetParameters()[0].ParameterType, derivedMethod, + customRoleEventsRole)); + LogManager.Debug( + $"Loaded listener for [Event]CustomRole {customRoleEventsRole}: EVENT={derivedMethod.GetParameters()[0].ParameterType}, METHOD={derivedMethod.Name}()"); } } } - catch (Exception e) - { - LogManager.Error($"Failed to act CustomRoleEventHandler::LoadListeners() - {e.GetType().FullName}: {e.Message}\n{e.StackTrace}"); - } } - - internal void InvokeSafely(IPlayerEvent playerEvent) + catch (Exception e) { - if (Listeners.Count == 0) - return; + LogManager.Error( + $"Failed to act CustomRoleEventHandler::LoadListeners() - {e.GetType().FullName}: {e.Message}\n{e.StackTrace}"); + } + } - if (playerEvent is ICancellableEvent { IsAllowed: false }) - return; + internal void InvokeSafely(IPlayerEvent playerEvent) + { + if (Listeners.Count == 0) + return; - Type eventType = playerEvent.GetType(); - foreach (Listener listener in Listeners) - if (listener.Event == eventType) - { - listener.Method.Invoke(listener.Instance, [playerEvent]); - return; - } - } + if (playerEvent is ICancellableEvent { IsAllowed: false }) + return; - internal static void InvokeAll(IPlayerEvent ev) - { - if (_activeListeners == 0) + var eventType = playerEvent.GetType(); + foreach (var listener in Listeners) + if (listener.Event == eventType) + { + listener.Method.Invoke(listener.Instance, [playerEvent]); return; - - foreach (KeyValuePair pair in SummonedCustomRole.List) - pair.Value.EventHandler?.InvokeSafely(ev); - } + } } - public class Listener(Type @event, MethodInfo method, object instance) + internal static void InvokeAll(IPlayerEvent ev) { - public Type Event { get; } = @event; - - public MethodInfo Method { get; } = method; + if (_activeListeners == 0) + return; - public object Instance { get; } = instance; + foreach (var pair in SummonedCustomRole.List) + pair.Value.EventHandler?.InvokeSafely(ev); } } + +public class Listener(Type @event, MethodInfo method, object instance) +{ + public Type Event { get; } = @event; + + public MethodInfo Method { get; } = method; + + public object Instance { get; } = instance; +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/API/Features/DisguiseTeam.cs b/UncomplicatedCustomRoles/API/Features/DisguiseTeam.cs index f98b9a9..6b40024 100644 --- a/UncomplicatedCustomRoles/API/Features/DisguiseTeam.cs +++ b/UncomplicatedCustomRoles/API/Features/DisguiseTeam.cs @@ -8,61 +8,60 @@ * If not, see . */ -using PlayerRoles; using System.Collections.Concurrent; +using PlayerRoles; using UncomplicatedCustomRoles.Patches; -namespace UncomplicatedCustomRoles.API.Features -{ - public class DisguiseTeam - { - /// - /// Maps a player id to the their real team is being faked as. - /// - public static readonly ConcurrentDictionary List = new(); +namespace UncomplicatedCustomRoles.API.Features; - /// - /// Maps a player id to the overridden used to trick the server into - /// treating the player as (not) an human. - /// - public static readonly ConcurrentDictionary RoleBaseList = new(); +public class DisguiseTeam +{ + /// + /// Maps a player id to the their real team is being faked as. + /// + public static readonly ConcurrentDictionary List = new(); - /// - /// Registers a faked together with the overridden that is - /// exposed as the player's current role. The role base is always set alongside its team, so there is no - /// separate "role base only" entry point. - /// - /// The player id. - /// The team to fake. - /// The role base to expose as the player's current role. - public static void Set(int playerId, Team team, PlayerRoleBase roleBase) - { - List[playerId] = team; - RoleBaseList[playerId] = roleBase; - TeamPatchManager.EnsurePatched(); - } + /// + /// Maps a player id to the overridden used to trick the server into + /// treating the player as (not) an human. + /// + public static readonly ConcurrentDictionary RoleBaseList = new(); - /// - /// Removes every disguise data for the given player and, if no disguise is left, removes the team patches. - /// - /// The player id. - public static void Remove(int playerId) - { - List.TryRemove(playerId, out _); - RoleBaseList.TryRemove(playerId, out _); + /// + /// Registers a faked together with the overridden that is + /// exposed as the player's current role. The role base is always set alongside its team, so there is no + /// separate "role base only" entry point. + /// + /// The player id. + /// The team to fake. + /// The role base to expose as the player's current role. + public static void Set(int playerId, Team team, PlayerRoleBase roleBase) + { + List[playerId] = team; + RoleBaseList[playerId] = roleBase; + TeamPatchManager.EnsurePatched(); + } - if (List.IsEmpty) - TeamPatchManager.EnsureUnpatched(); - } + /// + /// Removes every disguise data for the given player and, if no disguise is left, removes the team patches. + /// + /// The player id. + public static void Remove(int playerId) + { + List.TryRemove(playerId, out _); + RoleBaseList.TryRemove(playerId, out _); - /// - /// Clears every disguise data and removes the team patches. Used during plugin (re)load. - /// - public static void Clear() - { - List.Clear(); - RoleBaseList.Clear(); + if (List.IsEmpty) TeamPatchManager.EnsureUnpatched(); - } + } + + /// + /// Clears every disguise data and removes the team patches. Used during plugin (re)load. + /// + public static void Clear() + { + List.Clear(); + RoleBaseList.Clear(); + TeamPatchManager.EnsureUnpatched(); } } \ No newline at end of file diff --git a/UncomplicatedCustomRoles/API/Features/Effect.cs b/UncomplicatedCustomRoles/API/Features/Effect.cs index 61b56e7..b45413c 100644 --- a/UncomplicatedCustomRoles/API/Features/Effect.cs +++ b/UncomplicatedCustomRoles/API/Features/Effect.cs @@ -1,8 +1,8 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . @@ -10,28 +10,27 @@ using UncomplicatedCustomRoles.API.Interfaces; -namespace UncomplicatedCustomRoles.Manager +namespace UncomplicatedCustomRoles.Manager; + +public class Effect : IEffect { - public class Effect : IEffect - { - /// - /// Gets or sets the of the effect - /// - public string EffectType { get; set; } = "MovementBoost"; + /// + /// Gets or sets the of the effect + /// + public string EffectType { get; set; } = "MovementBoost"; - /// - /// Gets or sets the duration of the effect - /// - public float Duration { get; set; } = -1; + /// + /// Gets or sets the duration of the effect + /// + public float Duration { get; set; } = -1; - /// - /// Gets or sets the intensity of the effect - /// - public byte Intensity { get; set; } = 1; + /// + /// Gets or sets the intensity of the effect + /// + public byte Intensity { get; set; } = 1; - /// - /// Gets or sets whether the effect can be removed by using SCP-500 - /// - public bool Removable { get; set; } = false; - } + /// + /// Gets or sets whether the effect can be removed by using SCP-500 + /// + public bool Removable { get; set; } = false; } \ No newline at end of file diff --git a/UncomplicatedCustomRoles/API/Features/Escape.cs b/UncomplicatedCustomRoles/API/Features/Escape.cs index 72cce11..52bbb8c 100644 --- a/UncomplicatedCustomRoles/API/Features/Escape.cs +++ b/UncomplicatedCustomRoles/API/Features/Escape.cs @@ -1,30 +1,29 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . */ +using System.Collections.Generic; using LabApi.Features.Wrappers; using MEC; -using System.Collections.Generic; -namespace UncomplicatedCustomRoles.API.Features +namespace UncomplicatedCustomRoles.API.Features; + +internal class Escape { - internal class Escape - { - /// - /// Gets the escape bucket to avoid the spam of SubclassSpawn of a custom role during the spawn - /// - public static HashSet Bucket { get; } = new(); + /// + /// Gets the escape bucket to avoid the spam of SubclassSpawn of a custom role during the spawn + /// + public static HashSet Bucket { get; } = []; - public static void AddBucket(Player player, float waitingTime = 5f) - { - Bucket.Add(player.PlayerId); - Timing.CallDelayed(waitingTime, () => Bucket.Remove(player.PlayerId)); - } + public static void AddBucket(Player player, float waitingTime = 5f) + { + Bucket.Add(player.PlayerId); + Timing.CallDelayed(waitingTime, () => Bucket.Remove(player.PlayerId)); } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/API/Features/EventCustomRole.cs b/UncomplicatedCustomRoles/API/Features/EventCustomRole.cs index 76bef16..c578b42 100644 --- a/UncomplicatedCustomRoles/API/Features/EventCustomRole.cs +++ b/UncomplicatedCustomRoles/API/Features/EventCustomRole.cs @@ -1,1389 +1,1775 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . */ -using LabApi.Events.Arguments.PlayerEvents; -using LabApi.Events.Arguments.ServerEvents; -using PlayerRoles; using System; using System.Collections.Generic; using System.Text.RegularExpressions; using LabApi.Events.Arguments.ObjectiveEvents; +using LabApi.Events.Arguments.PlayerEvents; using LabApi.Events.Arguments.Scp127Events; using LabApi.Events.Arguments.Scp3114Events; +using LabApi.Events.Arguments.ServerEvents; +using PlayerRoles; using UncomplicatedCustomRoles.API.Features.Behaviour; using UncomplicatedCustomRoles.API.Interfaces; using UncomplicatedCustomRoles.Manager; using UnityEngine; -namespace UncomplicatedCustomRoles.API.Features -{ +namespace UncomplicatedCustomRoles.API.Features; #nullable enable - public class EventCustomRole : ICustomRole - { - /// - /// Gets or sets the unique Id - /// - public virtual int Id { get; set; } = 1; - - /// - /// Gets or sets the name of the custom role.

- /// Thisn won't be shown to players, just a thing to help you recognize better your custom roles. - ///
- public virtual string Name { get; set; } = "Janitor"; - - /// - /// Gets or sets whether the name should be hidden in favor of the - /// - public virtual bool OverrideRoleName { get; set; } = false; - - /// - /// Gets or sets the nickname that will be set to the player if not null. - /// - public virtual string? Nickname { get; set; } = "D-%dnumber%"; - - /// - /// Gets or sets the CustomInfo that will be give to the player.

- /// Will be visible only to other players - ///
- public virtual string CustomInfo { get; set; } = "Janitor"; - - /// - /// Gets or sets the badge name - /// - public virtual string BadgeName { get; set; } = "Janitor"; - - /// - /// Gets or sets the badge color - /// - public virtual string BadgeColor { get; set; } = "pumpkin"; - - /// - /// Gets or sets the of the player - /// - public virtual RoleTypeId Role { get; set; } = RoleTypeId.ClassD; - - /// - /// Gets or sets the of the player - /// - public virtual Team? Team { get; set; } = null; - - /// - /// Gets or sets the the Role Appeareance for the player.

- /// If it's equal to then won't be applied - ///
- public virtual RoleTypeId RoleAppearance { get; set; } = RoleTypeId.ClassD; - - /// - /// Gets or sets the (s) that will be "friends" with this custom role - /// - public virtual List IsFriendOf { get; set; } = new(); - - /// - /// Gets or sets the - /// - public virtual HealthBehaviour Health { get; set; } = new(); - - /// - /// Gets or sets the - /// - public virtual AhpBehaviour Ahp { get; set; } = new(); - - /// - /// Gets or sets the - /// - public virtual HumeShieldBehaviour HumeShield { get; set; } = new(); - - /// - /// Gets or sets the - /// - public virtual List? Effects { get; set; } = new(); - - /// - /// Gets or sets the - /// - public virtual StaminaBehaviour Stamina { get; set; } = new(); - - /// - /// Gets or sets the maximum number of candies that can be took by the player without losing hands - /// - public virtual int MaxScp330Candies { get; set; } = 2; - - /// - /// Gets or sets whether the player can escape or not - /// - public virtual bool CanEscape { get; set; } = true; - - /// - /// Gets or sets the role after escape - /// - public virtual Dictionary RoleAfterEscape { get; set; } = new() +public class EventCustomRole : ICustomRole +{ + /// + /// Gets or sets the unique Id + /// + public virtual int Id { get; set; } = 1; + + /// + /// Gets or sets the name of the custom role.

+ /// Thisn won't be shown to players, just a thing to help you recognize better your custom roles. + ///
+ public virtual string Name { get; set; } = "Janitor"; + + /// + /// Gets or sets whether the name should be hidden in favor of the + /// + public virtual bool OverrideRoleName { get; set; } = false; + + /// + /// Gets or sets the nickname that will be set to the player if not null. + /// + public virtual string? Nickname { get; set; } = "D-%dnumber%"; + + /// + /// Gets or sets the CustomInfo that will be give to the player.

+ /// Will be visible only to other players + ///
+ public virtual string CustomInfo { get; set; } = "Janitor"; + + /// + /// Gets or sets the badge name + /// + public virtual string BadgeName { get; set; } = "Janitor"; + + /// + /// Gets or sets the badge color + /// + public virtual string BadgeColor { get; set; } = "pumpkin"; + + /// + /// Gets or sets the of the player + /// + public virtual RoleTypeId Role { get; set; } = RoleTypeId.ClassD; + + /// + /// Gets or sets the of the player + /// + public virtual Team? Team { get; set; } = null; + + /// + /// Gets or sets the the Role Appeareance for the player.

+ /// If it's equal to then won't be applied + ///
+ public virtual RoleTypeId RoleAppearance { get; set; } = RoleTypeId.ClassD; + + /// + /// Gets or sets the (s) that will be "friends" with this custom role + /// + public virtual List IsFriendOf { get; set; } = []; + + /// + /// Gets or sets the + /// + public virtual HealthBehaviour Health { get; set; } = new(); + + /// + /// Gets or sets the + /// + public virtual AhpBehaviour Ahp { get; set; } = new(); + + /// + /// Gets or sets the + /// + public virtual HumeShieldBehaviour HumeShield { get; set; } = new(); + + /// + /// Gets or sets the + /// + public virtual List? Effects { get; set; } = []; + + /// + /// Gets or sets the + /// + public virtual StaminaBehaviour Stamina { get; set; } = new(); + + /// + /// Gets or sets the maximum number of candies that can be took by the player without losing hands + /// + public virtual int MaxScp330Candies { get; set; } = 2; + + /// + /// Gets or sets whether the player can escape or not + /// + public virtual bool CanEscape { get; set; } = true; + + /// + /// Gets or sets the role after escape + /// + public virtual Dictionary RoleAfterEscape { get; set; } = new() + { { - { - "default", - "InternalRole Spectator" - }, - { - "cuffed by InternalTeam ChaosInsurgency", - "InternalRole ClassD" - } - }; - - /// - /// Gets or sets the scale of the player - /// - public virtual Vector3 Scale { get; set; } = Vector3.one; - - /// - /// Gets or sets the broadcast that will be shown to the player when spawned - /// - public virtual string SpawnBroadcast { get; set; } = "You are a Janitor!\nClean the Light Containment Zone!"; - - /// - /// Gets or sets the broadcast duration - /// - public virtual ushort SpawnBroadcastDuration { get; set; } = 5; - - /// - /// Gets or sets the hint that will be shown to the player when spawned - /// - public virtual string SpawnHint { get; set; } = "This hint will be shown when you will spawn as a Janitor!"; - - /// - /// Gets or sets hint duration - /// - public virtual float SpawnHintDuration { get; set; } = 5; - - /// - /// Gets or sets the custom inventory limits to override the default ones - /// - public virtual Dictionary CustomInventoryLimits { get; set; } = new() + "default", + "InternalRole Spectator" + }, { - { - ItemCategory.Medical, - 2 - } - }; - - /// - /// Gets or sets the inventory of the player - /// - public virtual List Inventory { get; set; } = new() + "cuffed by InternalTeam ChaosInsurgency", + "InternalRole ClassD" + } + }; + + /// + /// Gets or sets the scale of the player + /// + public virtual Vector3 Scale { get; set; } = Vector3.one; + + /// + /// Gets or sets the broadcast that will be shown to the player when spawned + /// + public virtual string SpawnBroadcast { get; set; } = + "You are a Janitor!\nClean the Light Containment Zone!"; + + /// + /// Gets or sets the broadcast duration + /// + public virtual ushort SpawnBroadcastDuration { get; set; } = 5; + + /// + /// Gets or sets the hint that will be shown to the player when spawned + /// + public virtual string SpawnHint { get; set; } = "This hint will be shown when you will spawn as a Janitor!"; + + /// + /// Gets or sets hint duration + /// + public virtual float SpawnHintDuration { get; set; } = 5; + + /// + /// Gets or sets the custom inventory limits to override the default ones + /// + public virtual Dictionary CustomInventoryLimits { get; set; } = new() + { { - ItemType.Flashlight, - ItemType.KeycardJanitor - }; - - /// - /// Gets or sets the custom items inventory of the player - /// - public virtual List CustomItemsInventory { get; set; } = new(); - - /// - /// Gets or sets the ammo inventory of the player - /// - public virtual Dictionary Ammo { get; set; } = new() + ItemCategory.Medical, + 2 + } + }; + + /// + /// Gets or sets the inventory of the player + /// + public virtual List Inventory { get; set; } = + [ + ItemType.Flashlight, + ItemType.KeycardJanitor + ]; + + /// + /// Gets or sets the custom items inventory of the player + /// + public virtual List CustomItemsInventory { get; set; } = []; + + /// + /// Gets or sets the ammo inventory of the player + /// + public virtual Dictionary Ammo { get; set; } = new() + { { - { - ItemType.Ammo9x19, - 10 - } - }; - - /// - /// Gets or sets the damage multiplier.

- /// This will increase - keep normal - or decrease the damage that this role will do - ///
- public virtual float DamageMultiplier { get; set; } = 1; - - /// - /// Gets or sets the - /// - public virtual SpawnBehaviour? SpawnSettings { get; set; } = new(); - - /// - /// Gets or sets the of the custom role - /// - public virtual List? CustomFlags { get; set; } = null; - - /// - /// Gets or sets whether the custom role should be evaluated during normal spawn events or not - /// - public virtual bool IgnoreSpawnSystem { get; set; } = false; - - public override string ToString() => $"{Regex.Replace(Name, "(.*?)", "$1")} ({Id})"; - - /// - /// Invoked when the Custom Role is spawned - /// - /// - public virtual void OnSpawned(SummonedCustomRole role) - { } - - /// - /// Invoked when the Custom Role is spawned - /// - /// - public virtual void OnRemoved(SummonedCustomRole role) - { } - - /// - /// Called before kicking a from the server. - /// - /// The instance. - public virtual void OnKicking(PlayerKickingEventArgs ev) { } - - /// - /// Called after a has been kicked from the server. - /// - /// The instance. - public virtual void OnKicked(PlayerKickedEventArgs ev) { } - - /// - /// Called before banning a from the server. - /// - /// The instance. - public virtual void OnBanning(PlayerBanningEventArgs ev) { } - - /// - /// Called before a danger state changes. - /// - /// The instance. - [Obsolete("Not available on LabAPI")] - public virtual void OnChangingDangerState(object ev) { } - - /// - /// Called after a player has been banned from the server. - /// - /// The instance. - public virtual void OnBanned(PlayerBannedEventArgs ev) { } - - /// - /// Called before a earns an achievement. - /// - /// The instance. - public virtual void OnReceivedAchievement(PlayerReceivedAchievementEventArgs ev) { } - - /// - /// Called before using a usable item. - /// - /// The instance. - public virtual void OnUsingItem(PlayerUsingItemEventArgs ev) { } - - /// - /// Called before completed using of a usable item. - /// - /// The instance. - [Obsolete("Only works on EXILED due to the need of a patch, please refer to OnUsedItem")] - public virtual void OnUsingItemCompleted(object ev) { } - - /// - /// Called after a used a item. - /// - /// The instance. - public virtual void OnUsedItem(PlayerUsedItemEventArgs ev) { } - - /// - /// Called before a has stopped the use of a item. - /// - /// The instance. - public virtual void OnCancellingItemUse(PlayerCancellingUsingItemEventArgs ev) { } - - /// - /// Called after a has stopped the use of a item. - /// - /// The instance. - public virtual void OnCancelledItemUse(PlayerCancelledUsingItemEventArgs ev) { } - - /// - /// Called after a interacted with something. - /// - /// The instance. - [Obsolete("The generic interaction event is not available in LabAPI, please handle every interaction in a separate method.")] - public virtual void OnInteracted(object ev) { } - - /// - /// Called before spawning a ragdoll. - /// - /// The instance. - public virtual void OnSpawningRagdoll(PlayerSpawningRagdollEventArgs ev) { } - - /// - /// Called after spawning a ragdoll. - /// - /// The instance. - public virtual void OnSpawnedRagdoll(PlayerSpawnedRagdollEventArgs ev) { } - - /// - /// Called before activating the warhead panel. - /// - /// The instance. - [Obsolete("Not available on LabAPI")] - public virtual void OnActivatingWarheadPanel(object ev) { } - - /// - /// Called before activating a workstation. - /// - /// The instance. - [Obsolete("Not available on LabAPI")] - public virtual void OnActivatingWorkstation(object ev) { } - - /// - /// Called before deactivating a workstation. - /// - /// The instance. - [Obsolete("Not available on LabAPI")] - public virtual void OnDeactivatingWorkstation(object ev) { } - - /// - /// Called after a has left the server. - /// - /// The instance. - public virtual void OnLeft(PlayerLeftEventArgs ev) { } - - /// - /// Called after a died. - /// - /// The instance. - public virtual void OnDied(PlayerDeathEventArgs ev) { } - - /// - /// Called before changing a role. - /// - /// The instance. - /// If is set to when Escape is , awards will still be given to the escapee's team even though they will 'fail' to escape. Use to block escapes instead. - public virtual void OnChangingRole(PlayerChangingRoleEventArgs ev) { } - - /// - /// Called before throwing a grenade. - /// - /// The instance. - public virtual void OnThrowingProjectile(PlayerThrowingProjectileEventArgs ev) { } - - /// - /// Called after threw a grenade. - /// - /// The instance. - public virtual void OnThrewProjectile(PlayerThrewProjectileEventArgs ev) { } - - /// - /// Called before receving a throwing request. - /// - /// The instance. - [Obsolete("Please refer to OnThrowingItem")] - public virtual void OnThrowingRequest(object ev) { } - - /// - /// Called before a throws an item. - /// - /// The instance. - public virtual void OnThrowingItem(PlayerThrowingItemEventArgs ev) { } - - /// - /// Called after threw an item. - /// - /// The instance. - public virtual void OnThrewItem(PlayerThrewItemEventArgs ev) { } - - /// - /// Called before dropping an item. - /// - /// The instance. - public virtual void OnDroppingItem(PlayerDroppingItemEventArgs ev) { } - - /// - /// Called after dropping an item. - /// - /// The instance. - public virtual void OnDroppedItem(PlayerDroppedItemEventArgs ev) { } - - /// - /// Called before dropping a null item. - /// - /// The instance. - [Obsolete("Not available on LabAPI")] - public virtual void OnDroppingNothing(object ev) { } - - /// - /// Called before a picks up an item. - /// - /// The instance. - public virtual void OnPickingUpItem(PlayerPickingUpItemEventArgs ev) { } - - /// - /// Called before handcuffing a . - /// - /// The instance. - public virtual void OnHandcuffing(PlayerCuffingEventArgs ev) { } - - /// - /// Called after handcuffing a . - /// - /// The instance. - public virtual void OnHandcuffed(PlayerCuffedEventArgs ev) { } - - /// - /// Called before freeing a handcuffed . - /// - /// The instance. - public virtual void OnRemovingHandcuffs(PlayerUncuffingEventArgs ev) { } - - /// - /// Called after freeing a handcuffed . - /// - /// The instance. - public virtual void OnRemovedHandcuffs(PlayerUncuffedEventArgs ev) { } - - /// - /// Called before a escapes. - /// - /// The instance. - public virtual void OnEscaping(PlayerEscapingEventArgs ev) { } - - /// - /// Called before a escapes. - /// - /// The instance. - public virtual void OnEscaped(PlayerEscapedEventArgs ev) { } - - /// - /// Called before a begins speaking in the intercom. - /// - /// The instance. - public virtual void OnIntercomSpeaking(PlayerUsingIntercomEventArgs ev) { } - - /// - /// Called after a finished speaking in the intercom. - /// - /// The instance. - public virtual void OnIntercomSpeakingFinished(PlayerUsedIntercomEventArgs ev) { } - - /// - /// Called after a shoots a weapon. - /// - /// The instance. - public virtual void OnShot(PlayerShotWeaponEventArgs ev) { } - - /// - /// Called before a shoots a weapon. - /// - /// The instance. - public virtual void OnShooting(PlayerShootingWeaponEventArgs ev) { } - - /// - /// Called before a enters the pocket dimension. - /// - /// The instance. - public virtual void OnEnteringPocketDimension(PlayerEnteringPocketDimensionEventArgs ev) { } - - /// - /// Called after a enters the pocket dimension. - /// - /// The instance. - - public virtual void OnEnteredPocketDimension(PlayerEnteredPocketDimensionEventArgs ev) { } - - /// - /// Called before a leaves the pocket dimension. - /// - /// The instance. - [Obsolete("Not available on LabAPI, please see OnLeftPocketDimension")] - public virtual void OnEscapingPocketDimension(object ev) { } - - /// - /// Called before a leaves the pocket dimension. - /// - /// The instance. - public virtual void OnLeavingPocketDimension(PlayerLeavingPocketDimensionEventArgs ev) { } - - /// - /// Called before a fails to escape the pocket dimension. - /// - /// The instance. - [Obsolete("Not available on LabAPI, please see OnLeftPocketDimension")] - public virtual void OnFailingEscapePocketDimension(object ev) { } - - /// - /// Called after a left the pocket dimension. - /// - /// The instance. - public virtual void OnLeftPocketDimension(PlayerLeftPocketDimensionEventArgs ev) { } - - /// - /// Called before a enters killer collision. - /// - /// The instance. - [Obsolete("Not available on LabAPI")] - public virtual void OnEnteringKillerCollision(object ev) { } - - /// - /// Called before a reloads a weapon. - /// - /// The instance. - public virtual void OnReloadingWeapon(PlayerReloadingWeaponEventArgs ev) { } - - /// - /// Called after a held item changes. - /// - /// The instance. - public virtual void OnChangedItem(PlayerChangedItemEventArgs ev) { } - - /// - /// Called before a held item changes. - /// - /// The instance. - public virtual void OnChangingItem(PlayerChangingItemEventArgs ev) { } - - /// - /// Called before changing a group. - /// - /// The instance. - public virtual void OnChangingGroup(PlayerGroupChangingEventArgs ev) { } - - /// - /// Called after changing a group. - /// - /// The instance. - public virtual void OnChangedGroup(PlayerGroupChangedEventArgs ev) { } - - /// - /// Called before a interacts with an elevator. - /// - /// The instance. - public virtual void OnInteractingElevator(PlayerInteractingElevatorEventArgs ev) { } - - /// - /// Called after a interacts with an elevator. - /// - /// The instance. - public virtual void OnInteractedElevator(PlayerInteractedElevatorEventArgs ev) { } - - /// - /// Called before a interacts with a locker. - /// - /// The instance. - public virtual void OnInteractingLocker(PlayerInteractingLockerEventArgs ev) { } - - /// - /// Called after a interacts with a locker. - /// - /// The instance. - public virtual void OnInteractedLocker(PlayerInteractedLockerEventArgs ev) { } - - /// - /// Called before a interacts with a generator. - /// - /// The instance. - public virtual void OnInteractingGenerator(PlayerInteractingGeneratorEventArgs ev) { } - - /// - /// Called after a interacts with a generator. - /// - /// The instance. - public virtual void OnInteractedGenerator(PlayerInteractedGeneratorEventArgs ev) { } - - /// - /// Called before a interacts with a door. - /// - /// The instance. - public virtual void OnInteractingDoor(PlayerInteractingDoorEventArgs ev) { } - - /// - /// Called after a interacts with a door. - /// - /// The instance. - public virtual void OnInteractedDoor(PlayerInteractedDoorEventArgs ev) { } - - /// - /// Called before a interacts with SCP-330. - /// - /// The instance. - public virtual void OnInteractingScp330(PlayerInteractingScp330EventArgs ev) { } - - /// - /// Called after a interacts with SCP-330. - /// - /// The instance. - public virtual void OnInteractedScp330(PlayerInteractedScp330EventArgs ev) { } - - /// - /// Called before a interacts with a shooting target. - /// - /// The instance. - public virtual void OnInteractingShootingTarget(PlayerInteractingShootingTargetEventArgs ev) { } - - /// - /// Called after a interacts with a shooting target. - /// - /// The instance. - public virtual void OnInteractedShootingTarget(PlayerInteractedShootingTargetEventArgs ev) { } - - /// - /// Called before a triggers a tesla. - /// - /// The instance. - public virtual void OnTriggeringTesla(PlayerTriggeringTeslaEventArgs ev) { } - - /// - /// Called after a triggers a tesla. - /// - /// The instance. - public virtual void OnTriggeredTesla(PlayerTriggeredTeslaEventArgs ev) { } - - /// - /// Called before a receives a status effect. - /// - /// The instance. - [Obsolete("Not available on LabAPI, please refer to OnUpdatedEffect")] - public virtual void OnReceivingEffect(object ev) { } - - /// - /// Called before a receives a status effect. - /// - /// The instance. - public virtual void OnUpdatingEffect(PlayerEffectUpdatingEventArgs ev) { } - - /// - /// Called after a receives a status effect. - /// - /// The instance. - public virtual void OnUpdatedEffect(PlayerEffectUpdatedEventArgs ev) { } - - /// - /// Called before a user's radio battery charge is changed. - /// - /// The instance. - [Obsolete("Not available on LabAPI, please refer to OnUsingRadio")] - public virtual void OnUsingRadioBattery(object ev) { } - - /// - /// Called before a uses a Radio. - /// - /// The instance. - public virtual void OnUsingRadio(PlayerUsingRadioEventArgs ev) { } - - /// - /// Called before a MicroHID state is changed. - /// - /// The instance. - [Obsolete("Not available on LabAPI")] - public virtual void OnChangingMicroHIDState(object ev) { } - - /// - /// Called before a MicroHID energy is changed. - /// - /// The instance. - [Obsolete("Not available on LabAPI")] - public virtual void OnUsingMicroHIDEnergy(object ev) { } - - /// - /// Called before a damages a shooting target. - /// - /// The instance. - public virtual void OnDamagingShootingTarget(PlayerDamagingShootingTargetEventArgs ev) { } - - /// - /// Called after a damages a shooting target. - /// - /// The instance. - public virtual void OnDamagedShootingTarget(PlayerDamagedShootingTargetEventArgs ev) { } - - /// - /// Called before a flips a coin. - /// - /// The instance. - public virtual void OnFlippingCoin(PlayerFlippingCoinEventArgs ev) { } - - /// - /// Called after a flips a coin. - /// - /// The instance. - public virtual void OnFlippedCoin(PlayerFlippedCoinEventArgs ev) { } - - /// - /// Called before a toggles the flashlight. - /// - /// The instance. - public virtual void OnTogglingFlashlight(PlayerTogglingFlashlightEventArgs ev) { } - - /// - /// Called after a toggles the flashlight. - /// - /// The instance. - public virtual void OnToggledFlashlight(PlayerToggledFlashlightEventArgs ev) { } - - /// - /// Called before a unloads a weapon. - /// - /// The instance. - public virtual void OnUnloadingWeapon(PlayerUnloadingWeaponEventArgs ev) { } - - /// - /// Called before a unloads a weapon. - /// - /// The instance. - public virtual void OnUnloadedWeapon(PlayerUnloadedWeaponEventArgs ev) { } - - /// - /// Called after a triggers an aim action. - /// - /// The instance. - public virtual void OnAimingDownSight(PlayerAimedWeaponEventArgs ev) { } - - /// - /// Called before a toggles the weapon's flashlight. - /// - /// The instance. - public virtual void OnTogglingWeaponFlashlight(PlayerTogglingWeaponFlashlightEventArgs ev) { } - - /// - /// Called after a toggles the weapon's flashlight. - /// - /// The instance. - public virtual void OnToggledWeaponFlashlight(PlayerToggledWeaponFlashlightEventArgs ev) { } - - /// - /// Called before a dryfires a weapon. - /// - /// The instance. - public virtual void OnDryfiringWeapon(PlayerDryFiringWeaponEventArgs ev) { } - - /// - /// Called after a dryfires a weapon. - /// - /// The instance. - public virtual void OnDryfiredWeapon(PlayerDryFiredWeaponEventArgs ev) { } - - /// - /// Invoked after a presses the voicechat key. - /// - /// The instance. - [Obsolete("Not available on LabAPI, please refer to OnSendingVoiceMessage")] - public virtual void OnVoiceChatting(object ev) { } - - /// - /// Called before a sends a Voice Message. - /// - /// The instance. - public virtual void OnSendingVoiceMessage(PlayerSendingVoiceMessageEventArgs ev) { } - - /// - /// Called before a receives a Voice Message. - /// - /// The instance. - public virtual void OnReceivingVoiceMessage(PlayerReceivingVoiceMessageEventArgs ev) { } - - /// - /// Called before a makes noise. - /// - /// The instance. - [Obsolete("Not available on LabAPI")] - public virtual void OnMakingNoise(object ev) { } - - /// - /// Called after a lands. - /// - /// The instance. - [Obsolete("Not available on LabAPI")] - public virtual void OnLanding(object ev) { } - - /// - /// Called after a presses the transmission key. - /// - /// The instance. - [Obsolete("Not available on LabAPI, please refer to OnUsingRadio")] - public virtual void OnTransmitting(object ev) { } - - /// - /// Called before a changes move state. - /// - /// The instance. - public virtual void OnMovementStateChanged(PlayerMovementStateChangedEventArgs ev) { } - - /// - /// Called after a changes spectated player. - /// - /// The instance. - public virtual void OnChangedSpectator(PlayerChangedSpectatorEventArgs ev) { } - - /// - /// Called before a toggles the NoClip mode. - /// - /// The instance. - public virtual void OnTogglingNoClip(PlayerTogglingNoclipEventArgs ev) { } - - /// - /// Called after a toggles the NoClip mode. - /// - /// The instance. - public virtual void OnToggledNoClip(PlayerToggledNoclipEventArgs ev) { } - - /// - /// Called before a toggles overwatch. - /// - /// The instance. - [Obsolete("Not available on LabAPI")] - public virtual void OnTogglingOverwatch(object ev) { } - - /// - /// Called before turning the radio on/off. - /// - /// The instance. - public virtual void OnTogglingRadio(PlayerTogglingRadioEventArgs ev) { } - - /// - /// Called after turning the radio on/off. - /// - /// The instance. - public virtual void OnToggledRadio(PlayerToggledRadioEventArgs ev) { } - - /// - /// Called before a searches a Pickup. - /// - /// The instance. - public virtual void OnSearchPickupRequest(PlayerSearchingPickupEventArgs ev) { } - - /// - /// Called after a searches a Pickup. - /// - /// The instance. - public virtual void OnSearchedPickupRequest(PlayerSearchedPickupEventArgs ev) { } - - /// - /// Called before a sends a message inside the admin chat. - /// - /// The instance. - public virtual void OnSendingAdminChatMessage(SendingAdminChatEventArgs ev) { } - - /// - /// Called after a sent a message inside the admin chat. - /// - /// The instance. - public virtual void OnSentAdminChatMessage(SentAdminChatEventArgs ev) { } - - /// - /// Called after a has an item added to their inventory. - /// - /// The event handler. - public virtual void OnPickupCreated(PickupCreatedEventArgs ev) { } - - /// - /// Called after a has an item removed from their inventory. - /// - /// The event handler. - public virtual void OnPickupDestroyed(PickupDestroyedEventArgs ev) { } - - /// - /// Called before a enters in an environmental hazard. - /// - /// The instance. - public virtual void OnEnteringEnvironmentalHazard(PlayerEnteringHazardEventArgs ev) { } - - /// - /// Called after a enters in an environmental hazard. - /// - /// The instance. - public virtual void OnEnteredEnvironmentalHazard(PlayerEnteredHazardEventArgs ev) { } - - /// - /// Called when a stays on an environmental hazard. - /// - /// The instance. - public virtual void OnStayingOnEnvironmentalHazard(PlayersStayingInHazardEventArgs ev) { } - - /// - /// Called before a exits from an environmental hazard. - /// - /// The instance. - public virtual void OnExitingEnvironmentalHazard(PlayerLeavingHazardEventArgs ev) { } - - /// - /// Called after a exited from an environmental hazard. - /// - /// The instance. - public virtual void OnExitedEnvironmentalHazard(PlayerLeftHazardEventArgs ev) { } - - /// - /// Called before a damage a window. - /// - /// The instance. - public virtual void OnPlayerDamageWindow(PlayerDamagingWindowEventArgs ev) { } - - /// - /// Called before a unlocks a generator. - /// - /// The instance. - public virtual void OnUnlockingGenerator(PlayerUnlockingGeneratorEventArgs ev) { } - - /// - /// Called before a opens a generator. - /// - /// The instance. - public virtual void OnOpeningGenerator(PlayerOpeningGeneratorEventArgs ev) { } - - /// - /// Called before a closes a generator. - /// - /// The instance. - public virtual void OnClosingGenerator(PlayerClosingGeneratorEventArgs ev) { } - - /// - /// Called before a turns on the generator by switching lever. - /// - /// The instance. - public virtual void OnActivatingGenerator(PlayerActivatingGeneratorEventArgs ev) { } - - /// - /// Called before dropping ammo. - /// - /// The instance. - public virtual void OnDroppingAmmo(PlayerDroppingAmmoEventArgs ev) { } - - /// - /// Called after dropping ammo. - /// - /// The instance. - public virtual void OnDroppedAmmo(PlayerDroppedAmmoEventArgs ev) { } - - /// - /// Called before being muted. - /// - /// The instance. - public virtual void OnIssuingMute(PlayerMutingEventArgs ev) { } - - /// - /// Called after being muted. - /// - /// The instance. - public virtual void OnIssuedMute(PlayerMutedEventArgs ev) { } - - /// - /// Called before being unmuted. - /// - /// The instance. - public virtual void OnRevokingMute(PlayerUnmutingEventArgs ev) { } - - /// - /// Called after being unmuted. - /// - /// The instance. - public virtual void OnRevokedMute(PlayerUnmutedEventArgs ev) { } - - /// - /// Called before a user's radio preset is changed. - /// - /// The instance. - public virtual void OnChangingRadioPreset(PlayerChangingRadioRangeEventArgs ev) { } - - /// - /// Called before hurting a player. - /// - /// The instance. - public virtual void OnHurting(PlayerHurtingEventArgs ev) { } - - /// - /// Called ater a being hurt. - /// - /// The instance. - public virtual void OnHurt(PlayerHurtEventArgs ev) { } - - /// - /// Called before a is healed. - /// - /// The instance. - [Obsolete("Not available on LabAPI")] - public virtual void OnHealing(object ev) { } - - /// - /// Called after a is healed. - /// - /// The instance. - [Obsolete("Not available on LabAPI")] - public virtual void OnHealed(object ev) { } - - /// - /// Called before a dies. - /// - /// The instance. - public virtual void OnDying(PlayerDyingEventArgs ev) { } - - /// - /// Called before a s custom display name is changed. - /// - /// The instance. - public virtual void OnChangingNickname(PlayerChangingNicknameEventArgs ev) { } - - /// - /// Called after a s custom display name is changed. - /// - /// The instance. - public virtual void OnChangedNickname(PlayerChangedNicknameEventArgs ev) { } - - /// - /// Called when a jumps. - /// - /// The instance. - public virtual void OnPlayerJumped(PlayerJumpedEventArgs ev) { } - - /// - /// Called when a movement state changes. - /// - /// The instance. - public virtual void OnPlayerMovementStateChanged(PlayerMovementStateChangedEventArgs ev) { } - - /// - /// Called when a is changing attachments. - /// - /// The instance. - public virtual void OnPlayerChangingAttachments(PlayerChangingAttachmentsEventArgs ev) { } - - /// - /// Called when a has changed attachments. - /// - /// The instance. - public virtual void OnPlayerChangedAttachments(PlayerChangedAttachmentsEventArgs ev) { } - - /// - /// Called when a is sending attachments preferences. - /// - /// The instance. - public virtual void OnPlayerSendingAttachmentsPrefs(PlayerSendingAttachmentsPrefsEventArgs ev) { } - - /// - /// Called when a has sent attachments preferences. - /// - /// The instance. - public virtual void OnPlayerSentAttachmentsPrefs(PlayerSentAttachmentsPrefsEventArgs ev) { } - - /// - /// Called when the server elevator sequence changes. - /// - /// The instance. - public virtual void OnServerElevatorSequenceChanged(ElevatorSequenceChangedEventArgs ev) { } - - /// - /// Called when a interacts with a warhead lever. - /// - /// The instance. - public virtual void OnPlayerInteractingWarheadLever(PlayerInteractingWarheadLeverEventArgs ev) { } - - /// - /// Called when a has interacted with a warhead lever. - /// - /// The instance. - public virtual void OnPlayerInteractedWarheadLever(PlayerInteractedWarheadLeverEventArgs ev) { } - - /// - /// Gets called when detects enemy player using SCP-1344. - /// - /// The instance. - public virtual void OnDetectedByScp1344(PlayerDetectedByScp1344EventArgs ev) { } - - /// - /// Called when SCP-3114 is disguising. - /// - /// The instance. - public virtual void OnScp3114Disguising(Scp3114DisguisingEventArgs ev) { } - - /// - /// Called when SCP-3114 has disguised. - /// - /// The instance. - public virtual void OnScp3114Disguised(Scp3114DisguisedEventArgs ev) { } - - /// - /// Called when SCP-3114 is revealing. - /// - /// The instance. - public virtual void OnScp3114Revealing(Scp3114RevealingEventArgs ev) { } - - /// - /// Called when SCP-3114 has revealed. - /// - /// The instance. - public virtual void OnScp3114Revealed(Scp3114RevealedEventArgs ev) { } - - /// - /// Called when SCP-3114 starts dancing. - /// - /// The instance. - public virtual void OnScp3114StartingDancing(Scp3114StartingDanceEventArgs ev) { } - - /// - /// Called when SCP-3114 has started dancing. - /// - /// The instance. - public virtual void OnScp3114StartedDancing(Scp3114StartedDanceEventArgs ev) { } - - /// - /// Called when a is spinning a revolver. - /// - /// The instance. - public virtual void OnPlayerSpinningRevolver(PlayerSpinningRevolverEventArgs ev) { } - - /// - /// Called when a has spun a revolver. - /// - /// The instance. - public virtual void OnPlayerSpunRevolver(PlayerSpinnedRevolverEventArgs ev) { } - - /// - /// Called when a toggles disruptor mode. - /// - /// The instance. - public virtual void OnPlayerToggledDisruptorFiringMode(PlayerToggledDisruptorFiringModeEventArgs ev) { } - - /// - /// Called when SCP-127 gains experience. - /// - /// The instance. - public virtual void OnGainingExp(Scp127GainExperienceEventArgs ev) { } - - /// - /// Called when SCP-127 has gained experience. - /// - /// The instance. - public virtual void OnGainedExp(Scp127GainExperienceEventArgs ev) { } - - /// - /// Called when SCP-127 is levelling up. - /// - /// The instance. - public virtual void OnLevellingUp(Scp127LevellingUpEventArgs ev) { } - - /// - /// Called when SCP-127 has levelled up. - /// - /// The instance. - public virtual void OnLevelUp(Scp127LevelUpEventArgs ev) { } - - /// - /// Called when SCP-127 is talking. - /// - /// The instance. - public virtual void OnTalking(Scp127TalkingEventArgs ev) { } - - /// - /// Called when SCP-127 has talked. - /// - /// The instance. - public virtual void OnTalked(Scp127TalkedEventArgs ev) { } - - /// - /// Called when a badge visibility is changing. - /// - /// The instance. - public virtual void OnChangingBadgeVisibility(PlayerChangingBadgeVisibilityEventArgs ev) { } - - /// - /// Called when a badge visibility has changed. - /// - /// The instance. - public virtual void OnChangedBadgeVisibility(PlayerChangedBadgeVisibilityEventArgs ev) { } - - /// - /// Called when a is processing a Jailbird message. - /// - /// The instance. - public virtual void OnProcessingJailbirdMessage(PlayerProcessingJailbirdMessageEventArgs ev) { } - - /// - /// Called when a has processed a Jailbird message. - /// - /// The instance. - public virtual void OnProcessedJailbirdMessage(PlayerProcessedJailbirdMessageEventArgs ev) { } - - /// - /// Called when a is completing item use. - /// - /// The instance. - public virtual void OnUsingItemCompleting(PlayerUsingItemEventArgs ev) { } - - /// - /// Called when a is completing item use. - /// - /// The instance. - public virtual void OnUsedItemCompleting(PlayerUsedItemEventArgs ev) { } - - /// - /// Called when SCP-3114 strangle is aborting. - /// - /// The instance. - public virtual void OnStrangleAborting(Scp3114StrangleAbortingEventArgs ev) { } - - /// - /// Called when SCP-3114 strangle has aborted. - /// - /// The instance. - public virtual void OnStrangleAborted(Scp3114StrangleAbortedEventArgs ev) { } - - /// - /// Called when SCP-3114 strangle is starting. - /// - /// The instance. - public virtual void OnStrangleStarting(Scp3114StrangleStartingEventArgs ev) { } - - /// - /// Called when SCP-3114 strangle has started. - /// - /// The instance. - public virtual void OnStrangleStarted(Scp3114StrangleStartedEventArgs ev) { } - - /// - /// Called when a is inspecting a keycard. - /// - /// The instance. - public virtual void OnInspectingKeycard(PlayerInspectingKeycardEventArgs ev) { } - - /// - /// Called when a has inspected a keycard. - /// - /// The instance. - public virtual void OnInspectedKeycard(PlayerInspectedKeycardEventArgs ev) { } - - /// - /// Called when a ' room has changed. - /// - /// The instance. - public virtual void OnRoomChanged(PlayerRoomChangedEventArgs ev) { } - - /// - /// Called when a ' zone has changed. - /// - /// The instance. - public virtual void OnZoneChanged(PlayerZoneChangedEventArgs ev) { } - - /// - /// Called when a is added to the RA player list. - /// - /// The instance. - public virtual void OnRaPlayerListAddedPlayer(PlayerRaPlayerListAddedPlayerEventArgs ev) { } - - /// - /// Called when a is being added to the RA player list. - /// - /// The instance. - public virtual void OnRaPlayerListAddingPlayer(PlayerRaPlayerListAddingPlayerEventArgs ev) { } - - /// - /// Called when a requests custom RA info. - /// - /// The instance. - public virtual void OnRequestedCustomRaInfo(PlayerRequestedCustomRaInfoEventArgs ev) { } - - /// - /// Called when a requests RA player info. - /// - /// The instance. - public virtual void OnRequestedRaPlayerInfo(PlayerRequestedRaPlayerInfoEventArgs ev) { } - - /// - /// Called when a is requesting RA player info. - /// - /// The instance. - public virtual void OnRequestingRaPlayerInfo(PlayerRequestingRaPlayerInfoEventArgs ev) { } - - /// - /// Called when a requests the RA player list. - /// - /// The instance. - public virtual void OnRequestedRaPlayerList(PlayerRequestedRaPlayerListEventArgs ev) { } - - /// - /// Called when a is requesting the RA player list. - /// - /// The instance. - public virtual void OnRequestingRaPlayerList(PlayerRequestingRaPlayerListEventArgs ev) { } - - /// - /// Called when a requests RA players info. - /// - /// The instance. - public virtual void OnRequestedRaPlayersInfo(PlayerRequestedRaPlayersInfoEventArgs ev) { } - - /// - /// Called when a is requesting RA players info. - /// - /// The instance. - public virtual void OnRequestingRaPlayersInfo(PlayerRequestingRaPlayersInfoEventArgs ev) { } - - /// - /// Called when an objective is completing. - /// - /// The instance. - public virtual void OnCompleting(ObjectiveCompletingBaseEventArgs ev) { } - - /// - /// Called when an objective is completed. - /// - /// The instance. - public virtual void OnCompleted(ObjectiveCompletedBaseEventArgs ev) { } - - /// - /// Called when activating generator objective is completing. - /// - /// The instance. - public virtual void OnActivatingGeneratorCompleting(GeneratorActivatingEventArgs ev) { } - - /// - /// Called when activating generator objective is completed. - /// - /// The instance. - public virtual void OnActivatedGeneratorCompleted(GeneratorActivatedEventArgs ev) { } - - /// - /// Called when damaging SCP objective is completing. - /// - /// The instance. - public virtual void OnDamagingScpCompleting(ScpDamagingObjectiveEventArgs ev) { } - - /// - /// Called when damaging SCP objective is completed. - /// - /// The instance. - public virtual void OnDamagedScpCompleted(ScpDamagedObjectiveEventArgs ev) { } - - /// - /// Called when escaping objective is completing. - /// - /// The instance. - public virtual void OnEscapingCompleting(EscapingObjectiveEventArgs ev) { } - - /// - /// Called when escaping objective is completed. - /// - /// The instance. - public virtual void OnEscapedCompleted(EscapedObjectiveEventArgs ev) { } - - /// - /// Called when killing enemy objective is completing. - /// - /// The instance. - public virtual void OnKillingEnemyCompleting(EnemyKillingObjectiveEventArgs ev) { } - - /// - /// Called when killing enemy objective is completed. - /// - /// The instance. - public virtual void OnKilledEnemyCompleted(EnemyKilledObjectiveEventArgs ev) { } - - /// - /// Called when picking SCP item objective is completing. - /// - /// The instance. - public virtual void OnPickingScpItemCompleting(ScpItemPickingObjectiveEventArgs ev) { } - - /// - /// Called when picking SCP item objective is completed. - /// - /// The instance. - public virtual void OnPickedScpItemCompleted(ScpItemPickedObjectiveEventArgs ev) { } - } -} + ItemType.Ammo9x19, + 10 + } + }; + + /// + /// Gets or sets the damage multiplier.

+ /// This will increase - keep normal - or decrease the damage that this role will do + ///
+ public virtual float DamageMultiplier { get; set; } = 1; + + /// + /// Gets or sets the + /// + public virtual SpawnBehaviour? SpawnSettings { get; set; } = new(); + + /// + /// Gets or sets the of the custom role + /// + public virtual List? CustomFlags { get; set; } = null; + + /// + /// Gets or sets whether the custom role should be evaluated during normal spawn events or not + /// + public virtual bool IgnoreSpawnSystem { get; set; } = false; + + public override string ToString() + { + return $"{Regex.Replace(Name, "(.*?)", "$1")} ({Id})"; + } + + /// + /// Invoked when the Custom Role is spawned + /// + /// + public virtual void OnSpawned(SummonedCustomRole role) + { + } + + /// + /// Invoked when the Custom Role is spawned + /// + /// + public virtual void OnRemoved(SummonedCustomRole role) + { + } + + /// + /// Called before kicking a from the server. + /// + /// The instance. + public virtual void OnKicking(PlayerKickingEventArgs ev) + { + } + + /// + /// Called after a has been kicked from the server. + /// + /// The instance. + public virtual void OnKicked(PlayerKickedEventArgs ev) + { + } + + /// + /// Called before banning a from the server. + /// + /// The instance. + public virtual void OnBanning(PlayerBanningEventArgs ev) + { + } + + /// + /// Called before a danger state changes. + /// + /// The instance. + [Obsolete("Not available on LabAPI")] + public virtual void OnChangingDangerState(object ev) + { + } + + /// + /// Called after a player has been banned from the server. + /// + /// The instance. + public virtual void OnBanned(PlayerBannedEventArgs ev) + { + } + + /// + /// Called before a earns an achievement. + /// + /// The instance. + public virtual void OnReceivedAchievement(PlayerReceivedAchievementEventArgs ev) + { + } + + /// + /// Called before using a usable item. + /// + /// The instance. + public virtual void OnUsingItem(PlayerUsingItemEventArgs ev) + { + } + + /// + /// Called before completed using of a usable item. + /// + /// The instance. + [Obsolete("Only works on EXILED due to the need of a patch, please refer to OnUsedItem")] + public virtual void OnUsingItemCompleted(object ev) + { + } + + /// + /// Called after a used a + /// item. + /// + /// The instance. + public virtual void OnUsedItem(PlayerUsedItemEventArgs ev) + { + } + + /// + /// Called before a has stopped the use of a + /// item. + /// + /// The instance. + public virtual void OnCancellingItemUse(PlayerCancellingUsingItemEventArgs ev) + { + } + + /// + /// Called after a has stopped the use of a + /// item. + /// + /// The instance. + public virtual void OnCancelledItemUse(PlayerCancelledUsingItemEventArgs ev) + { + } + + /// + /// Called after a interacted with something. + /// + /// The instance. + [Obsolete( + "The generic interaction event is not available in LabAPI, please handle every interaction in a separate method.")] + public virtual void OnInteracted(object ev) + { + } + + /// + /// Called before spawning a ragdoll. + /// + /// The instance. + public virtual void OnSpawningRagdoll(PlayerSpawningRagdollEventArgs ev) + { + } + + /// + /// Called after spawning a ragdoll. + /// + /// The instance. + public virtual void OnSpawnedRagdoll(PlayerSpawnedRagdollEventArgs ev) + { + } + + /// + /// Called before activating the warhead panel. + /// + /// The instance. + [Obsolete("Not available on LabAPI")] + public virtual void OnActivatingWarheadPanel(object ev) + { + } + + /// + /// Called before activating a workstation. + /// + /// The instance. + [Obsolete("Not available on LabAPI")] + public virtual void OnActivatingWorkstation(object ev) + { + } + + /// + /// Called before deactivating a workstation. + /// + /// The instance. + [Obsolete("Not available on LabAPI")] + public virtual void OnDeactivatingWorkstation(object ev) + { + } + + /// + /// Called after a has left the server. + /// + /// The instance. + public virtual void OnLeft(PlayerLeftEventArgs ev) + { + } + + /// + /// Called after a died. + /// + /// The instance. + public virtual void OnDied(PlayerDeathEventArgs ev) + { + } + + /// + /// Called before changing a role. + /// + /// The instance. + /// + /// If is set to when Escape is + /// , awards will still be given to the escapee's team even though they will 'fail' to escape. + /// Use to block escapes instead. + /// + public virtual void OnChangingRole(PlayerChangingRoleEventArgs ev) + { + } + + /// + /// Called before throwing a grenade. + /// + /// The instance. + public virtual void OnThrowingProjectile(PlayerThrowingProjectileEventArgs ev) + { + } + + /// + /// Called after threw a grenade. + /// + /// The instance. + public virtual void OnThrewProjectile(PlayerThrewProjectileEventArgs ev) + { + } + + /// + /// Called before receving a throwing request. + /// + /// The instance. + [Obsolete("Please refer to OnThrowingItem")] + public virtual void OnThrowingRequest(object ev) + { + } + + /// + /// Called before a throws an item. + /// + /// The instance. + public virtual void OnThrowingItem(PlayerThrowingItemEventArgs ev) + { + } + + /// + /// Called after threw an item. + /// + /// The instance. + public virtual void OnThrewItem(PlayerThrewItemEventArgs ev) + { + } + + /// + /// Called before dropping an item. + /// + /// The instance. + public virtual void OnDroppingItem(PlayerDroppingItemEventArgs ev) + { + } + + /// + /// Called after dropping an item. + /// + /// The instance. + public virtual void OnDroppedItem(PlayerDroppedItemEventArgs ev) + { + } + + /// + /// Called before dropping a null item. + /// + /// The instance. + [Obsolete("Not available on LabAPI")] + public virtual void OnDroppingNothing(object ev) + { + } + + /// + /// Called before a picks up an item. + /// + /// The instance. + public virtual void OnPickingUpItem(PlayerPickingUpItemEventArgs ev) + { + } + + /// + /// Called before handcuffing a . + /// + /// The instance. + public virtual void OnHandcuffing(PlayerCuffingEventArgs ev) + { + } + + /// + /// Called after handcuffing a . + /// + /// The instance. + public virtual void OnHandcuffed(PlayerCuffedEventArgs ev) + { + } + + /// + /// Called before freeing a handcuffed . + /// + /// The instance. + public virtual void OnRemovingHandcuffs(PlayerUncuffingEventArgs ev) + { + } + + /// + /// Called after freeing a handcuffed . + /// + /// The instance. + public virtual void OnRemovedHandcuffs(PlayerUncuffedEventArgs ev) + { + } + + /// + /// Called before a escapes. + /// + /// The instance. + public virtual void OnEscaping(PlayerEscapingEventArgs ev) + { + } + + /// + /// Called before a escapes. + /// + /// The instance. + public virtual void OnEscaped(PlayerEscapedEventArgs ev) + { + } + + /// + /// Called before a begins speaking in the intercom. + /// + /// The instance. + public virtual void OnIntercomSpeaking(PlayerUsingIntercomEventArgs ev) + { + } + + /// + /// Called after a finished speaking in the intercom. + /// + /// The instance. + public virtual void OnIntercomSpeakingFinished(PlayerUsedIntercomEventArgs ev) + { + } + + /// + /// Called after a shoots a weapon. + /// + /// The instance. + public virtual void OnShot(PlayerShotWeaponEventArgs ev) + { + } + + /// + /// Called before a shoots a weapon. + /// + /// The instance. + public virtual void OnShooting(PlayerShootingWeaponEventArgs ev) + { + } + + /// + /// Called before a enters the pocket dimension. + /// + /// The instance. + public virtual void OnEnteringPocketDimension(PlayerEnteringPocketDimensionEventArgs ev) + { + } + + /// + /// Called after a enters the pocket dimension. + /// + /// The instance. + public virtual void OnEnteredPocketDimension(PlayerEnteredPocketDimensionEventArgs ev) + { + } + + /// + /// Called before a leaves the pocket dimension. + /// + /// The instance. + [Obsolete("Not available on LabAPI, please see OnLeftPocketDimension")] + public virtual void OnEscapingPocketDimension(object ev) + { + } + + /// + /// Called before a leaves the pocket dimension. + /// + /// The instance. + public virtual void OnLeavingPocketDimension(PlayerLeavingPocketDimensionEventArgs ev) + { + } + + /// + /// Called before a fails to escape the pocket dimension. + /// + /// The instance. + [Obsolete("Not available on LabAPI, please see OnLeftPocketDimension")] + public virtual void OnFailingEscapePocketDimension(object ev) + { + } + + /// + /// Called after a left the pocket dimension. + /// + /// The instance. + public virtual void OnLeftPocketDimension(PlayerLeftPocketDimensionEventArgs ev) + { + } + + /// + /// Called before a enters killer collision. + /// + /// The instance. + [Obsolete("Not available on LabAPI")] + public virtual void OnEnteringKillerCollision(object ev) + { + } + + /// + /// Called before a reloads a weapon. + /// + /// The instance. + public virtual void OnReloadingWeapon(PlayerReloadingWeaponEventArgs ev) + { + } + + /// + /// Called after a held item changes. + /// + /// The instance. + public virtual void OnChangedItem(PlayerChangedItemEventArgs ev) + { + } + + /// + /// Called before a held item changes. + /// + /// The instance. + public virtual void OnChangingItem(PlayerChangingItemEventArgs ev) + { + } + + /// + /// Called before changing a group. + /// + /// The instance. + public virtual void OnChangingGroup(PlayerGroupChangingEventArgs ev) + { + } + + /// + /// Called after changing a group. + /// + /// The instance. + public virtual void OnChangedGroup(PlayerGroupChangedEventArgs ev) + { + } + + /// + /// Called before a interacts with an elevator. + /// + /// The instance. + public virtual void OnInteractingElevator(PlayerInteractingElevatorEventArgs ev) + { + } + + /// + /// Called after a interacts with an elevator. + /// + /// The instance. + public virtual void OnInteractedElevator(PlayerInteractedElevatorEventArgs ev) + { + } + + /// + /// Called before a interacts with a locker. + /// + /// The instance. + public virtual void OnInteractingLocker(PlayerInteractingLockerEventArgs ev) + { + } + + /// + /// Called after a interacts with a locker. + /// + /// The instance. + public virtual void OnInteractedLocker(PlayerInteractedLockerEventArgs ev) + { + } + + /// + /// Called before a interacts with a generator. + /// + /// The instance. + public virtual void OnInteractingGenerator(PlayerInteractingGeneratorEventArgs ev) + { + } + + /// + /// Called after a interacts with a generator. + /// + /// The instance. + public virtual void OnInteractedGenerator(PlayerInteractedGeneratorEventArgs ev) + { + } + + /// + /// Called before a interacts with a door. + /// + /// The instance. + public virtual void OnInteractingDoor(PlayerInteractingDoorEventArgs ev) + { + } + + /// + /// Called after a interacts with a door. + /// + /// The instance. + public virtual void OnInteractedDoor(PlayerInteractedDoorEventArgs ev) + { + } + + /// + /// Called before a interacts with SCP-330. + /// + /// The instance. + public virtual void OnInteractingScp330(PlayerInteractingScp330EventArgs ev) + { + } + + /// + /// Called after a interacts with SCP-330. + /// + /// The instance. + public virtual void OnInteractedScp330(PlayerInteractedScp330EventArgs ev) + { + } + + /// + /// Called before a interacts with a shooting target. + /// + /// The instance. + public virtual void OnInteractingShootingTarget(PlayerInteractingShootingTargetEventArgs ev) + { + } + + /// + /// Called after a interacts with a shooting target. + /// + /// The instance. + public virtual void OnInteractedShootingTarget(PlayerInteractedShootingTargetEventArgs ev) + { + } + + /// + /// Called before a triggers a tesla. + /// + /// The instance. + public virtual void OnTriggeringTesla(PlayerTriggeringTeslaEventArgs ev) + { + } + + /// + /// Called after a triggers a tesla. + /// + /// The instance. + public virtual void OnTriggeredTesla(PlayerTriggeredTeslaEventArgs ev) + { + } + + /// + /// Called before a receives a status effect. + /// + /// The instance. + [Obsolete("Not available on LabAPI, please refer to OnUpdatedEffect")] + public virtual void OnReceivingEffect(object ev) + { + } + + /// + /// Called before a receives a status effect. + /// + /// The instance. + public virtual void OnUpdatingEffect(PlayerEffectUpdatingEventArgs ev) + { + } + + /// + /// Called after a receives a status effect. + /// + /// The instance. + public virtual void OnUpdatedEffect(PlayerEffectUpdatedEventArgs ev) + { + } + + /// + /// Called before a user's radio battery charge is changed. + /// + /// The instance. + [Obsolete("Not available on LabAPI, please refer to OnUsingRadio")] + public virtual void OnUsingRadioBattery(object ev) + { + } + + /// + /// Called before a uses a Radio. + /// + /// The instance. + public virtual void OnUsingRadio(PlayerUsingRadioEventArgs ev) + { + } + + /// + /// Called before a MicroHID state is changed. + /// + /// The instance. + [Obsolete("Not available on LabAPI")] + public virtual void OnChangingMicroHIDState(object ev) + { + } + + /// + /// Called before a MicroHID energy is changed. + /// + /// The instance. + [Obsolete("Not available on LabAPI")] + public virtual void OnUsingMicroHIDEnergy(object ev) + { + } + + /// + /// Called before a damages a shooting target. + /// + /// The instance. + public virtual void OnDamagingShootingTarget(PlayerDamagingShootingTargetEventArgs ev) + { + } + + /// + /// Called after a damages a shooting target. + /// + /// The instance. + public virtual void OnDamagedShootingTarget(PlayerDamagedShootingTargetEventArgs ev) + { + } + + /// + /// Called before a flips a coin. + /// + /// The instance. + public virtual void OnFlippingCoin(PlayerFlippingCoinEventArgs ev) + { + } + + /// + /// Called after a flips a coin. + /// + /// The instance. + public virtual void OnFlippedCoin(PlayerFlippedCoinEventArgs ev) + { + } + + /// + /// Called before a toggles the flashlight. + /// + /// The instance. + public virtual void OnTogglingFlashlight(PlayerTogglingFlashlightEventArgs ev) + { + } + + /// + /// Called after a toggles the flashlight. + /// + /// The instance. + public virtual void OnToggledFlashlight(PlayerToggledFlashlightEventArgs ev) + { + } + + /// + /// Called before a unloads a weapon. + /// + /// The instance. + public virtual void OnUnloadingWeapon(PlayerUnloadingWeaponEventArgs ev) + { + } + /// + /// Called before a unloads a weapon. + /// + /// The instance. + public virtual void OnUnloadedWeapon(PlayerUnloadedWeaponEventArgs ev) + { + } + + /// + /// Called after a triggers an aim action. + /// + /// The instance. + public virtual void OnAimingDownSight(PlayerAimedWeaponEventArgs ev) + { + } + + /// + /// Called before a toggles the weapon's flashlight. + /// + /// The instance. + public virtual void OnTogglingWeaponFlashlight(PlayerTogglingWeaponFlashlightEventArgs ev) + { + } + + /// + /// Called after a toggles the weapon's flashlight. + /// + /// The instance. + public virtual void OnToggledWeaponFlashlight(PlayerToggledWeaponFlashlightEventArgs ev) + { + } + + /// + /// Called before a dryfires a weapon. + /// + /// The instance. + public virtual void OnDryfiringWeapon(PlayerDryFiringWeaponEventArgs ev) + { + } + + /// + /// Called after a dryfires a weapon. + /// + /// The instance. + public virtual void OnDryfiredWeapon(PlayerDryFiredWeaponEventArgs ev) + { + } + + /// + /// Invoked after a presses the voicechat key. + /// + /// The instance. + [Obsolete("Not available on LabAPI, please refer to OnSendingVoiceMessage")] + public virtual void OnVoiceChatting(object ev) + { + } + + /// + /// Called before a sends a Voice Message. + /// + /// The instance. + public virtual void OnSendingVoiceMessage(PlayerSendingVoiceMessageEventArgs ev) + { + } + + /// + /// Called before a receives a Voice Message. + /// + /// The instance. + public virtual void OnReceivingVoiceMessage(PlayerReceivingVoiceMessageEventArgs ev) + { + } + + /// + /// Called before a makes noise. + /// + /// The instance. + [Obsolete("Not available on LabAPI")] + public virtual void OnMakingNoise(object ev) + { + } + + /// + /// Called after a lands. + /// + /// The instance. + [Obsolete("Not available on LabAPI")] + public virtual void OnLanding(object ev) + { + } + + /// + /// Called after a presses the transmission key. + /// + /// The instance. + [Obsolete("Not available on LabAPI, please refer to OnUsingRadio")] + public virtual void OnTransmitting(object ev) + { + } + + /// + /// Called before a changes move state. + /// + /// The instance. + public virtual void OnMovementStateChanged(PlayerMovementStateChangedEventArgs ev) + { + } + + /// + /// Called after a changes spectated player. + /// + /// The instance. + public virtual void OnChangedSpectator(PlayerChangedSpectatorEventArgs ev) + { + } + + /// + /// Called before a toggles the NoClip mode. + /// + /// The instance. + public virtual void OnTogglingNoClip(PlayerTogglingNoclipEventArgs ev) + { + } + + /// + /// Called after a toggles the NoClip mode. + /// + /// The instance. + public virtual void OnToggledNoClip(PlayerToggledNoclipEventArgs ev) + { + } + + /// + /// Called before a toggles overwatch. + /// + /// The instance. + [Obsolete("Not available on LabAPI")] + public virtual void OnTogglingOverwatch(object ev) + { + } + + /// + /// Called before turning the radio on/off. + /// + /// The instance. + public virtual void OnTogglingRadio(PlayerTogglingRadioEventArgs ev) + { + } + + /// + /// Called after turning the radio on/off. + /// + /// The instance. + public virtual void OnToggledRadio(PlayerToggledRadioEventArgs ev) + { + } + + /// + /// Called before a searches a Pickup. + /// + /// The instance. + public virtual void OnSearchPickupRequest(PlayerSearchingPickupEventArgs ev) + { + } + + /// + /// Called after a searches a Pickup. + /// + /// The instance. + public virtual void OnSearchedPickupRequest(PlayerSearchedPickupEventArgs ev) + { + } + + /// + /// Called before a sends a message inside the admin chat. + /// + /// The instance. + public virtual void OnSendingAdminChatMessage(SendingAdminChatEventArgs ev) + { + } + + /// + /// Called after a sent a message inside the admin chat. + /// + /// The instance. + public virtual void OnSentAdminChatMessage(SentAdminChatEventArgs ev) + { + } + + /// + /// Called after a has an item added to their inventory. + /// + /// The event handler. + public virtual void OnPickupCreated(PickupCreatedEventArgs ev) + { + } + + /// + /// Called after a has an item removed from their inventory. + /// + /// The event handler. + public virtual void OnPickupDestroyed(PickupDestroyedEventArgs ev) + { + } + + /// + /// Called before a enters in an environmental hazard. + /// + /// The instance. + public virtual void OnEnteringEnvironmentalHazard(PlayerEnteringHazardEventArgs ev) + { + } + + /// + /// Called after a enters in an environmental hazard. + /// + /// The instance. + public virtual void OnEnteredEnvironmentalHazard(PlayerEnteredHazardEventArgs ev) + { + } + + /// + /// Called when a stays on an environmental hazard. + /// + /// The instance. + public virtual void OnStayingOnEnvironmentalHazard(PlayersStayingInHazardEventArgs ev) + { + } + + /// + /// Called before a exits from an environmental hazard. + /// + /// The instance. + public virtual void OnExitingEnvironmentalHazard(PlayerLeavingHazardEventArgs ev) + { + } + + /// + /// Called after a exited from an environmental hazard. + /// + /// The instance. + public virtual void OnExitedEnvironmentalHazard(PlayerLeftHazardEventArgs ev) + { + } + + /// + /// Called before a damage a window. + /// + /// The instance. + public virtual void OnPlayerDamageWindow(PlayerDamagingWindowEventArgs ev) + { + } + + /// + /// Called before a unlocks a generator. + /// + /// The instance. + public virtual void OnUnlockingGenerator(PlayerUnlockingGeneratorEventArgs ev) + { + } + + /// + /// Called before a opens a generator. + /// + /// The instance. + public virtual void OnOpeningGenerator(PlayerOpeningGeneratorEventArgs ev) + { + } + + /// + /// Called before a closes a generator. + /// + /// The instance. + public virtual void OnClosingGenerator(PlayerClosingGeneratorEventArgs ev) + { + } + + /// + /// Called before a turns on the generator by switching lever. + /// + /// The instance. + public virtual void OnActivatingGenerator(PlayerActivatingGeneratorEventArgs ev) + { + } + + /// + /// Called before dropping ammo. + /// + /// The instance. + public virtual void OnDroppingAmmo(PlayerDroppingAmmoEventArgs ev) + { + } + + /// + /// Called after dropping ammo. + /// + /// The instance. + public virtual void OnDroppedAmmo(PlayerDroppedAmmoEventArgs ev) + { + } + + /// + /// Called before being muted. + /// + /// The instance. + public virtual void OnIssuingMute(PlayerMutingEventArgs ev) + { + } + + /// + /// Called after being muted. + /// + /// The instance. + public virtual void OnIssuedMute(PlayerMutedEventArgs ev) + { + } + + /// + /// Called before being unmuted. + /// + /// The instance. + public virtual void OnRevokingMute(PlayerUnmutingEventArgs ev) + { + } + + /// + /// Called after being unmuted. + /// + /// The instance. + public virtual void OnRevokedMute(PlayerUnmutedEventArgs ev) + { + } + + /// + /// Called before a user's radio preset is changed. + /// + /// The instance. + public virtual void OnChangingRadioPreset(PlayerChangingRadioRangeEventArgs ev) + { + } + + /// + /// Called before hurting a player. + /// + /// The instance. + public virtual void OnHurting(PlayerHurtingEventArgs ev) + { + } + + /// + /// Called ater a being hurt. + /// + /// The instance. + public virtual void OnHurt(PlayerHurtEventArgs ev) + { + } + + /// + /// Called before a is healed. + /// + /// The instance. + [Obsolete("Not available on LabAPI")] + public virtual void OnHealing(object ev) + { + } + + /// + /// Called after a is healed. + /// + /// The instance. + [Obsolete("Not available on LabAPI")] + public virtual void OnHealed(object ev) + { + } + + /// + /// Called before a dies. + /// + /// The instance. + public virtual void OnDying(PlayerDyingEventArgs ev) + { + } + + /// + /// Called before a s custom display name is changed. + /// + /// The instance. + public virtual void OnChangingNickname(PlayerChangingNicknameEventArgs ev) + { + } + + /// + /// Called after a s custom display name is changed. + /// + /// The instance. + public virtual void OnChangedNickname(PlayerChangedNicknameEventArgs ev) + { + } + + /// + /// Called when a jumps. + /// + /// The instance. + public virtual void OnPlayerJumped(PlayerJumpedEventArgs ev) + { + } + + /// + /// Called when a movement state changes. + /// + /// The instance. + public virtual void OnPlayerMovementStateChanged(PlayerMovementStateChangedEventArgs ev) + { + } + + /// + /// Called when a is changing attachments. + /// + /// The instance. + public virtual void OnPlayerChangingAttachments(PlayerChangingAttachmentsEventArgs ev) + { + } + + /// + /// Called when a has changed attachments. + /// + /// The instance. + public virtual void OnPlayerChangedAttachments(PlayerChangedAttachmentsEventArgs ev) + { + } + + /// + /// Called when a is sending attachments preferences. + /// + /// The instance. + public virtual void OnPlayerSendingAttachmentsPrefs(PlayerSendingAttachmentsPrefsEventArgs ev) + { + } + + /// + /// Called when a has sent attachments preferences. + /// + /// The instance. + public virtual void OnPlayerSentAttachmentsPrefs(PlayerSentAttachmentsPrefsEventArgs ev) + { + } + + /// + /// Called when the server elevator sequence changes. + /// + /// The instance. + public virtual void OnServerElevatorSequenceChanged(ElevatorSequenceChangedEventArgs ev) + { + } + + /// + /// Called when a interacts with a warhead lever. + /// + /// The instance. + public virtual void OnPlayerInteractingWarheadLever(PlayerInteractingWarheadLeverEventArgs ev) + { + } + + /// + /// Called when a has interacted with a warhead lever. + /// + /// The instance. + public virtual void OnPlayerInteractedWarheadLever(PlayerInteractedWarheadLeverEventArgs ev) + { + } + + /// + /// Gets called when detects enemy player using SCP-1344. + /// + /// The instance. + public virtual void OnDetectedByScp1344(PlayerDetectedByScp1344EventArgs ev) + { + } + + /// + /// Called when SCP-3114 is disguising. + /// + /// The instance. + public virtual void OnScp3114Disguising(Scp3114DisguisingEventArgs ev) + { + } + + /// + /// Called when SCP-3114 has disguised. + /// + /// The instance. + public virtual void OnScp3114Disguised(Scp3114DisguisedEventArgs ev) + { + } + + /// + /// Called when SCP-3114 is revealing. + /// + /// The instance. + public virtual void OnScp3114Revealing(Scp3114RevealingEventArgs ev) + { + } + + /// + /// Called when SCP-3114 has revealed. + /// + /// The instance. + public virtual void OnScp3114Revealed(Scp3114RevealedEventArgs ev) + { + } + + /// + /// Called when SCP-3114 starts dancing. + /// + /// The instance. + public virtual void OnScp3114StartingDancing(Scp3114StartingDanceEventArgs ev) + { + } + + /// + /// Called when SCP-3114 has started dancing. + /// + /// The instance. + public virtual void OnScp3114StartedDancing(Scp3114StartedDanceEventArgs ev) + { + } + + /// + /// Called when a is spinning a revolver. + /// + /// The instance. + public virtual void OnPlayerSpinningRevolver(PlayerSpinningRevolverEventArgs ev) + { + } + + /// + /// Called when a has spun a revolver. + /// + /// The instance. + public virtual void OnPlayerSpunRevolver(PlayerSpinnedRevolverEventArgs ev) + { + } + + /// + /// Called when a toggles disruptor mode. + /// + /// The instance. + public virtual void OnPlayerToggledDisruptorFiringMode(PlayerToggledDisruptorFiringModeEventArgs ev) + { + } + + /// + /// Called when SCP-127 gains experience. + /// + /// The instance. + public virtual void OnGainingExp(Scp127GainExperienceEventArgs ev) + { + } + + /// + /// Called when SCP-127 has gained experience. + /// + /// The instance. + public virtual void OnGainedExp(Scp127GainExperienceEventArgs ev) + { + } + + /// + /// Called when SCP-127 is levelling up. + /// + /// The instance. + public virtual void OnLevellingUp(Scp127LevellingUpEventArgs ev) + { + } + + /// + /// Called when SCP-127 has levelled up. + /// + /// The instance. + public virtual void OnLevelUp(Scp127LevelUpEventArgs ev) + { + } + + /// + /// Called when SCP-127 is talking. + /// + /// The instance. + public virtual void OnTalking(Scp127TalkingEventArgs ev) + { + } + + /// + /// Called when SCP-127 has talked. + /// + /// The instance. + public virtual void OnTalked(Scp127TalkedEventArgs ev) + { + } + + /// + /// Called when a badge visibility is changing. + /// + /// The instance. + public virtual void OnChangingBadgeVisibility(PlayerChangingBadgeVisibilityEventArgs ev) + { + } + + /// + /// Called when a badge visibility has changed. + /// + /// The instance. + public virtual void OnChangedBadgeVisibility(PlayerChangedBadgeVisibilityEventArgs ev) + { + } + + /// + /// Called when a is processing a Jailbird message. + /// + /// The instance. + public virtual void OnProcessingJailbirdMessage(PlayerProcessingJailbirdMessageEventArgs ev) + { + } + + /// + /// Called when a has processed a Jailbird message. + /// + /// The instance. + public virtual void OnProcessedJailbirdMessage(PlayerProcessedJailbirdMessageEventArgs ev) + { + } + + /// + /// Called when a is completing item use. + /// + /// The instance. + public virtual void OnUsingItemCompleting(PlayerUsingItemEventArgs ev) + { + } + + /// + /// Called when a is completing item use. + /// + /// The instance. + public virtual void OnUsedItemCompleting(PlayerUsedItemEventArgs ev) + { + } + + /// + /// Called when SCP-3114 strangle is aborting. + /// + /// The instance. + public virtual void OnStrangleAborting(Scp3114StrangleAbortingEventArgs ev) + { + } + + /// + /// Called when SCP-3114 strangle has aborted. + /// + /// The instance. + public virtual void OnStrangleAborted(Scp3114StrangleAbortedEventArgs ev) + { + } + + /// + /// Called when SCP-3114 strangle is starting. + /// + /// The instance. + public virtual void OnStrangleStarting(Scp3114StrangleStartingEventArgs ev) + { + } + + /// + /// Called when SCP-3114 strangle has started. + /// + /// The instance. + public virtual void OnStrangleStarted(Scp3114StrangleStartedEventArgs ev) + { + } + + /// + /// Called when a is inspecting a keycard. + /// + /// The instance. + public virtual void OnInspectingKeycard(PlayerInspectingKeycardEventArgs ev) + { + } + + /// + /// Called when a has inspected a keycard. + /// + /// The instance. + public virtual void OnInspectedKeycard(PlayerInspectedKeycardEventArgs ev) + { + } + + /// + /// Called when a ' room has changed. + /// + /// The instance. + public virtual void OnRoomChanged(PlayerRoomChangedEventArgs ev) + { + } + + /// + /// Called when a ' zone has changed. + /// + /// The instance. + public virtual void OnZoneChanged(PlayerZoneChangedEventArgs ev) + { + } + + /// + /// Called when a is added to the RA player list. + /// + /// The instance. + public virtual void OnRaPlayerListAddedPlayer(PlayerRaPlayerListAddedPlayerEventArgs ev) + { + } + + /// + /// Called when a is being added to the RA player list. + /// + /// The instance. + public virtual void OnRaPlayerListAddingPlayer(PlayerRaPlayerListAddingPlayerEventArgs ev) + { + } + + /// + /// Called when a requests custom RA info. + /// + /// The instance. + public virtual void OnRequestedCustomRaInfo(PlayerRequestedCustomRaInfoEventArgs ev) + { + } + + /// + /// Called when a requests RA player info. + /// + /// The instance. + public virtual void OnRequestedRaPlayerInfo(PlayerRequestedRaPlayerInfoEventArgs ev) + { + } + + /// + /// Called when a is requesting RA player info. + /// + /// The instance. + public virtual void OnRequestingRaPlayerInfo(PlayerRequestingRaPlayerInfoEventArgs ev) + { + } + + /// + /// Called when a requests the RA player list. + /// + /// The instance. + public virtual void OnRequestedRaPlayerList(PlayerRequestedRaPlayerListEventArgs ev) + { + } + + /// + /// Called when a is requesting the RA player list. + /// + /// The instance. + public virtual void OnRequestingRaPlayerList(PlayerRequestingRaPlayerListEventArgs ev) + { + } + + /// + /// Called when a requests RA players info. + /// + /// The instance. + public virtual void OnRequestedRaPlayersInfo(PlayerRequestedRaPlayersInfoEventArgs ev) + { + } + + /// + /// Called when a is requesting RA players info. + /// + /// The instance. + public virtual void OnRequestingRaPlayersInfo(PlayerRequestingRaPlayersInfoEventArgs ev) + { + } + + /// + /// Called when an objective is completing. + /// + /// The instance. + public virtual void OnCompleting(ObjectiveCompletingBaseEventArgs ev) + { + } + + /// + /// Called when an objective is completed. + /// + /// The instance. + public virtual void OnCompleted(ObjectiveCompletedBaseEventArgs ev) + { + } + + /// + /// Called when activating generator objective is completing. + /// + /// The instance. + public virtual void OnActivatingGeneratorCompleting(GeneratorActivatingEventArgs ev) + { + } + + /// + /// Called when activating generator objective is completed. + /// + /// The instance. + public virtual void OnActivatedGeneratorCompleted(GeneratorActivatedEventArgs ev) + { + } + + /// + /// Called when damaging SCP objective is completing. + /// + /// The instance. + public virtual void OnDamagingScpCompleting(ScpDamagingObjectiveEventArgs ev) + { + } + + /// + /// Called when damaging SCP objective is completed. + /// + /// The instance. + public virtual void OnDamagedScpCompleted(ScpDamagedObjectiveEventArgs ev) + { + } + + /// + /// Called when escaping objective is completing. + /// + /// The instance. + public virtual void OnEscapingCompleting(EscapingObjectiveEventArgs ev) + { + } + + /// + /// Called when escaping objective is completed. + /// + /// The instance. + public virtual void OnEscapedCompleted(EscapedObjectiveEventArgs ev) + { + } + + /// + /// Called when killing enemy objective is completing. + /// + /// The instance. + public virtual void OnKillingEnemyCompleting(EnemyKillingObjectiveEventArgs ev) + { + } + + /// + /// Called when killing enemy objective is completed. + /// + /// The instance. + public virtual void OnKilledEnemyCompleted(EnemyKilledObjectiveEventArgs ev) + { + } + + /// + /// Called when picking SCP item objective is completing. + /// + /// The instance. + public virtual void OnPickingScpItemCompleting(ScpItemPickingObjectiveEventArgs ev) + { + } + + /// + /// Called when picking SCP item objective is completed. + /// + /// The instance. + public virtual void OnPickedScpItemCompleted(ScpItemPickedObjectiveEventArgs ev) + { + } +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/API/Features/InfiniteEffect.cs b/UncomplicatedCustomRoles/API/Features/InfiniteEffect.cs index 790188e..9e1aecc 100644 --- a/UncomplicatedCustomRoles/API/Features/InfiniteEffect.cs +++ b/UncomplicatedCustomRoles/API/Features/InfiniteEffect.cs @@ -1,66 +1,65 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . */ -using MEC; +#nullable enable using System.Collections.Generic; +using MEC; -#nullable enable -namespace UncomplicatedCustomRoles.API.Features +namespace UncomplicatedCustomRoles.API.Features; + +public class InfiniteEffect { - public class InfiniteEffect - { - /// - /// Whether the infinite effect coroutine is running or not - /// - public static bool IsRunning => CoroutineHandle.IsRunning; + /// + /// Whether the infinite effect coroutine is running or not + /// + public static bool IsRunning => CoroutineHandle.IsRunning; - internal static CoroutineHandle CoroutineHandle { get; private set; } + internal static CoroutineHandle CoroutineHandle { get; private set; } - internal static bool EffectAssociationAllowed { get; set; } = false; + internal static bool EffectAssociationAllowed { get; set; } - /// - /// Start the coroutine - /// - public static void Start() - { - if (IsRunning) - return; + /// + /// Start the coroutine + /// + public static void Start() + { + if (IsRunning) + return; - CoroutineHandle = Timing.RunCoroutine(Actor()); - } + CoroutineHandle = Timing.RunCoroutine(Actor()); + } - /// - /// Stop the coroutine - /// - public static void Stop() - { - if (!IsRunning) - return; + /// + /// Stop the coroutine + /// + public static void Stop() + { + if (!IsRunning) + return; - Timing.KillCoroutines(CoroutineHandle); - } + Timing.KillCoroutines(CoroutineHandle); + } - internal static IEnumerator Actor() + internal static IEnumerator Actor() + { + while (EffectAssociationAllowed) { - while (EffectAssociationAllowed) - { - SummonedCustomRole.InfiniteEffectActor(); + SummonedCustomRole.InfiniteEffectActor(); - yield return Timing.WaitForSeconds(2.5f); - } + yield return Timing.WaitForSeconds(2.5f); } + } - internal static void Terminate() - { - EffectAssociationAllowed = false; - Stop(); - } + internal static void Terminate() + { + EffectAssociationAllowed = false; + Stop(); } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/API/Features/LogEntry.cs b/UncomplicatedCustomRoles/API/Features/LogEntry.cs index 2e04f20..e70383d 100644 --- a/UncomplicatedCustomRoles/API/Features/LogEntry.cs +++ b/UncomplicatedCustomRoles/API/Features/LogEntry.cs @@ -1,62 +1,67 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . */ -using Discord; -using System.Text.Json.Serialization; using System; +using System.Text.Json.Serialization; +using Discord; -namespace UncomplicatedCustomRoles.API.Features +namespace UncomplicatedCustomRoles.API.Features; + +internal class LogEntry { - internal class LogEntry + [JsonConstructor] + public LogEntry(long time, string level, string content, string error = null) { - /// - /// Gets the time in unix milliseconds of the message - /// - public long Time { get; } + Time = time; + Level = level; + Content = content; + Error = error; + } - /// - /// Gets the or a custom LogLevel of the message - /// - public string Level { get; } + public LogEntry(long time, LogLevel level, string content, string error = null) : this(time, level.ToString(), + content, error) + { + } - /// - /// Gets the message of the log - /// - public string Content { get; } + /// + /// Gets the time in unix milliseconds of the message + /// + public long Time { get; } -#nullable enable - /// - /// Gets the custom error code of the message - can be null! - /// - public string? Error { get; } -#nullable disable + /// + /// Gets the or a custom LogLevel of the message + /// + public string Level { get; } - /// - /// Gets the instance of the Error as string - /// - public string PublicError => Error is null ? string.Empty : $"{Error} "; + /// + /// Gets the message of the log + /// + public string Content { get; } - [JsonIgnore] - public DateTimeOffset DateTimeOffset => DateTimeOffset.FromUnixTimeMilliseconds(Time); +#nullable enable + /// + /// Gets the custom error code of the message - can be null! + /// + public string? Error { get; } +#nullable disable - [JsonConstructor] - public LogEntry(long time, string level, string content, string error = null) - { - Time = time; - Level = level; - Content = content; - Error = error; - } + /// + /// Gets the instance of the Error as string + /// + public string PublicError => Error is null ? string.Empty : $"{Error} "; - public LogEntry(long time, LogLevel level, string content, string error = null) : this(time, level.ToString(), content, error) { } + [JsonIgnore] public DateTimeOffset DateTimeOffset => DateTimeOffset.FromUnixTimeMilliseconds(Time); - public override string ToString() => $"[{DateTimeOffset.Year}-{DateTimeOffset.Month}-{DateTimeOffset.Day} {DateTimeOffset.Hour}:{DateTimeOffset.Minute}:{DateTimeOffset.Second} {DateTimeOffset.Offset}] [{Level}] [UncomplicatedCustomRoles] {PublicError}{Content}"; + public override string ToString() + { + return + $"[{DateTimeOffset.Year}-{DateTimeOffset.Month}-{DateTimeOffset.Day} {DateTimeOffset.Hour}:{DateTimeOffset.Minute}:{DateTimeOffset.Second} {DateTimeOffset.Offset}] [{Level}] [UncomplicatedCustomRoles] {PublicError}{Content}"; } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/API/Features/Messages/OwnerMessage.cs b/UncomplicatedCustomRoles/API/Features/Messages/OwnerMessage.cs index 214a5c3..55a207b 100644 --- a/UncomplicatedCustomRoles/API/Features/Messages/OwnerMessage.cs +++ b/UncomplicatedCustomRoles/API/Features/Messages/OwnerMessage.cs @@ -1,20 +1,17 @@ -using LabApi.Features.Wrappers; -using System.Text.Json.Serialization; +using System.Text.Json.Serialization; +using LabApi.Features.Wrappers; -namespace UncomplicatedCustomRoles.API.Features.Messages +namespace UncomplicatedCustomRoles.API.Features.Messages; + +internal class OwnerMessage { - internal class OwnerMessage + public OwnerMessage(Player player, string discordId) { - [JsonPropertyName("user_id")] - public string UserId { get; set; } + UserId = player.UserId; + DiscordId = discordId; + } - [JsonPropertyName("discord_id")] - public string DiscordId { get; set; } + [JsonPropertyName("user_id")] public string UserId { get; set; } - public OwnerMessage(Player player, string discordId) - { - UserId = player.UserId; - DiscordId = discordId; - } - } -} + [JsonPropertyName("discord_id")] public string DiscordId { get; set; } +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/API/Features/Messages/PresenceMessage.cs b/UncomplicatedCustomRoles/API/Features/Messages/PresenceMessage.cs index eeb2119..73f84f6 100644 --- a/UncomplicatedCustomRoles/API/Features/Messages/PresenceMessage.cs +++ b/UncomplicatedCustomRoles/API/Features/Messages/PresenceMessage.cs @@ -1,37 +1,23 @@ -using LabApi.Features.Wrappers; -using System.Text.Json.Serialization; +using System.Text.Json.Serialization; +using LabApi.Features.Wrappers; -namespace UncomplicatedCustomRoles.API.Features.Messages -{ - internal class PresenceMessage - { - [JsonPropertyName("server_port")] - public int Port { get; set; } = Server.Port; - - [JsonPropertyName("player_count")] - public int PlayerCount { get; set; } = Server.PlayerCount; +namespace UncomplicatedCustomRoles.API.Features.Messages; - [JsonPropertyName("max_players")] - public int MaxPlayers { get; set; } = Server.MaxPlayers; - - [JsonPropertyName("name")] - public string Name { get; set; } = Server.ServerListName; +internal class PresenceMessage +{ + [JsonPropertyName("server_port")] public int Port { get; set; } = Server.Port; - [JsonPropertyName("max_tps")] - public int MaxTps { get; set; } = Server.MaxTps; + [JsonPropertyName("player_count")] public int PlayerCount { get; set; } = Server.PlayerCount; - [JsonPropertyName("tps")] - public double Tps { get; set; } = Server.Tps; + [JsonPropertyName("max_players")] public int MaxPlayers { get; set; } = Server.MaxPlayers; - [JsonPropertyName("plugin")] - public string PluginName => "UCR"; + [JsonPropertyName("name")] public string Name { get; set; } = Server.ServerListName; - [JsonPropertyName("version")] - public string Version { get; set; } = Plugin.Instance.Version.ToString(4); + [JsonPropertyName("max_tps")] public int MaxTps { get; set; } = Server.MaxTps; + [JsonPropertyName("tps")] public double Tps { get; set; } = Server.Tps; + [JsonPropertyName("plugin")] public string PluginName => "UCR"; - public PresenceMessage() - { } - } -} + [JsonPropertyName("version")] public string Version { get; set; } = Plugin.Instance.Version.ToString(4); +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/API/Features/Messages/ShareLogMessage.cs b/UncomplicatedCustomRoles/API/Features/Messages/ShareLogMessage.cs index bfad133..b732d83 100644 --- a/UncomplicatedCustomRoles/API/Features/Messages/ShareLogMessage.cs +++ b/UncomplicatedCustomRoles/API/Features/Messages/ShareLogMessage.cs @@ -1,26 +1,22 @@ -using LabApi.Features; -using System.Text.Json.Serialization; +using System.Text.Json.Serialization; +using LabApi.Features; using UncomplicatedCustomRoles.Manager; -namespace UncomplicatedCustomRoles.API.Features.Messages +namespace UncomplicatedCustomRoles.API.Features.Messages; + +internal class ShareLogMessage { - internal class ShareLogMessage + public ShareLogMessage(string message) { - [JsonPropertyName("labapi_version")] - public string LabAPIVersion { get; set; } = LabApiProperties.CompiledVersion; + Message = message; + } - [JsonPropertyName("plugin_version")] - public string PluginVersion { get; set; } = Plugin.Instance.Version.ToString(4); + [JsonPropertyName("labapi_version")] public string LabAPIVersion { get; set; } = LabApiProperties.CompiledVersion; - [JsonPropertyName("hash")] - public string Hash { get; set; } = VersionManager.HashFile(Plugin.Instance.FilePath); + [JsonPropertyName("plugin_version")] + public string PluginVersion { get; set; } = Plugin.Instance.Version.ToString(4); - [JsonPropertyName("message")] - public string Message { get; set; } + [JsonPropertyName("hash")] public string Hash { get; set; } = VersionManager.HashFile(Plugin.Instance.FilePath); - public ShareLogMessage(string message) - { - Message = message; - } - } -} + [JsonPropertyName("message")] public string Message { get; set; } +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/API/Features/Spawn.cs b/UncomplicatedCustomRoles/API/Features/Spawn.cs index 6ccf5db..05f8270 100644 --- a/UncomplicatedCustomRoles/API/Features/Spawn.cs +++ b/UncomplicatedCustomRoles/API/Features/Spawn.cs @@ -1,8 +1,8 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . @@ -10,32 +10,31 @@ using System.Collections.Generic; -namespace UncomplicatedCustomRoles.API.Features +namespace UncomplicatedCustomRoles.API.Features; + +public class Spawn { - public class Spawn - { - /// - /// Whether the next respawn wave should be handled by UCR - /// - public static bool DoHandleWave { get; internal set; } = true; + /// + /// Whether the next respawn wave should be handled by UCR + /// + public static bool DoHandleWave { get; internal set; } = true; - /// - /// Gets the list of every player Id that will be spawned in the next wave - /// - public static HashSet SpawnQueue { get; } = new(); + /// + /// Gets the list of every player Id that will be spawned in the next wave + /// + public static HashSet SpawnQueue { get; } = []; - /// - /// Gets a list of players that are being spawned - in this way we don't trigger the pugin - /// - internal static HashSet Spawning { get; } = new(); + /// + /// Gets a list of players that are being spawned - in this way we don't trigger the pugin + /// + internal static HashSet Spawning { get; } = []; - /// - /// Disable the UCR next respawn wave evaluation - /// - public static void DisableSpawnWave() - { - DoHandleWave = false; - SpawnQueue.Clear(); - } + /// + /// Disable the UCR next respawn wave evaluation + /// + public static void DisableSpawnWave() + { + DoHandleWave = false; + SpawnQueue.Clear(); } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/API/Features/SpawnPoint.cs b/UncomplicatedCustomRoles/API/Features/SpawnPoint.cs index dcc243f..529a810 100644 --- a/UncomplicatedCustomRoles/API/Features/SpawnPoint.cs +++ b/UncomplicatedCustomRoles/API/Features/SpawnPoint.cs @@ -1,192 +1,234 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . */ -using System.Text.Json.Serialization; using System.Collections.Generic; using System.Linq; +using System.Text.Json.Serialization; using LabApi.Features.Wrappers; using UncomplicatedCustomRoles.API.Struct; using UncomplicatedCustomRoles.Extensions; using UnityEngine; -namespace UncomplicatedCustomRoles.API.Features +namespace UncomplicatedCustomRoles.API.Features; + +public class SpawnPoint { - public class SpawnPoint + [JsonConstructor] + internal SpawnPoint(string name, string roomId, Triplet positionBase, + Quadruple rotationBase, Triplet roomRotationBase, + bool sync = true, bool @fixed = false) + { + Name = name; + RoomId = roomId.Replace("Christmas", "").Replace("Halloween", ""); + PositionBase = positionBase; + RotationBase = rotationBase; + RoomRotationBase = roomRotationBase; + Sync = sync; + Fixed = @fixed; + + if (!Sync) + UnsyncedList.Add(this); + else + List.Add(this); + } + + internal SpawnPoint(string name, Player player) : this(name, player.Room?.GameObject.name ?? string.Empty, + (player.Room is not null ? player.Room.Position - player.Position : player.Position).ToTriplet(), + new Quadruple(player.Rotation.x, player.Rotation.y, player.Rotation.z, + player.Rotation.w), + player.Room?.Rotation.eulerAngles.ToTriplet() ?? new Triplet(0f, 0f, 0f)) + { + } + + /// + /// Gets the list of every synced in the server + /// + public static HashSet List { get; } = []; + + /// + /// Gets the list of every unsynced in the server + /// + public static HashSet UnsyncedList { get; } = []; + + /// + /// Gets the name of the + /// + public string Name { get; } + + /// + /// Gets the Room ID of the + /// + public string RoomId { get; } + + /// + /// Gets the base position of the + /// + public Triplet PositionBase { get; } + + /// + /// Gets the base rotation of the + /// + public Quadruple RotationBase { get; } + + /// + /// Gets the base room rotation of the + /// + public Triplet RoomRotationBase { get; } + + /// + /// Gets whether the is synced with the UCS cloud (or local file) or not + /// + [JsonIgnore] + public bool Sync { get; set; } + + /// + /// Gets whether the is fixed in the position or not (if fixed then it's NOT linked to the + /// room) + /// + [JsonIgnore] + public bool Fixed { get; set; } + + /// + /// Gets the position of the as a + /// + [JsonIgnore] + public Vector3 Position => new(PositionBase.First, PositionBase.Second, PositionBase.Third); + + /// + /// Gets the rotation of the as a + /// + [JsonIgnore] + public Quaternion Rotation => new(RotationBase.First, RotationBase.Second, RotationBase.Third, RotationBase.Fourth); + + /// + /// Gets the room rotation of the as a + /// + [JsonIgnore] + public Vector3 RoomRotation => new(RoomRotationBase.First, RoomRotationBase.Second, RoomRotationBase.Third); + + /// + /// Gets the linked to the , or null if not found + /// + [JsonIgnore] + public Room Room => RoomId != "" + ? Room.List.FirstOrDefault(room => + room.GameObject.name.Replace("Christmas", "").Replace("Halloween", "") == RoomId) + : null; + + /// + /// Gets a value indicating whether the property is not . + /// + [JsonIgnore] + public bool HasRoom => Room is not null; + + /// + /// Destroys the , removing it from the list + /// + public void Destroy() + { + List.Remove(this); + UnsyncedList.Remove(this); + } + + /// + /// Gets the corrected location of the , taking into account the room rotation + /// + /// + public Vector3 CorrectLocation() + { + return CorrectLocation(Room); + } + + private Vector3 CorrectLocation(Room room) + { + if (Fixed || room is null) + return Position; + + if (room.Rotation.eulerAngles == RoomRotation) + return Position; + + return Quaternion.AngleAxis(room.Rotation.eulerAngles.y - RoomRotation.y, Vector3.up) * Position; + } + + /// + /// Sets the specified player's position and rotation to match the spawn point. + /// + /// + /// If a room is available, the player's position is set relative to the room's position; + /// otherwise, the player's position is set to the spawn point's position. In both cases, the player's rotation + /// is set to match the spawn point. + /// + /// The player to be spawned. Cannot be null. + public void Spawn(Player player) + { + var room = Room; + + if (room is not null) + player.Position = room.Position - CorrectLocation(room); + else + player.Position = Position; + + player.Rotation = Rotation; + } + + public override string ToString() + { + return + $"SpawnPoint '{Name}' at {(Room != null ? Room.GameObject.name.Replace("Christmas", "").Replace("Halloween", "") : "RoomWasNotFound")} ({Position} @ {RoomRotation}) [{HasRoom}]"; + } + + /// + /// Creates a new instance that is not synchronized with the network. + /// + /// The unique name to assign to the spawn point. Cannot be null or empty. + /// The identifier of the room to which the spawn point belongs. Cannot be null or empty. + /// The base position of the spawn point, specified as a triplet of coordinates. + /// The base rotation of the spawn point, specified as a quadruple representing rotation values. + /// The base rotation of the room, specified as a triplet of rotation values. + /// A instance that is not registered for network synchronization. + public static SpawnPoint CreateNotSync(string name, string roomId, Triplet positionBase, + Quadruple rotationBase, Triplet roomRotationBase) + { + return new SpawnPoint(name, roomId, positionBase, rotationBase, roomRotationBase, false); + } + + /// + /// Creates a at a fixed position and rotation with the specified name. + /// + /// The name to assign to the spawn point. Cannot be . + /// The world position where the spawn point will be placed. + /// The world rotation to apply to the spawn point. + /// + /// A instance configured at the specified position and rotation, with fixed + /// (non-randomized) placement. + /// + public static SpawnPoint CreateFixed(string name, Vector3 positionBase, Quaternion rotationBase) + { + return new SpawnPoint(name, "", Triplet.FromVector3(positionBase), + Quadruple.FromQuaternion(rotationBase), + new Triplet(0f, 0f, 0f), false, true); + } + + public static SpawnPoint Get(string name) + { + return List.FirstOrDefault(sp => sp.Name == name) ?? UnsyncedList.FirstOrDefault(sp => sp.Name == name); + } + + public static bool TryGet(string name, out SpawnPoint spawnPoint) + { + spawnPoint = Get(name); + return spawnPoint != null; + } + + public static bool Exists(string name) { - /// - /// Gets the list of every synced in the server - /// - public static HashSet List { get; } = new(); - - /// - /// Gets the list of every unsynced in the server - /// - public static HashSet UnsyncedList { get; } = new(); - - /// - /// Gets the name of the - /// - public string Name { get; } - - /// - /// Gets the Room ID of the - /// - public string RoomId { get; } - - /// - /// Gets the base position of the - /// - public Triplet PositionBase { get; } - - /// - /// Gets the base rotation of the - /// - public Quadruple RotationBase { get; } - - /// - /// Gets the base room rotation of the - /// - public Triplet RoomRotationBase { get; } - - /// - /// Gets whether the is synced with the UCS cloud (or local file) or not - /// - [JsonIgnore] - public bool Sync { get; set; } - - /// - /// Gets whether the is fixed in the position or not (if fixed then it's NOT linked to the room) - /// - [JsonIgnore] - public bool Fixed { get; set; } - - /// - /// Gets the position of the as a - /// - [JsonIgnore] - public Vector3 Position => new(PositionBase.First, PositionBase.Second, PositionBase.Third); - - /// - /// Gets the rotation of the as a - /// - [JsonIgnore] - public Quaternion Rotation => new(RotationBase.First, RotationBase.Second, RotationBase.Third, RotationBase.Fourth); - - /// - /// Gets the room rotation of the as a - /// - [JsonIgnore] - public Vector3 RoomRotation => new(RoomRotationBase.First, RoomRotationBase.Second, RoomRotationBase.Third); - - /// - /// Gets the linked to the , or null if not found - /// - [JsonIgnore] - public Room Room => RoomId != "" ? Room.List.FirstOrDefault(room => room.GameObject.name.Replace("Christmas", "").Replace("Halloween", "") == RoomId) : null; - - /// - /// Gets a value indicating whether the property is not . - /// - [JsonIgnore] - public bool HasRoom => Room is not null; - - [JsonConstructor] - internal SpawnPoint(string name, string roomId, Triplet positionBase, Quadruple rotationBase, Triplet roomRotationBase, bool sync = true, bool @fixed = false) - { - Name = name; - RoomId = roomId.Replace("Christmas", "").Replace("Halloween", ""); - PositionBase = positionBase; - RotationBase = rotationBase; - RoomRotationBase = roomRotationBase; - Sync = sync; - Fixed = @fixed; - - if (!Sync) - UnsyncedList.Add(this); - else - List.Add(this); - } - - internal SpawnPoint(string name, Player player) : this(name, player.Room?.GameObject.name ?? string.Empty, (player.Room is not null ? player.Room.Position - player.Position : player.Position).ToTriplet(), new(player.Rotation.x, player.Rotation.y, player.Rotation.z, player.Rotation.w), player.Room?.Rotation.eulerAngles.ToTriplet() ?? new(0f, 0f, 0f)) { } - - /// - /// Destroys the , removing it from the list - /// - public void Destroy() => List.Remove(this); - - /// - /// Gets the corrected location of the , taking into account the room rotation - /// - /// - public Vector3 CorrectLocation() - { - if (Fixed) - return Position; - - if (Room.Rotation.eulerAngles == RoomRotation) - return Position; - - return Quaternion.AngleAxis(Room.Rotation.eulerAngles.y - RoomRotation.y, Vector3.up) * Position; - } - - /// - /// Sets the specified player's position and rotation to match the spawn point. - /// - /// If a room is available, the player's position is set relative to the room's position; - /// otherwise, the player's position is set to the spawn point's position. In both cases, the player's rotation - /// is set to match the spawn point. - /// The player to be spawned. Cannot be null. - public void Spawn(Player player) - { - if (HasRoom) - player.Position = Room.Position - CorrectLocation(); - else - player.Position = Position; - - player.Rotation = Rotation; - } - - public override string ToString() => $"SpawnPoint '{Name}' at {(Room != null ? Room.GameObject.name.Replace("Christmas", "").Replace("Halloween", "") : "RoomWasNotFound")} ({Position} @ {RoomRotation}) [{HasRoom}]"; - - /// - /// Creates a new instance that is not synchronized with the network. - /// - /// The unique name to assign to the spawn point. Cannot be null or empty. - /// The identifier of the room to which the spawn point belongs. Cannot be null or empty. - /// The base position of the spawn point, specified as a triplet of coordinates. - /// The base rotation of the spawn point, specified as a quadruple representing rotation values. - /// The base rotation of the room, specified as a triplet of rotation values. - /// A instance that is not registered for network synchronization. - public static SpawnPoint CreateNotSync(string name, string roomId, Triplet positionBase, Quadruple rotationBase, Triplet roomRotationBase) => new(name, roomId, positionBase, rotationBase, roomRotationBase, sync: false); - - /// - /// Creates a at a fixed position and rotation with the specified name. - /// - /// The name to assign to the spawn point. Cannot be . - /// The world position where the spawn point will be placed. - /// The world rotation to apply to the spawn point. - /// A instance configured at the specified position and rotation, with fixed - /// (non-randomized) placement. - public static SpawnPoint CreateFixed(string name, Vector3 positionBase, Quaternion rotationBase) => new(name, "", Triplet.FromVector3(positionBase), Quadruple.FromQuaternion(rotationBase), new(0f, 0f, 0f), false, true); - - public static SpawnPoint Get(string name) => List.FirstOrDefault(sp => sp.Name == name) ?? UnsyncedList.FirstOrDefault(sp => sp.Name == name); - - public static bool TryGet(string name, out SpawnPoint spawnPoint) - { - spawnPoint = Get(name); - return spawnPoint != null; - } - - public static bool Exists(string name) - { - return List.Any(sp => sp.Name == name) || UnsyncedList.Any(sp => sp.Name == name); - } + return List.Any(sp => sp.Name == name) || UnsyncedList.Any(sp => sp.Name == name); } } \ No newline at end of file diff --git a/UncomplicatedCustomRoles/API/Features/SummonedCustomRole.cs b/UncomplicatedCustomRoles/API/Features/SummonedCustomRole.cs index 874e9b9..8525741 100644 --- a/UncomplicatedCustomRoles/API/Features/SummonedCustomRole.cs +++ b/UncomplicatedCustomRoles/API/Features/SummonedCustomRole.cs @@ -1,23 +1,23 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . */ -using LabApi.Features.Wrappers; -using MEC; -using PlayerRoles; -using PlayerRoles.PlayableScps; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Text; +using LabApi.Features.Wrappers; +using MEC; +using PlayerRoles; using PlayerRoles.FirstPersonControl; +using PlayerRoles.PlayableScps; using Respawning.Objectives; using UncomplicatedCustomRoles.API.Features.Controllers; using UncomplicatedCustomRoles.API.Features.CustomModules; @@ -28,711 +28,760 @@ using UncomplicatedCustomRoles.Integrations; using UncomplicatedCustomRoles.Manager; using UnityEngine; +using Object = UnityEngine.Object; -namespace UncomplicatedCustomRoles.API.Features -{ +namespace UncomplicatedCustomRoles.API.Features; #pragma warning disable IDE1006 // Stili di denominazione - public class SummonedCustomRole +public class SummonedCustomRole +{ + /// + /// The duration of a tick + /// + public const float TickDuration = 0.25f; + + // Cache to reduce LINQ usage + private static readonly ConcurrentDictionary _cachedListByPlayerId = new(); + + private static readonly ConcurrentDictionary _cachedCountByRoleId = new(); + + internal static int EventTriggeredModuleTotal; + + private int _eventModuleCount; + + internal SummonedCustomRole(Player player, ICustomRole role, Triplet? badge, + List infiniteEffects, PlayerInfoArea playerInfo, CustomInfo customInfo, bool isCustomNickname = false) { - /// - /// Gets every - /// - public static ConcurrentDictionary List { get; } = new(); - - // Cache to reduce LINQ usage - private static readonly ConcurrentDictionary _cachedListByPlayerId = new(); - - private static readonly ConcurrentDictionary _cachedCountByRoleId = new(); - - /// - /// The unique identifier for this instance of - /// - public string Id { get; } - - /// - /// Gets the - /// - public Player Player { get; } - - /// - /// Gets the 's - /// - public ICustomRole Role { get; } - - /// - /// Gets the UNIX timestamp when the player spawned - /// - public long SpawnTime { get; } - - /// - /// Gets the badge of the player if it has one - /// - public Triplet? Badge { get; private set; } - - /// - /// Gets the list of infinite - /// - public List InfiniteEffects { get; } - - /// - /// Gets the current nickname of the player - if null the role didn't changed it! - /// - public bool IsCustomNickname { get; } - - /// - /// Gets the instance of the current instance - /// - public CustomRoleEventHandler EventHandler { get; } - - /// - /// Gets the original of the player - /// - public PlayerInfoArea PlayerInfoArea { get; } - - /// - /// Gets the instance of the player - /// - public CustomInfo CustomInfo { get; } - - /// - /// Gets the of a generic Coroutine that can be used by the custom role manager - /// - public CoroutineHandle GenericCoroutine { get; private set; } - - /// - /// Gets the where you'll be able to add custom actions that will be executed during the execution.

- /// You must return a : if false the coroutine will skip the precoded actions - ///
- public List> CustomActions { get; } = new(); - - /// - /// Gets whether the current has a different team base with a different - /// - public bool IsOverwrittenRole => _roleBase is not null; - - /// - /// Gets whether the current implements a coroutine for handling basic plugin features - /// - public bool IsDefaultCoroutineRole => (Role.HumeShield?.Amount ?? 0) > 0 && (Role.HumeShield?.RegenerationAmount ?? 0) > 0; - - /// - /// Gets if the current SummonedCustomRole is valid or not - /// - public bool IsValid => _internalValid && Player.IsAlive; - - /// - /// Gets the time in UNIX timestamp (seconds) when the received the last damage - /// - public long LastDamageTime { get; internal set; } - - /// - /// Gets a of every installed - /// - public IReadOnlyCollection CustomModules => _customModules; - - private FpcStandardRoleBase _roleBase { get; set; } = null; - - private bool _internalValid { get; set; } - - private bool _isRegeneratingHume { get; set; } - - private List _customModules { get; } - - private int _eventModuleCount; - - internal static int EventTriggeredModuleTotal; - - internal RoleTypeId Appearance => Role.RoleAppearance != Role.Role ? Role.RoleAppearance : RoleTypeId.None; - - internal Vector3 Scale => Role.Scale != Vector3.one && Role.Scale != Vector3.zero ? Role.Scale : Vector3.one; - - /// - /// The duration of a tick - /// - public const float TickDuration = 0.25f; - - internal SummonedCustomRole(Player player, ICustomRole role, Triplet? badge, List infiniteEffects, PlayerInfoArea playerInfo, CustomInfo customInfo, bool isCustomNickname = false) + Id = Guid.NewGuid().ToString(); + Player = player; + Role = role; + SpawnTime = DateTimeOffset.Now.ToUnixTimeMilliseconds(); + Badge = badge; + InfiniteEffects = infiniteEffects; + IsCustomNickname = isCustomNickname; + PlayerInfoArea = playerInfo; + CustomInfo = customInfo; + _internalValid = true; + + if (IsDefaultCoroutineRole) + GenericCoroutine = Timing.RunCoroutine(RoleTickCoroutine()); + + _customModules = CustomModule.Load(Role.CustomFlags ?? [], this); + _eventModuleCount = _customModules.Count(m => m.TriggerOnEvents.Count > 0); + EventTriggeredModuleTotal += _eventModuleCount; + + if (Role.Team is not null && Role.Team != Role.Role.GetTeam()) { - Id = Guid.NewGuid().ToString(); - Player = player; - Role = role; - SpawnTime = DateTimeOffset.Now.ToUnixTimeMilliseconds(); - Badge = badge; - InfiniteEffects = infiniteEffects; - IsCustomNickname = isCustomNickname; - PlayerInfoArea = playerInfo; - CustomInfo = customInfo; - _internalValid = true; - - if (IsDefaultCoroutineRole) - GenericCoroutine = Timing.RunCoroutine(RoleTickCoroutine()); - - _customModules = CustomModule.Load(Role.CustomFlags ?? new(), this); - - _eventModuleCount = _customModules.Count(m => m.TriggerOnEvents.Count > 0); - EventTriggeredModuleTotal += _eventModuleCount; - - if (Role.Team is not null && Role.Team != Role.Role.GetTeam()) - { - EvaluateRoleBase(); - LogManager.Debug($"EVALUATED ROLEBASE {_roleBase?.GetType().FullName} with team {_roleBase?.Team}"); - } + EvaluateRoleBase(); + LogManager.Debug($"EVALUATED ROLEBASE {_roleBase?.GetType().FullName} with team {_roleBase?.Team}"); + } - UnityEngine.Object.Destroy(Player.GameObject.GetComponent()); - - EventHandler = new(this); - List[Id] = this; - _cachedListByPlayerId[player.PlayerId] = this; - if (_cachedCountByRoleId.TryGetValue(role.Id, out int count)) - _cachedCountByRoleId[role.Id] = count + 1; - else - _cachedCountByRoleId[role.Id] = 1; - - if (Role is EventCustomRole eventCustomRole) - eventCustomRole.OnSpawned(this); + Object.Destroy(Player.GameObject.GetComponent()); + + EventHandler = new CustomRoleEventHandler(this); + List[Id] = this; + _cachedListByPlayerId[player.PlayerId] = this; + if (_cachedCountByRoleId.TryGetValue(role.Id, out var count)) + _cachedCountByRoleId[role.Id] = count + 1; + else + _cachedCountByRoleId[role.Id] = 1; - // Appearance handling - if (Appearance != RoleTypeId.None) + if (Role is EventCustomRole eventCustomRole) + eventCustomRole.OnSpawned(this); + + // Appearance handling + if (Appearance != RoleTypeId.None) + Timing.CallDelayed(0.75f, () => { - Timing.CallDelayed(0.75f, () => - { - if (!_internalValid || Player is null || !Player.IsAlive) - return; + if (!_internalValid || Player is null || !Player.IsAlive) + return; - LogManager.Debug($"Changing the appearance of the role {Role.Id} [{Role.Name}] to {Role.RoleAppearance}"); + LogManager.Debug( + $"Changing the appearance of the role {Role.Id} [{Role.Name}] to {Role.RoleAppearance}"); - if (LabApiExtensions.IsAvailable) - LabApiExtensions.AddFakeRole(Player, Role.RoleAppearance); - else - Player.ChangeAppearance(Role.RoleAppearance, SpawnManager.LoadAppearanceAffectedPlayers(Player), true); + if (LabApiExtensions.IsAvailable) + LabApiExtensions.AddFakeRole(Player, Role.RoleAppearance); + else + Player.ChangeAppearance(Role.RoleAppearance, SpawnManager.LoadAppearanceAffectedPlayers(Player), + true); - CustomInfo.Role = Role.RoleAppearance.GetFullName(); - }); - } - } + CustomInfo.Role = Role.RoleAppearance.GetFullName(); + }); + } - /// - /// Try to set in order to override the current Player.Role.Base to trick the server into thinking that the player is / is not an Human - /// - private void EvaluateRoleBase() + /// + /// Gets every + /// + public static ConcurrentDictionary List { get; } = new(); + + /// + /// The unique identifier for this instance of + /// + public string Id { get; } + + /// + /// Gets the + /// + public Player Player { get; } + + /// + /// Gets the 's + /// + public ICustomRole Role { get; } + + /// + /// Gets the UNIX timestamp when the player spawned + /// + public long SpawnTime { get; } + + /// + /// Gets the badge of the player if it has one + /// + public Triplet? Badge { get; } + + /// + /// Gets the list of infinite + /// + public List InfiniteEffects { get; } + + /// + /// Gets the current nickname of the player - if null the role didn't changed it! + /// + public bool IsCustomNickname { get; } + + /// + /// Gets the instance of the current instance + /// + public CustomRoleEventHandler EventHandler { get; } + + /// + /// Gets the original of the player + /// + public PlayerInfoArea PlayerInfoArea { get; } + + /// + /// Gets the instance of the player + /// + public CustomInfo CustomInfo { get; } + + /// + /// Gets the of a generic Coroutine that can be used by the custom role manager + /// + public CoroutineHandle GenericCoroutine { get; } + + /// + /// Gets the where you'll be able to add custom actions that will + /// be executed during the execution.

+ /// You must return a : if false the coroutine will skip the precoded actions + ///
+ public List> CustomActions { get; } = []; + + /// + /// Gets whether the current has a different team base with a different + /// + /// + public bool IsOverwrittenRole => _roleBase is not null; + + /// + /// Gets whether the current implements a coroutine for handling basic plugin + /// features + /// + public bool IsDefaultCoroutineRole => + (Role.HumeShield?.Amount ?? 0) > 0 && (Role.HumeShield?.RegenerationAmount ?? 0) > 0; + + /// + /// Gets if the current SummonedCustomRole is valid or not + /// + public bool IsValid => _internalValid && Player.IsAlive; + + /// + /// Gets the time in UNIX timestamp (seconds) when the received the last damage + /// + public long LastDamageTime { get; internal set; } + + /// + /// Gets a of every installed + /// + public IReadOnlyCollection CustomModules => _customModules; + + private FpcStandardRoleBase _roleBase { get; set; } + + private bool _internalValid { get; set; } + + private bool _isRegeneratingHume { get; set; } + + private List _customModules { get; } + + internal RoleTypeId Appearance => Role.RoleAppearance != Role.Role ? Role.RoleAppearance : RoleTypeId.None; + + internal Vector3 Scale => Role.Scale != Vector3.one && Role.Scale != Vector3.zero ? Role.Scale : Vector3.one; + + /// + /// Try to set in order to override the current Player.Role.Base to trick the server into + /// thinking that the player is / is not an Human + /// + private void EvaluateRoleBase() + { + try { - try + var originalRole = Player.RoleBase as FpcStandardRoleBase; + + if (Role.Team is null) + return; + + if (originalRole is null) { - FpcStandardRoleBase originalRole = Player.RoleBase as FpcStandardRoleBase; + LogManager.Error( + "Failed to evaluate RoleBase for SummonedCustomRole::EvaluateRoleBase() - originalRole is null"); + return; + } - if (Role.Team is null) - return; - - if (originalRole is null) + if (Role.Team is Team.SCPs) + // ReSharper disable once Unity.IncorrectMonoBehaviourInstantiation + _roleBase = new FpcStandardScp { - LogManager.Error("Failed to evaluate RoleBase for SummonedCustomRole::EvaluateRoleBase() - originalRole is null"); - return; - } - - if (Role.Team is Team.SCPs) - // ReSharper disable once Unity.IncorrectMonoBehaviourInstantiation - _roleBase = new FpcStandardScp - { - _roleTypeId = Role.Role, - _maxHealth = Role.Health.Maximum, - _cameraTransform = originalRole._cameraTransform, - _lastPos = originalRole._lastPos, - _hubTransform = originalRole._hubTransform, - FpcModule = originalRole.FpcModule, - VisibilityController = originalRole.VisibilityController, - VoiceModule = originalRole.VoiceModule, - _lastOwner = Player.ReferenceHub, - Ragdoll = originalRole.Ragdoll, - RoleAvatar = originalRole.RoleAvatar, - SpectatorModule = originalRole.SpectatorModule - }; - else - // ReSharper disable once Unity.IncorrectMonoBehaviourInstantiation - _roleBase = new HumanRole - { - _roleId = Role.Role, - _team = Role.Team ?? Role.Role.GetTeam(), - _roleColor = Role.Role.GetRoleColor(), - _cameraTransform = originalRole._cameraTransform, - _lastPos = originalRole._lastPos, - _hubTransform = originalRole._hubTransform, - FpcModule = originalRole.FpcModule, - VisibilityController = originalRole.VisibilityController, - VoiceModule = originalRole.VoiceModule, - VariantsModule = originalRole.VariantsModule, - _lastOwner = Player.ReferenceHub, - Ragdoll = originalRole.Ragdoll, - RoleAvatar = originalRole.RoleAvatar, - SpectatorModule = originalRole.SpectatorModule - }; - - DisguiseTeam.Set(Player.PlayerId, Role.Team ?? Role.Role.GetTeam(), _roleBase); - - Timing.CallDelayed(3.25f, delegate + _roleTypeId = Role.Role, + _maxHealth = Role.Health.Maximum, + _cameraTransform = originalRole._cameraTransform, + _lastPos = originalRole._lastPos, + _hubTransform = originalRole._hubTransform, + FpcModule = originalRole.FpcModule, + VisibilityController = originalRole.VisibilityController, + VoiceModule = originalRole.VoiceModule, + _lastOwner = Player.ReferenceHub, + Ragdoll = originalRole.Ragdoll, + RoleAvatar = originalRole.RoleAvatar, + SpectatorModule = originalRole.SpectatorModule + }; + else + // ReSharper disable once Unity.IncorrectMonoBehaviourInstantiation + _roleBase = new HumanRole { - if (!_internalValid || _roleBase is null) - return; - - _roleBase.Pooled = false; - DisguiseTeam.Set(Player.PlayerId, Role.Team ?? Role.Role.GetTeam(), _roleBase); - }); - } - catch (Exception e) + _roleId = Role.Role, + _team = Role.Team ?? Role.Role.GetTeam(), + _roleColor = Role.Role.GetRoleColor(), + _cameraTransform = originalRole._cameraTransform, + _lastPos = originalRole._lastPos, + _hubTransform = originalRole._hubTransform, + FpcModule = originalRole.FpcModule, + VisibilityController = originalRole.VisibilityController, + VoiceModule = originalRole.VoiceModule, + VariantsModule = originalRole.VariantsModule, + _lastOwner = Player.ReferenceHub, + Ragdoll = originalRole.Ragdoll, + RoleAvatar = originalRole.RoleAvatar, + SpectatorModule = originalRole.SpectatorModule + }; + + DisguiseTeam.Set(Player.PlayerId, Role.Team ?? Role.Role.GetTeam(), _roleBase); + + Timing.CallDelayed(3.25f, delegate { - LogManager.Error($"Failed to evaluate RoleBase for SummonedCustomRole::EvaluateRoleBase() - {e}"); - } - } + if (!_internalValid || _roleBase is null) + return; - /// - /// Runs every custom action in and evaluate their results - /// - /// - private bool EvaluateCustomActions() + _roleBase.Pooled = false; + DisguiseTeam.Set(Player.PlayerId, Role.Team ?? Role.Role.GetTeam(), _roleBase); + }); + } + catch (Exception e) { - bool _result = false; - foreach (Func func in CustomActions) - _result &= func(this); - return _result; + LogManager.Error($"Failed to evaluate RoleBase for SummonedCustomRole::EvaluateRoleBase() - {e}"); } + } - /// - /// Remove the SummonedCustomRole from the list by destroying it! - /// - public void Destroy() + /// + /// Runs every custom action in and evaluate their results + /// + /// + private bool EvaluateCustomActions() + { + var _result = true; + foreach (var func in CustomActions) + _result &= func(this); + return _result; + } + + /// + /// Remove the SummonedCustomRole from the list by destroying it! + /// + public void Destroy() + { + LogManager.Silent($"Destroying instance {Id} of CR {Role.Id} of PL {Player}"); + Remove(); + List.TryRemove(Id, out _); + _cachedListByPlayerId.TryRemove(Player.PlayerId, out _); + if (_cachedCountByRoleId.TryGetValue(Role.Id, out var count) && count > 0) { - LogManager.Silent($"Destroying instance {Id} of CR {Role.Id} of PL {Player}"); - Remove(); - List.TryRemove(Id, out _); - _cachedListByPlayerId.TryRemove(Player.PlayerId, out _); - if (_cachedCountByRoleId.TryGetValue(Role.Id, out int count) && count > 0) - { - count--; - if (count == 0) - _cachedCountByRoleId.TryRemove(Role.Id, out _); - else - _cachedCountByRoleId[Role.Id] = count; - } + count--; + if (count == 0) + _cachedCountByRoleId.TryRemove(Role.Id, out _); + else + _cachedCountByRoleId[Role.Id] = count; } + } - /// - /// Remove the current CustomRole from the player without destroying the instance - /// - public void Remove() + /// + /// Remove the current CustomRole from the player without destroying the instance + /// + public void Remove() + { + try { - try + foreach (var module in _customModules.ToArray()) { - foreach (CustomModule module in _customModules.ToArray()) - { - module.OnRemoved(); - _customModules.Remove(module); - } + module.OnRemoved(); + _customModules.Remove(module); + } - if (Role.BadgeName is not null && Role.BadgeName.Length > 1 && Role.BadgeColor is not null && Role.BadgeColor.Length > 2 && Badge is not null && Badge is Triplet badge) - { - Player.ReferenceHub.serverRoles.SetText(badge.First); - Player.ReferenceHub.serverRoles.SetColor(badge.Second); - Player.ReferenceHub.serverRoles.RefreshLocalTag(); + if (Role.BadgeName is not null && Role.BadgeName.Length > 1 && Role.BadgeColor is not null && + Role.BadgeColor.Length > 2 && Badge is not null && Badge is Triplet badge) + { + Player.ReferenceHub.serverRoles.SetText(badge.First); + Player.ReferenceHub.serverRoles.SetColor(badge.Second); + Player.ReferenceHub.serverRoles.RefreshLocalTag(); - LogManager.Debug($"Badge detected, fixed"); - } + LogManager.Debug("Badge detected, fixed"); + } - CustomInfo.SuppressExternalSync = true; - try - { - Player.ReferenceHub.nicknameSync.Network_playerInfoToShow = PlayerInfoArea; - Player.ReferenceHub.nicknameSync.Network_customPlayerInfoString = string.Empty; - } - finally - { - CustomInfo.SuppressExternalSync = false; - } - - LogManager.Debug("Scale reset to 1, 1, 1"); - Player.Scale = new(1, 1, 1); - - Player.IsDisarmed = false; - - DisguiseTeam.Remove(Player.PlayerId); - - // Reset ammo limit - if (Role.Ammo is Dictionary ammoList && ammoList.Count > 0) - foreach (ItemType ammo in ammoList.Keys) - Player.ResetAmmoLimit(ammo); - - // Reset category limit - if (Role.CustomInventoryLimits is Dictionary inventoryLimits && inventoryLimits.Count > 0) - foreach (ItemCategory category in inventoryLimits.Keys) - Player.ResetCategoryLimit(category); - - if (IsCustomNickname) - Player.DisplayName = null; - - if (IsDefaultCoroutineRole && GenericCoroutine.IsRunning) - Timing.KillCoroutines(GenericCoroutine); - - // Remove effects - Player.DisableAllEffects(); - InfiniteEffects.Clear(); - - if (Appearance != RoleTypeId.None && LabApiExtensions.IsAvailable) - LabApiExtensions.RemoveFakeRole(Player); - - if (Role is EventCustomRole eventCustomRole) - eventCustomRole.OnRemoved(this); + CustomInfo.SuppressExternalSync = true; + try + { + Player.ReferenceHub.nicknameSync.Network_playerInfoToShow = PlayerInfoArea; + Player.ReferenceHub.nicknameSync.Network_customPlayerInfoString = string.Empty; } - catch (Exception e) + finally { - LogManager.Error($"Failed to act SummonedCustomRole::Remove() - {e.GetType().FullName}: {e.Message}\n{e.StackTrace}"); + CustomInfo.SuppressExternalSync = false; } - EventHandler?.Unload(); - - EventTriggeredModuleTotal -= _eventModuleCount; - if (EventTriggeredModuleTotal < 0) - EventTriggeredModuleTotal = 0; - _eventModuleCount = 0; - - _customModules.Clear(); - _internalValid = false; - } + LogManager.Debug("Scale reset to 1, 1, 1"); + Player.Scale = new Vector3(1, 1, 1); - /// - /// If the role is this coroutine will handle every functions that requires one - /// - /// - private IEnumerator RoleTickCoroutine() - { - while (_internalValid && Player.IsAlive && IsDefaultCoroutineRole) - { - if (Player.HumeShield < Role.HumeShield.Maximum && DateTimeOffset.UtcNow.ToUnixTimeSeconds() - LastDamageTime >= Role.HumeShield.RegenerationDelay && !_isRegeneratingHume) - Timing.RunCoroutine(HumeShieldCoroutine()); + Player.IsDisarmed = false; - yield return Timing.WaitForSeconds(TickDuration); - } - } + DisguiseTeam.Remove(Player.PlayerId); + + // Reset ammo limit + if (Role.Ammo is Dictionary ammoList && ammoList.Count > 0) + foreach (var ammo in ammoList.Keys) + Player.ResetAmmoLimit(ammo); + + // Reset category limit + if (Role.CustomInventoryLimits is Dictionary inventoryLimits && + inventoryLimits.Count > 0) + foreach (var category in inventoryLimits.Keys) + Player.ResetCategoryLimit(category); + + if (IsCustomNickname) + Player.DisplayName = null; + + if (IsDefaultCoroutineRole && GenericCoroutine.IsRunning) + Timing.KillCoroutines(GenericCoroutine); + + // Remove effects + Player.DisableAllEffects(); + InfiniteEffects.Clear(); - /// - /// The coroutine to regenerate Hume Shield - /// - /// - public IEnumerator HumeShieldCoroutine() + if (Appearance != RoleTypeId.None && LabApiExtensions.IsAvailable) + LabApiExtensions.RemoveFakeRole(Player); + + if (Role is EventCustomRole eventCustomRole) + eventCustomRole.OnRemoved(this); + } + catch (Exception e) { - _isRegeneratingHume = true; - while (_internalValid && Player.IsAlive && Player.HumeShield < Role.HumeShield.Maximum && DateTimeOffset.UtcNow.ToUnixTimeSeconds() - LastDamageTime >= Role.HumeShield.RegenerationDelay) - { - Player.HumeShield += Role.HumeShield.RegenerationAmount; - yield return Role.HumeShield.RegenerationSpeed == 0 ? Timing.WaitForOneFrame : Timing.WaitForSeconds(Role.HumeShield.RegenerationSpeed); - } - _isRegeneratingHume = false; + LogManager.Error( + $"Failed to act SummonedCustomRole::Remove() - {e.GetType().FullName}: {e.Message}\n{e.StackTrace}"); } - /// - /// Gets a that this custom role implements - /// - /// - /// - public T GetModule() where T : CustomModule => _customModules.FirstOrDefault(cm => cm.GetType() == typeof(T)) as T; - - /// - /// Gets a array that contains every custom module with the same type - /// - /// - /// - public T[] GetModules() where T : CustomModule + EventHandler?.Unload(); + + EventTriggeredModuleTotal -= _eventModuleCount; + if (EventTriggeredModuleTotal < 0) + EventTriggeredModuleTotal = 0; + _eventModuleCount = 0; + + _customModules.Clear(); + _internalValid = false; + } + + /// + /// If the role is this coroutine will handle every functions that requires one + /// + /// + private IEnumerator RoleTickCoroutine() + { + while (_internalValid && Player.IsAlive && IsDefaultCoroutineRole) { - if (_customModules.Count == 0) - return Array.Empty(); + if (EvaluateCustomActions() && Player.HumeShield < Role.HumeShield.Maximum && + DateTimeOffset.UtcNow.ToUnixTimeSeconds() - LastDamageTime >= Role.HumeShield.RegenerationDelay && + !_isRegeneratingHume) + Timing.RunCoroutine(HumeShieldCoroutine()); - return _customModules - .OfType() - .ToArray(); + yield return Timing.WaitForSeconds(TickDuration); } + } - /// - /// Try to get a if its implemented - /// - /// - /// - /// - public bool TryGetModule(out T module) where T : CustomModule + /// + /// The coroutine to regenerate Hume Shield + /// + /// + public IEnumerator HumeShieldCoroutine() + { + _isRegeneratingHume = true; + while (_internalValid && Player.IsAlive && Player.HumeShield < Role.HumeShield.Maximum && + DateTimeOffset.UtcNow.ToUnixTimeSeconds() - LastDamageTime >= Role.HumeShield.RegenerationDelay) { - module = GetModule(); - return module != null; + Player.HumeShield += Role.HumeShield.RegenerationAmount; + yield return Role.HumeShield.RegenerationSpeed == 0 + ? Timing.WaitForOneFrame + : Timing.WaitForSeconds(Role.HumeShield.RegenerationSpeed); } - /// - /// Gets if the current implements the given - /// - /// - /// - public bool HasModule() where T : CustomModule => _customModules.Any(cm => cm.GetType() == typeof(T)); + _isRegeneratingHume = false; + } + + /// + /// Gets a that this custom role implements + /// + /// + /// + public T GetModule() where T : CustomModule + { + return _customModules.FirstOrDefault(cm => cm.GetType() == typeof(T)) as T; + } + + /// + /// Gets a array that contains every custom module with the same type + /// + /// + /// + public T[] GetModules() where T : CustomModule + { + if (_customModules.Count == 0) + return []; + + return _customModules + .OfType() + .ToArray(); + } + + /// + /// Try to get a if its implemented + /// + /// + /// + /// + public bool TryGetModule(out T module) where T : CustomModule + { + module = GetModule(); + return module != null; + } + + /// + /// Gets if the current implements the given + /// + /// + /// + public bool HasModule() where T : CustomModule + { + return _customModules.Any(cm => cm.GetType() == typeof(T)); + } #nullable enable - /// - /// Add a new to the current instance - /// - /// - public void AddModule(Type type, Dictionary? args = null) + /// + /// Add a new to the current instance + /// + /// + public void AddModule(Type type, Dictionary? args = null) + { + if (CustomModule.FastAdd(type, this, args) is CustomModule module) { - if (CustomModule.FastAdd(type, this, args) is CustomModule module) + _customModules.Add(module); + if (module.TriggerOnEvents.Count > 0) { - _customModules.Add(module); - if (module.TriggerOnEvents.Count > 0) - { - _eventModuleCount++; - EventTriggeredModuleTotal++; - } + _eventModuleCount++; + EventTriggeredModuleTotal++; } } + } #nullable disable - /// - /// Try to remove the first - /// - /// - public void RemoveModule() where T : CustomModule + /// + /// Try to remove the first + /// + /// + public void RemoveModule() where T : CustomModule + { + if (TryGetModule(out T module)) { - if (TryGetModule(out T module)) + if (module.TriggerOnEvents.Count > 0) { - if (module.TriggerOnEvents.Count > 0) - { - _eventModuleCount--; - EventTriggeredModuleTotal--; - if (EventTriggeredModuleTotal < 0) - EventTriggeredModuleTotal = 0; - } - module.OnRemoved(); - _customModules.Remove(module); + _eventModuleCount--; + EventTriggeredModuleTotal--; + if (EventTriggeredModuleTotal < 0) + EventTriggeredModuleTotal = 0; } - } - /// - /// Remove every with the same given type - /// - /// - public void RemoveModules() where T : CustomModule - { - foreach (CustomModule _ in GetModules()) - RemoveModule(); + module.OnRemoved(); + _customModules.Remove(module); } + } - /// - /// Gets every with the same as a - /// - /// - /// - public static List Get(ICustomRole role) => List.Values.Where(scr => scr.Role == role).ToList(); - - /// - /// Gets a instance by the - /// - /// - /// - public static SummonedCustomRole Get(Player player) - { - if (player is null) - return null; + /// + /// Remove every with the same given type + /// + /// + public void RemoveModules() where T : CustomModule + { + foreach (CustomModule _ in GetModules()) + RemoveModule(); + } - if (_cachedListByPlayerId.TryGetValue(player.PlayerId, out SummonedCustomRole role)) - return role; + /// + /// Gets every with the same as a + /// + /// + /// + public static List Get(ICustomRole role) + { + return List.Values.Where(scr => scr.Role == role).ToList(); + } + /// + /// Gets a instance by the + /// + /// + /// + public static SummonedCustomRole Get(Player player) + { + if (player is null) return null; - } - /// - /// Gets a instance by the - /// - /// - /// - public static SummonedCustomRole Get(ReferenceHub player) - { - if (player is null) - return null; + if (_cachedListByPlayerId.TryGetValue(player.PlayerId, out var role)) + return role; - if (_cachedListByPlayerId.TryGetValue(player.PlayerId, out SummonedCustomRole role)) - return role; + return null; + } + /// + /// Gets a instance by the + /// + /// + /// + public static SummonedCustomRole Get(ReferenceHub player) + { + if (player is null) return null; - } - /// - /// Gets a instance by the Id - /// - /// - /// - public static SummonedCustomRole Get(string id) => List.Values.FirstOrDefault(scr => scr.Id == id); - - /// - /// Try to get a by the - /// - /// - /// - /// - public static bool TryGet(Player player, out SummonedCustomRole role) + if (_cachedListByPlayerId.TryGetValue(player.PlayerId, out var role)) + return role; + + return null; + } + + /// + /// Gets a instance by the Id + /// + /// + /// + public static SummonedCustomRole Get(string id) + { + return List.Values.FirstOrDefault(scr => scr.Id == id); + } + + /// + /// Try to get a by the + /// + /// + /// + /// + public static bool TryGet(Player player, out SummonedCustomRole role) + { + role = Get(player); + return role != null; + } + + /// + /// Try to get a by the + /// + /// + /// + /// + public static bool TryGet(ReferenceHub player, out SummonedCustomRole role) + { + if (player is null) { - role = Get(player); - return role != null; + role = null; + return false; } - /// - /// Try to get a by the - /// - /// - /// - /// - public static bool TryGet(ReferenceHub player, out SummonedCustomRole role) - { - if (player is null) - throw new ArgumentNullException(nameof(player)); + return _cachedListByPlayerId.TryGetValue(player.PlayerId, out role); + } - return _cachedListByPlayerId.TryGetValue(player.PlayerId, out role); - } + /// + /// Gets the number of with the same + /// + /// + /// + public static int Count(ICustomRole role) + { + return _cachedCountByRoleId.TryGetValue(role.Id, out var count) ? count : 0; + } - /// - /// Gets the number of with the same - /// - /// - /// - public static int Count(ICustomRole role) => _cachedCountByRoleId.TryGetValue(role.Id, out var count) ? count : 0; - - /// - /// Gets the number of with the same Id - /// - /// - /// - public static int Count(int id) => _cachedCountByRoleId.TryGetValue(id, out var count) ? count : 0; - - /// - /// Summon a new instance of by spawning a player - /// - /// - /// - /// - public static SummonedCustomRole Summon(Player player, ICustomRole role) - { - if (role.SpawnSettings is not null) - SpawnManager.SummonCustomSubclass(player, role.Id); - else - SpawnManager.SummonSubclassApplier(player, role); + /// + /// Gets the number of with the same Id + /// + /// + /// + public static int Count(int id) + { + return _cachedCountByRoleId.TryGetValue(id, out var count) ? count : 0; + } - return Get(player); - } + /// + /// Summon a new instance of by spawning a player + /// + /// + /// + /// + public static SummonedCustomRole Summon(Player player, ICustomRole role) + { + if (role.SpawnSettings is not null) + SpawnManager.SummonCustomSubclass(player, role.Id); + else + SpawnManager.SummonSubclassApplier(player, role); - /// - /// Try to get the custom of the of the found - /// - /// - /// - /// - public static bool TryPatchCustomRole(ReferenceHub player, out Team team) - { - if (player is not null && TryGet(player, out SummonedCustomRole customRole) && customRole.Role.Team is not null && customRole.Role.Team != customRole.Role.Role.GetTeam()) - { - team = (Team)customRole.Role.Team; - return true; - } + return Get(player); + } - team = player?.GetRoleId().GetTeam() ?? Team.OtherAlive; - return false; + /// + /// Try to get the custom of the of the found + /// + /// + /// + /// + /// + public static bool TryPatchCustomRole(ReferenceHub player, out Team team) + { + if (player is not null && TryGet(player, out var customRole) && customRole.Role.Team is not null && + customRole.Role.Team != customRole.Role.Role.GetTeam()) + { + team = (Team)customRole.Role.Team; + return true; } - /// - /// Try to get the custom of the of the found and override it only if necessary - /// - /// - /// - /// - public static bool TryPatchRoleBase(ReferenceHub player, out PlayerRoleBase roleBase) - { - if (player is not null && TryGet(player, out SummonedCustomRole customRole) && customRole._roleBase is not null) - { - roleBase = customRole._roleBase; - return true; - } + team = player?.GetRoleId().GetTeam() ?? Team.OtherAlive; + return false; + } - roleBase = null; - return false; + /// + /// Try to get the custom of the of the found + /// and override it only if necessary + /// + /// + /// + /// + public static bool TryPatchRoleBase(ReferenceHub player, out PlayerRoleBase roleBase) + { + if (player is not null && TryGet(player, out var customRole) && customRole._roleBase is not null) + { + roleBase = customRole._roleBase; + return true; } - /// - /// Try to check if the custom of the of the found is equal to the given - /// - /// - /// - /// - public static bool TryCheckForCustomTeam(ReferenceHub player, Team teamCheck, out bool result) - { - if (TryPatchCustomRole(player, out Team customTeam)) - { - result = customTeam == teamCheck; - return true; - } + roleBase = null; + return false; + } - result = false; - return false; + /// + /// Try to check if the custom of the of the found + /// is equal to the given + /// + /// + /// + /// + public static bool TryCheckForCustomTeam(ReferenceHub player, Team teamCheck, out bool result) + { + if (TryPatchCustomRole(player, out var customTeam)) + { + result = customTeam == teamCheck; + return true; } - /// - /// Try to get the custom of the of the found , otherwise return the given default - /// - /// - /// - /// - public static Team TryGetCustomTeam(ReferenceHub player, Team? def = null) - { - if (TryGet(player, out SummonedCustomRole customRole) && customRole.Role.Team is not null && customRole.Role.Team != customRole.Role.Role.GetTeam()) - return (Team)customRole.Role.Team; + result = false; + return false; + } - return def ?? player.GetRoleId().GetTeam(); - } + /// + /// Try to get the custom of the of the found + /// , otherwise return the given default + /// + /// + /// + /// + public static Team TryGetCustomTeam(ReferenceHub player, Team? def = null) + { + if (TryGet(player, out var customRole) && customRole.Role.Team is not null && + customRole.Role.Team != customRole.Role.Role.GetTeam()) + return (Team)customRole.Role.Team; + + return def ?? player.GetRoleId().GetTeam(); + } - /// - /// Try to get the Remote Admin text from a - /// - /// - /// - /// - - public static void TryParseRemoteAdmin(ReferenceHub player, StringBuilder builder) //REF + /// + /// Try to get the Remote Admin text from a + /// + /// + /// + /// + public static void TryParseRemoteAdmin(ReferenceHub player, StringBuilder builder) //REF + { + if (Plugin.HttpManager.Credits.TryGetValue(player.authManager.UserId, out var tag) && + !string.IsNullOrEmpty(tag.First) && !string.IsNullOrEmpty(tag.Second)) { - if (Plugin.HttpManager.Credits.TryGetValue(player.authManager.UserId, out Triplet tag) && - !string.IsNullOrEmpty(tag.First) && !string.IsNullOrEmpty(tag.Second)) - { - if (Plugin.HttpManager.IsJobRole.Contains(player.authManager.UserId)) - builder.AppendLine( - $"\nUCS Status: [UCS EMPLOYEE] {tag.First}"); - else - builder.AppendLine( - $"\nUCS Status: [UCS CONTRIBUTOR] {tag.First}"); - } + if (!SpawnManager.colorMap.TryGetValue(tag.Second, out var tagColor)) + tagColor = "white"; - if (TryGet(player, out SummonedCustomRole role)) - { - builder.AppendLine($"\nUncomplicatedCustomRoles v{Plugin.Instance.Version}"); - builder.AppendLine(Info.BuildInfo(role.Role)); - } + if (Plugin.HttpManager.IsJobRole.Contains(player.authManager.UserId)) + builder.AppendLine( + $"\nUCS Status: [UCS EMPLOYEE] {tag.First}"); + else + builder.AppendLine( + $"\nUCS Status: [UCS CONTRIBUTOR] {tag.First}"); } - public static void RemoveSpecificRole(int id) + if (TryGet(player, out var role)) { - foreach (SummonedCustomRole role in List.Values.Where(scr => scr.Role.Id == id)) - { - role.Destroy(); - role.Player.SendBroadcast("You Custom Role has been removed as it has been removed from the list!", 6); - } + builder.AppendLine( + $"\nUncomplicatedCustomRoles v{Plugin.Instance.Version}"); + builder.AppendLine(Info.BuildInfo(role.Role)); } + } - /// - /// Handle the infinite effects for every instance - /// - internal static void InfiniteEffectActor() + public static void RemoveSpecificRole(int id) + { + foreach (var role in List.Values.Where(scr => scr.Role.Id == id)) { - foreach (SummonedCustomRole Role in List.Values) - if (Role.InfiniteEffects.Any()) - foreach (IEffect Effect in Role.InfiniteEffects) - Role.Player.ReferenceHub.ForceApplyEffect(Effect.EffectType, Effect.Intensity, float.MaxValue); + role.Destroy(); + role.Player.SendBroadcast( + "Your Custom Role has been removed as it has been removed from the list!", 6); } + } - public override string ToString() => $"Player {Player.Nickname} ({Player.PlayerId}) - CustomRole {Role.Id} ({Role.Nickname})"; + /// + /// Handle the infinite effects for every instance + /// + internal static void InfiniteEffectActor() + { + foreach (var Role in List.Values) + if (Role.InfiniteEffects.Any()) + foreach (var Effect in Role.InfiniteEffects) + Role.Player.ReferenceHub.ForceApplyEffect(Effect.EffectType, Effect.Intensity, float.MaxValue); + } + + public override string ToString() + { + return $"Player {Player.Nickname} ({Player.PlayerId}) - CustomRole {Role.Id} ({Role.Nickname})"; } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/API/Interfaces/ICustomRole.cs b/UncomplicatedCustomRoles/API/Interfaces/ICustomRole.cs index 75c14d6..7954e6b 100644 --- a/UncomplicatedCustomRoles/API/Interfaces/ICustomRole.cs +++ b/UncomplicatedCustomRoles/API/Interfaces/ICustomRole.cs @@ -1,86 +1,84 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . */ -using PlayerRoles; using System.Collections.Generic; +using PlayerRoles; using UncomplicatedCustomRoles.API.Features.Behaviour; using UncomplicatedCustomRoles.Manager; using UnityEngine; -namespace UncomplicatedCustomRoles.API.Interfaces -{ +namespace UncomplicatedCustomRoles.API.Interfaces; #nullable enable - public interface ICustomRole - { - public abstract int Id { get; set; } +public interface ICustomRole +{ + public abstract int Id { get; set; } - public abstract string Name { get; set; } + public abstract string Name { get; set; } - public abstract bool OverrideRoleName { get; set; } + public abstract bool OverrideRoleName { get; set; } - public abstract string? Nickname { get; set; } + public abstract string? Nickname { get; set; } - public abstract string CustomInfo { get; set; } + public abstract string CustomInfo { get; set; } - public abstract string BadgeName { get; set; } + public abstract string BadgeName { get; set; } - public abstract string BadgeColor { get; set; } + public abstract string BadgeColor { get; set; } - public abstract RoleTypeId Role { get; set; } + public abstract RoleTypeId Role { get; set; } - public abstract Team? Team { get; set; } + public abstract Team? Team { get; set; } - public abstract RoleTypeId RoleAppearance { get; set; } + public abstract RoleTypeId RoleAppearance { get; set; } - public abstract List IsFriendOf { get; set; } + public abstract List IsFriendOf { get; set; } - public abstract HealthBehaviour Health { get; set; } + public abstract HealthBehaviour Health { get; set; } - public abstract AhpBehaviour Ahp { get; set; } + public abstract AhpBehaviour Ahp { get; set; } - public abstract HumeShieldBehaviour HumeShield { get; set; } + public abstract HumeShieldBehaviour HumeShield { get; set; } - public abstract List? Effects { get; set; } + public abstract List? Effects { get; set; } - public abstract StaminaBehaviour Stamina { get; set; } + public abstract StaminaBehaviour Stamina { get; set; } - public abstract int MaxScp330Candies { get; set; } + public abstract int MaxScp330Candies { get; set; } - public abstract bool CanEscape { get; set; } + public abstract bool CanEscape { get; set; } - public abstract Dictionary RoleAfterEscape { get; set; } + public abstract Dictionary RoleAfterEscape { get; set; } - public abstract Vector3 Scale { get; set; } + public abstract Vector3 Scale { get; set; } - public abstract string SpawnBroadcast { get; set; } + public abstract string SpawnBroadcast { get; set; } - public abstract ushort SpawnBroadcastDuration { get; set; } + public abstract ushort SpawnBroadcastDuration { get; set; } - public abstract string SpawnHint { get; set; } + public abstract string SpawnHint { get; set; } - public abstract float SpawnHintDuration { get; set; } + public abstract float SpawnHintDuration { get; set; } - public abstract Dictionary CustomInventoryLimits { get; set; } + public abstract Dictionary CustomInventoryLimits { get; set; } - public abstract List Inventory { get; set; } + public abstract List Inventory { get; set; } - public abstract List CustomItemsInventory { get; set; } + public abstract List CustomItemsInventory { get; set; } - public abstract Dictionary Ammo { get; set; } + public abstract Dictionary Ammo { get; set; } - public abstract float DamageMultiplier { get; set; } + public abstract float DamageMultiplier { get; set; } - public abstract SpawnBehaviour? SpawnSettings { get; set; } + public abstract SpawnBehaviour? SpawnSettings { get; set; } - public abstract List? CustomFlags { get; set; } + public abstract List? CustomFlags { get; set; } - public abstract bool IgnoreSpawnSystem { get; set; } - } + public abstract bool IgnoreSpawnSystem { get; set; } } \ No newline at end of file diff --git a/UncomplicatedCustomRoles/API/Interfaces/IEffect.cs b/UncomplicatedCustomRoles/API/Interfaces/IEffect.cs index 5504d42..c386a56 100644 --- a/UncomplicatedCustomRoles/API/Interfaces/IEffect.cs +++ b/UncomplicatedCustomRoles/API/Interfaces/IEffect.cs @@ -1,23 +1,22 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . */ - -namespace UncomplicatedCustomRoles.API.Interfaces + +namespace UncomplicatedCustomRoles.API.Interfaces; + +public interface IEffect { - public interface IEffect - { - public abstract string EffectType { get; set; } + public abstract string EffectType { get; set; } - public abstract float Duration { get; set; } + public abstract float Duration { get; set; } - public abstract byte Intensity { get; set; } + public abstract byte Intensity { get; set; } - public abstract bool Removable { get; set; } - } + public abstract bool Removable { get; set; } } \ No newline at end of file diff --git a/UncomplicatedCustomRoles/API/Interfaces/IUCRCommand.cs b/UncomplicatedCustomRoles/API/Interfaces/IUCRCommand.cs index 85b1faa..88f497f 100644 --- a/UncomplicatedCustomRoles/API/Interfaces/IUCRCommand.cs +++ b/UncomplicatedCustomRoles/API/Interfaces/IUCRCommand.cs @@ -1,26 +1,25 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . */ -using CommandSystem; using System.Collections.Generic; +using CommandSystem; + +namespace UncomplicatedCustomRoles.API.Interfaces; -namespace UncomplicatedCustomRoles.API.Interfaces +internal interface IUCRCommand { - internal interface IUCRCommand - { - public string Name { get; } + public string Name { get; } - public string Description { get; } + public string Description { get; } - public string RequiredPermission { get; } + public string RequiredPermission { get; } - public bool Executor(List arguments, ICommandSender sender, out string response); - } -} + public bool Executor(List arguments, ICommandSender sender, out string response); +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/API/Struct/Quadruple.cs b/UncomplicatedCustomRoles/API/Struct/Quadruple.cs index 4907261..90a701f 100644 --- a/UncomplicatedCustomRoles/API/Struct/Quadruple.cs +++ b/UncomplicatedCustomRoles/API/Struct/Quadruple.cs @@ -1,47 +1,54 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . */ -namespace UncomplicatedCustomRoles.API.Struct +using UnityEngine; + +namespace UncomplicatedCustomRoles.API.Struct; + +public readonly struct Quadruple { - public readonly struct Quadruple + /// + /// Gets the first value + /// + public TFirst First { get; } + + /// + /// Gets the second value + /// + public TSecond Second { get; } + + /// + /// Gets the third value + /// + public TThird Third { get; } + + /// + /// Gets the fourth value + /// + public TFourth Fourth { get; } + + public Quadruple(TFirst first, TSecond second, TThird third, TFourth fourth) + { + First = first; + Second = second; + Third = third; + Fourth = fourth; + } + + public override string ToString() + { + return $"({First}, {Second}, {Third}, {Fourth})"; + } + + public static Quadruple FromQuaternion(Quaternion quaternion) { - /// - /// Gets the first value - /// - public TFirst First { get; } - - /// - /// Gets the second value - /// - public TSecond Second { get; } - - /// - /// Gets the third value - /// - public TThird Third { get; } - - /// - /// Gets the fourth value - /// - public TFourth Fourth { get; } - - public Quadruple(TFirst first, TSecond second, TThird third, TFourth fourth) - { - First = first; - Second = second; - Third = third; - Fourth = fourth; - } - - public override string ToString() => $"({First}, {Second}, {Third}, {Fourth})"; - - public static Quadruple FromQuaternion(UnityEngine.Quaternion quaternion) => new(quaternion.x, quaternion.y, quaternion.z, quaternion.w); + return new Quadruple(quaternion.x, quaternion.y, quaternion.z, quaternion.w); } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/API/Struct/Triplet.cs b/UncomplicatedCustomRoles/API/Struct/Triplet.cs index ec4990f..dbeae48 100644 --- a/UncomplicatedCustomRoles/API/Struct/Triplet.cs +++ b/UncomplicatedCustomRoles/API/Struct/Triplet.cs @@ -1,51 +1,57 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . */ using System.Text.Json.Serialization; +using UnityEngine; -namespace UncomplicatedCustomRoles.API.Struct +namespace UncomplicatedCustomRoles.API.Struct; + +public readonly struct Triplet { - public readonly struct Triplet + /// + /// Gets the first value + /// + public TFirst First { get; } + + /// + /// Gets the second value + /// + public TSecond Second { get; } + + /// + /// Gets the third value + /// + public TThird Third { get; } + + [JsonConstructor] + public Triplet(TFirst first, TSecond second, TThird third) + { + First = first; + Second = second; + Third = third; + } + + public Triplet(Triplet clone) + { + First = clone.First; + Second = clone.Second; + Third = clone.Third; + } + + public override string ToString() + { + return $"({First}, {Second}, {Third})"; + } + + public static Triplet FromVector3(Vector3 vector) { - /// - /// Gets the first value - /// - public TFirst First { get; } - - /// - /// Gets the second value - /// - public TSecond Second { get; } - - /// - /// Gets the third value - /// - public TThird Third { get; } - - [JsonConstructor] - public Triplet(TFirst first, TSecond second, TThird third) - { - First = first; - Second = second; - Third = third; - } - - public Triplet(Triplet clone) - { - First = clone.First; - Second = clone.Second; - Third = clone.Third; - } - - public override string ToString() => $"({First}, {Second}, {Third})"; - - public static Triplet FromVector3(UnityEngine.Vector3 vector) => new(vector.x, vector.y, vector.z); + return new Triplet(vector.x, vector.y, vector.z); } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Commands/CommandParent.cs b/UncomplicatedCustomRoles/Commands/CommandParent.cs index fca0cae..9a37404 100644 --- a/UncomplicatedCustomRoles/Commands/CommandParent.cs +++ b/UncomplicatedCustomRoles/Commands/CommandParent.cs @@ -1,94 +1,93 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . */ -using CommandSystem; using System; using System.Collections.Generic; using System.Linq; +using CommandSystem; using LabApi.Features.Permissions; using UncomplicatedCustomRoles.API.Interfaces; -using UncomplicatedCustomRoles.Manager; using UncomplicatedCustomRoles.Extensions; +using UncomplicatedCustomRoles.Manager; + +namespace UncomplicatedCustomRoles.Commands; -namespace UncomplicatedCustomRoles.Commands +[CommandHandler(typeof(RemoteAdminCommandHandler))] +internal class CommandParent : ParentCommand { - [CommandHandler(typeof(RemoteAdminCommandHandler))] - internal class CommandParent : ParentCommand + public CommandParent() { - public CommandParent() => LoadGeneratedCommands(); + LoadGeneratedCommands(); + } + + public override string Command { get; } = "ucr"; + + public override string[] Aliases { get; } = []; - public override string Command { get; } = "ucr"; + public override string Description { get; } = "Manage the UCR features"; - public override string[] Aliases { get; } = new string[] { }; + public List RegisteredCommands { get; } = []; - public override string Description { get; } = "Manage the UCR features"; + public override void LoadGeneratedCommands() + { + RegisteredCommands.Add(new List()); + RegisteredCommands.Add(new Info()); + RegisteredCommands.Add(new Role()); + RegisteredCommands.Add(new Spawn()); + RegisteredCommands.Add(new CustomInfo()); + RegisteredCommands.Add(new Reload()); + RegisteredCommands.Add(new SpawnPoint()); + RegisteredCommands.Add(new Percentages()); + RegisteredCommands.Add(new Errors()); + RegisteredCommands.Add(new Generate()); + RegisteredCommands.Add(new Update()); + RegisteredCommands.Add(new Owner()); + RegisteredCommands.Add(new Version()); + RegisteredCommands.Add(new Debug()); + } - public override void LoadGeneratedCommands() + protected override bool ExecuteParent(ArraySegment arguments, ICommandSender sender, out string response) + { + if (!arguments.Any()) { - RegisteredCommands.Add(new List()); - RegisteredCommands.Add(new Info()); - RegisteredCommands.Add(new Role()); - RegisteredCommands.Add(new Spawn()); - RegisteredCommands.Add(new CustomInfo()); - RegisteredCommands.Add(new Reload()); - RegisteredCommands.Add(new SpawnPoint()); - RegisteredCommands.Add(new Percentages()); - RegisteredCommands.Add(new Errors()); - RegisteredCommands.Add(new Generate()); - RegisteredCommands.Add(new Update()); - RegisteredCommands.Add(new Owner()); - RegisteredCommands.Add(new Version()); - RegisteredCommands.Add(new Debug()); - } + // Help page + response = + $"\n>> UncomplicatedCustomRoles v{Plugin.Instance.Version}{(VersionManager.VersionInfo?.CustomName is not null ? $" '{VersionManager.VersionInfo.CustomName}'" : string.Empty)} <<\nby {Plugin.Instance.Author}\n\nAvailable commands:"; + + foreach (var Command in RegisteredCommands) + response += $"\n• ucr {Command.Name.GenerateWithBuffer(12)} → {Command.Description}"; - public List RegisteredCommands { get; } = new(); + response += "\nOwO"; + + return true; + } - protected override bool ExecuteParent(ArraySegment arguments, ICommandSender sender, out string response) { - if (!arguments.Any()) - { - // Help page - response = $"\n>> UncomplicatedCustomRoles v{Plugin.Instance.Version}{(VersionManager.VersionInfo?.CustomName is not null ? $" '{VersionManager.VersionInfo.CustomName}'" : string.Empty)} <<\nby {Plugin.Instance.Author}\n\nAvailable commands:"; - - foreach (IUCRCommand Command in RegisteredCommands) - response += $"\n• ucr {Command.Name.GenerateWithBuffer(12)} → {Command.Description}"; - - response += "\nOwO"; - - return true; - } - else - { - // Arguments compactor: - List Arguments = new(); - foreach (string Argument in arguments.Where(arg => arg != arguments.At(0))) - { - Arguments.Add(Argument); - } + var Arguments = arguments.Skip(1).ToList(); - IUCRCommand Command = RegisteredCommands.FirstOrDefault(command => command.Name == arguments.At(0)); + var Command = RegisteredCommands.FirstOrDefault(command => command.Name == arguments.At(0)); - if (Command is not null) - if (sender.HasPermissions(Command.RequiredPermission)) - return Command.Executor(Arguments, sender, out response); - else - { - response = $"You don't have enough permission(s) to execute that command!\nNeeded: {Command.RequiredPermission}"; - return false; - } + if (Command is not null) + if (sender.HasPermissions(Command.RequiredPermission)) + { + return Command.Executor(Arguments, sender, out response); + } else { - response = "Command not found!"; + response = + $"You don't have enough permission(s) to execute that command!\nNeeded: {Command.RequiredPermission}"; return false; } - } + + response = "Command not found!"; + return false; } } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Commands/CustomInfo.cs b/UncomplicatedCustomRoles/Commands/CustomInfo.cs index 9a95451..ccb1835 100644 --- a/UncomplicatedCustomRoles/Commands/CustomInfo.cs +++ b/UncomplicatedCustomRoles/Commands/CustomInfo.cs @@ -1,71 +1,71 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . */ +using System.Collections.Generic; using CommandSystem; using LabApi.Features.Wrappers; -using System.Collections.Generic; -using UncomplicatedCustomRoles.API.Features; using UncomplicatedCustomRoles.API.Interfaces; using UncomplicatedCustomRoles.Extensions; using UncomplicatedCustomRoles.Manager; -namespace UncomplicatedCustomRoles.Commands +namespace UncomplicatedCustomRoles.Commands; + +internal class CustomInfo : IUCRCommand { - internal class CustomInfo : IUCRCommand - { - public string Name => "cinfo"; + public string Name => "cinfo"; - public string Description => "Handle the Custom Role's Custom Info"; + public string Description => "Handle the Custom Role's Custom Info"; - public string RequiredPermission => "ucr.cinfo"; + public string RequiredPermission => "ucr.cinfo"; - public bool Executor(List arguments, ICommandSender sender, out string response) + public bool Executor(List arguments, ICommandSender sender, out string response) + { + if (arguments.Count < 3) { - if (arguments.Count < 3) - { - response = $"To execute this command provide at least 1 argument!\nUsage: ucr cinfo (content)"; - return false; - } - - if (!Player.TryGet(arguments[0], out Player player)) - { - response = "Cannot find player! Check the Player ID and try again!"; - return false; - } + response = + "To execute this command provide at least 1 argument!\nUsage: ucr cinfo (content)"; + return false; + } - if (!player.TryGetSummonedInstance(out SummonedCustomRole summonedInstance)) - { - response = $"Player {player.PlayerId} is not a Custom Role!"; - return false; - } + if (!Player.TryGet(arguments[0], out var player)) + { + response = "Cannot find player! Check the Player ID and try again!"; + return false; + } - string content = PlaceholderManager.ApplyPlaceholders(string.Join(" ", arguments.GetRange(2, arguments.Count - 2)), player, summonedInstance.Role); + if (!player.TryGetSummonedInstance(out var summonedInstance)) + { + response = $"Player {player.PlayerId} is not a Custom Role!"; + return false; + } - switch (arguments[1]) - { - case "nick": - summonedInstance.CustomInfo.Nickname = content; - break; - case "role": - summonedInstance.CustomInfo.Role = content; - break; - case "info": - summonedInstance.CustomInfo.Info = content; - break; - default: - response = $"Invalid field! Valid fields are: nick, role and info!"; - return false; - } + var content = PlaceholderManager.ApplyPlaceholders(string.Join(" ", arguments.GetRange(2, arguments.Count - 2)), + player, summonedInstance.Role); - response = $"Successfully updated CustomInfo of player {player.PlayerId} ({player.Nickname})!"; - return true; + switch (arguments[1]) + { + case "nick": + summonedInstance.CustomInfo.Nickname = content; + break; + case "role": + summonedInstance.CustomInfo.Role = content; + break; + case "info": + summonedInstance.CustomInfo.Info = content; + break; + default: + response = "Invalid field! Valid fields are: nick, role and info!"; + return false; } + + response = $"Successfully updated CustomInfo of player {player.PlayerId} ({player.Nickname})!"; + return true; } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Commands/Debug.cs b/UncomplicatedCustomRoles/Commands/Debug.cs index a09d70f..5fc6bf9 100644 --- a/UncomplicatedCustomRoles/Commands/Debug.cs +++ b/UncomplicatedCustomRoles/Commands/Debug.cs @@ -1,142 +1,148 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . */ -using CommandSystem; -using LabApi.Features.Wrappers; using System; using System.Collections.Generic; using System.Linq; using System.Reflection; -using UncomplicatedCustomRoles.API.Features; +using CommandSystem; +using LabApi.Features.Wrappers; using UncomplicatedCustomRoles.API.Interfaces; using UncomplicatedCustomRoles.Extensions; -namespace UncomplicatedCustomRoles.Commands +namespace UncomplicatedCustomRoles.Commands; + +public class Debug : IUCRCommand { - public class Debug : IUCRCommand - { - public string Name { get; } = "debug"; + private object ReferenceObject { get; set; } + public string Name { get; } = "debug"; - public string Description { get; } = "Debug the plugin by using some specific code"; + public string Description { get; } = "Debug the plugin by using some specific code"; - public string RequiredPermission { get; } = "ucr.debug"; + public string RequiredPermission { get; } = "ucr.debug"; - private object ReferenceObject { get; set; } = null; + public bool Executor(List args, ICommandSender sender, out string response) + { + response = null; - public bool Executor(List args, ICommandSender sender, out string response) + if (args.Count < 3) { - response = null; - - if (args.Count < 3) - { - response = "Usage: ucr debug [saveToRef?]"; - return false; - } - - object obj = null; - Type target = args[0] is "static" ? Plugin.Assembly.GetType(args[1]) : null; - - if (target is null && args[0] is "static") - { - response = $"Failed to start debug: Location '{args[1]}' is not valid!"; - return false; - } - - Player player = Player.Get(sender); - - if (args[0] is "plugin" or "pl") - obj = Plugin.Instance; - if (args[0] is "current_player_scr" or "cp_scr" && player is not null) - obj = player.GetSummonedInstance(); - else if (args[0] is "current_player" or "cp" && player is not null) - obj = player; - else if (args[0].StartsWith("player_") && int.TryParse(args[0].Replace("player_", string.Empty), out int id) && Player.TryGet(id, out Player player3)) - obj = player3; - else if (args[0].StartsWith("player_scr_") && int.TryParse(args[0].Replace("player_scr_", string.Empty), out int id2) && Player.TryGet(id2, out Player player4)) - obj = player4.GetSummonedInstance(); - else if (args[0].StartsWith("current_player_cm_") && player.TryGetSummonedInstance(out SummonedCustomRole role) && role.CustomModules.FirstOrDefault(cm => cm.Name == args[0].Replace("current_player_cm_", "")) is not null) - obj = role.CustomModules.FirstOrDefault(cm => cm.Name == args[0].Replace("current_player_cm_", "")); - else if (args[0] is "ref" or "reference") - obj = ReferenceObject; - - if (obj is null && args[0] is not "static") - { - response = $"Failed to start debug: Zone {args[0]} not found!"; - return false; - } - - if (args.Count is 4 && args[3] is "true" && args[0] is not ("ref" or "reference")) - ReferenceObject = obj; - - if (obj is not null && target is not null && obj.GetType() != target) - { - response = $"Failed to start debug: Given target of debug {target.FullName} is not equal to the found object {obj.GetType().FullName}!"; - return false; - } - - if (obj is null && target is null) - { - response = $"Failed to start debug: Both target and object cannot be null!\nIn order to start the debug of a static element, PLEASE give the target (Location) of it!"; - return false; - } - - target ??= obj.GetType(); - - if (HandleProperty(obj, target, args[2], out string value)) - response = $"Required value is:\n{value}"; - else if (HandleField(obj, target, args[2], out value)) - response = $"Required value is:\n{value}"; - - if (response is not null) - return true; - - response = $"Failed to find value: Element at {target.FullName}.{args[2]} not found!"; + response = "Usage: ucr debug [saveToRef?]"; return false; } - private bool HandleProperty(object obj, Type target, string name, out string value) + object obj = null; + var target = args[0] is "static" ? Plugin.Assembly.GetType(args[1]) : null; + + if (target is null && args[0] is "static") { - PropertyInfo property; + response = $"Failed to start debug: Location '{args[1]}' is not valid!"; + return false; + } - if (obj is null) - property = target.GetProperties(BindingFlags.Static).FirstOrDefault(p => p.Name == name && p.CanRead); - else - property = target.GetProperties().FirstOrDefault(p => p.Name == name && p.CanRead); + var player = Player.Get(sender); + + if (args[0] is "plugin" or "pl") + obj = Plugin.Instance; + if (args[0] is "current_player_scr" or "cp_scr" && player is not null) + obj = player.GetSummonedInstance(); + else if (args[0] is "current_player" or "cp" && player is not null) + obj = player; + else if (args[0].StartsWith("player_") && int.TryParse(args[0].Replace("player_", string.Empty), out var id) && + Player.TryGet(id, out var player3)) + obj = player3; + else if (args[0].StartsWith("player_scr_") && + int.TryParse(args[0].Replace("player_scr_", string.Empty), out var id2) && + Player.TryGet(id2, out var player4)) + obj = player4.GetSummonedInstance(); + else if (args[0].StartsWith("current_player_cm_") && player is not null && + player.TryGetSummonedInstance(out var role) && + role.CustomModules.FirstOrDefault(cm => cm.Name == args[0].Replace("current_player_cm_", "")) is not + null) + obj = role.CustomModules.FirstOrDefault(cm => cm.Name == args[0].Replace("current_player_cm_", "")); + else if (args[0] is "ref" or "reference") + obj = ReferenceObject; + + if (obj is null && args[0] is not "static") + { + response = $"Failed to start debug: Zone {args[0]} not found!"; + return false; + } - if (property is null) - { - value = null; - return false; - } + if (args.Count is 4 && args[3] is "true" && args[0] is not ("ref" or "reference")) + ReferenceObject = obj; - value = property.GetValue(obj).ToString(); - return true; + if (obj is not null && target is not null && obj.GetType() != target) + { + response = + $"Failed to start debug: Given target of debug {target.FullName} is not equal to the found object {obj.GetType().FullName}!"; + return false; } - private bool HandleField(object obj, Type target, string name, out string value) + if (obj is null && target is null) { - FieldInfo field; + response = + "Failed to start debug: Both target and object cannot be null!\nIn order to start the debug of a static element, PLEASE give the target (Location) of it!"; + return false; + } - if (obj is null) - field = target.GetFields().FirstOrDefault(f => f.Name == name && f.IsStatic); - else - field = target.GetFields().FirstOrDefault(f => f.Name == name); + target ??= obj.GetType(); - if (field is null) - { - value = null; - return false; - } + if (HandleProperty(obj, target, args[2], out var value)) + response = $"Required value is:\n{value}"; + else if (HandleField(obj, target, args[2], out value)) + response = $"Required value is:\n{value}"; - value = field.GetValue(obj).ToString(); + if (response is not null) return true; + + response = $"Failed to find value: Element at {target.FullName}.{args[2]} not found!"; + return false; + } + + private bool HandleProperty(object obj, Type target, string name, out string value) + { + PropertyInfo property; + + if (obj is null) + property = target.GetProperties(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic) + .FirstOrDefault(p => p.Name == name && p.CanRead); + else + property = target.GetProperties().FirstOrDefault(p => p.Name == name && p.CanRead); + + if (property is null) + { + value = null; + return false; + } + + value = property.GetValue(obj)?.ToString() ?? "null"; + return true; + } + + private bool HandleField(object obj, Type target, string name, out string value) + { + FieldInfo field; + + if (obj is null) + field = target.GetFields().FirstOrDefault(f => f.Name == name && f.IsStatic); + else + field = target.GetFields().FirstOrDefault(f => f.Name == name); + + if (field is null) + { + value = null; + return false; } + + value = field.GetValue(obj)?.ToString() ?? "null"; + return true; } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Commands/Errors.cs b/UncomplicatedCustomRoles/Commands/Errors.cs index c3cac54..fa74d78 100644 --- a/UncomplicatedCustomRoles/Commands/Errors.cs +++ b/UncomplicatedCustomRoles/Commands/Errors.cs @@ -1,102 +1,106 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . */ -using CommandSystem; using System.Collections.Generic; using System.IO; +using CommandSystem; using UncomplicatedCustomRoles.API.Features; using UncomplicatedCustomRoles.API.Interfaces; -using UncomplicatedCustomRoles.Compatibility; using YamlDotNet.Core; -namespace UncomplicatedCustomRoles.Commands +namespace UncomplicatedCustomRoles.Commands; + +public class Errors : IUCRCommand { - public class Errors : IUCRCommand - { - public string Name { get; } = "errors"; + public string Name { get; } = "errors"; - public string Description { get; } = "See every YAML error of every not loaded CustomRole"; + public string Description { get; } = "See every YAML error of every not loaded CustomRole"; - public string RequiredPermission { get; } = "ucr.errors"; + public string RequiredPermission { get; } = "ucr.errors"; - public bool Executor(List arguments, ICommandSender sender, out string response) + public bool Executor(List arguments, ICommandSender sender, out string response) + { + if (CustomRole.NotLoadedRoles.Count is 0) { - if (CustomRole.NotLoadedRoles.Count is 0) - { - response = "No CustomRoles with errors were found!\nYey :3"; - return true; - } - - response = string.Empty; + response = "No CustomRoles with errors were found!\nYey :3"; + return true; + } - foreach (ErrorCustomRole errorCustomRole in CustomRole.NotLoadedRoles) - { - response += $"\n📄 File: {Path.GetFileName(errorCustomRole.Path)}"; + response = string.Empty; - if (errorCustomRole.Exception is YamlException yamlException) - response += $"\n🔢 Line: {yamlException.Start.Line}, Column: {yamlException.Start.Column}"; + foreach (var errorCustomRole in CustomRole.NotLoadedRoles) + { + response += $"\n📄 File: {Path.GetFileName(errorCustomRole.Path)}"; - response += $"\n❌ Error: {errorCustomRole.Message}"; - response += $"\n💡 Suggestion: {(errorCustomRole.Exception is not null && errorCustomRole.Exception.Message is not null ? GetSuggestionFromMessage(errorCustomRole.Exception.Message) : string.Empty)}\n"; - } + if (errorCustomRole.Exception is YamlException yamlException) + response += + $"\n🔢 Line: {yamlException.Start.Line}, Column: {yamlException.Start.Column}"; - return true; + response += $"\n❌ Error: {errorCustomRole.Message}"; + response += + $"\n💡 Suggestion: {(errorCustomRole.Exception is not null && errorCustomRole.Exception.Message is not null ? GetSuggestionFromMessage(errorCustomRole.Exception.Message) : string.Empty)}\n"; } - private static string GetSuggestionFromMessage(string message) - { - message = message.ToLowerInvariant(); + return true; + } - if (message.Contains("mapping values are not allowed")) - return "Make sure there is a space after the colon (e.g., `name: GOC` instead of `name:GOC`)."; + private static string GetSuggestionFromMessage(string message) + { + message = message.ToLowerInvariant(); - if (message.Contains("expected 'mappingstart', got 'sequencestart'")) - return "Your YAML file begins with a list (`- item`) but should begin with a mapping. Try adding a top-level key like `teams:` before your list."; + if (message.Contains("mapping values are not allowed")) + return "Make sure there is a space after the colon (e.g., `name: GOC` instead of `name:GOC`)."; - if (message.Contains("while parsing a block mapping")) - return "Check indentation and YAML structure — something might be misaligned or nested incorrectly."; + if (message.Contains("expected 'mappingstart', got 'sequencestart'")) + return + "Your YAML file begins with a list (`- item`) but should begin with a mapping. Try adding a top-level key like `teams:` before your list."; - if (message.Contains("expected , but found")) - return "Possibly missing a `-` for a list item or the element ends prematurely."; + if (message.Contains("while parsing a block mapping")) + return "Check indentation and YAML structure — something might be misaligned or nested incorrectly."; - if (message.Contains("did not find expected key")) - return "A key may be missing or misaligned — ensure all keys are followed by colons and correctly indented."; + if (message.Contains("expected , but found")) + return "Possibly missing a `-` for a list item or the element ends prematurely."; - if (message.Contains("unexpected end of stream")) - return "The file might be cut off unexpectedly — check for missing closing brackets or incomplete blocks."; + if (message.Contains("did not find expected key")) + return + "A key may be missing or misaligned — ensure all keys are followed by colons and correctly indented."; - if (message.Contains("duplicate key")) - return "You may have defined the same key twice in the same block — YAML requires keys to be unique."; + if (message.Contains("unexpected end of stream")) + return "The file might be cut off unexpectedly — check for missing closing brackets or incomplete blocks."; - if (message.Contains("found character that cannot start any token")) - return "There's probably an illegal character or wrong symbol — double-check for stray tabs or weird characters."; + if (message.Contains("duplicate key")) + return "You may have defined the same key twice in the same block — YAML requires keys to be unique."; - if (message.Contains("found unexpected ':'")) - return "There might be a colon `:` in a value that should be quoted — try wrapping the value in quotes."; + if (message.Contains("found character that cannot start any token")) + return + "There's probably an illegal character or wrong symbol — double-check for stray tabs or weird characters."; - if (message.Contains("anchor") && message.Contains("not defined")) - return "You're referencing an anchor (&value or *value) that hasn't been defined."; + if (message.Contains("found unexpected ':'")) + return "There might be a colon `:` in a value that should be quoted — try wrapping the value in quotes."; - if (message.Contains("alias") && message.Contains("not found")) - return "YAML alias (*) points to something that doesn't exist — check spelling or anchor placement."; + if (message.Contains("anchor") && message.Contains("not defined")) + return "You're referencing an anchor (&value or *value) that hasn't been defined."; - if (message.Contains("cannot convert") && message.Contains("to")) - return "A value might be of the wrong type — make sure it's in the correct format (e.g., number vs string)."; + if (message.Contains("alias") && message.Contains("not found")) + return "YAML alias (*) points to something that doesn't exist — check spelling or anchor placement."; - if (message.Contains("sequence entries are not allowed here")) - return "You're probably using a list (`- item`) in an invalid place — check indentation and nesting."; + if (message.Contains("cannot convert") && message.Contains("to")) + return + "A value might be of the wrong type — make sure it's in the correct format (e.g., number vs string)."; - if (message.Contains("unexpected key") || message.Contains("unexpected property")) - return "This key may be misplaced or invalid — double-check your schema or property names."; + if (message.Contains("sequence entries are not allowed here")) + return "You're probably using a list (`- item`) in an invalid place — check indentation and nesting."; - return "Check your YAML syntax near this location. Be sure indentation, colons, and types are correct."; - } + if (message.Contains("unexpected key") || message.Contains("unexpected property")) + return "This key may be misplaced or invalid — double-check your schema or property names."; + + return "Check your YAML syntax near this location. Be sure indentation, colons, and types are correct."; } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Commands/Generate.cs b/UncomplicatedCustomRoles/Commands/Generate.cs index 9bc8695..dad8e5d 100644 --- a/UncomplicatedCustomRoles/Commands/Generate.cs +++ b/UncomplicatedCustomRoles/Commands/Generate.cs @@ -1,51 +1,62 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . */ -using CommandSystem; -using LabApi.Loader.Features.Yaml; using System.Collections.Generic; using System.IO; +using CommandSystem; +using LabApi.Loader.Features.Yaml; using UncomplicatedCustomRoles.API.Features; using UncomplicatedCustomRoles.API.Interfaces; using UncomplicatedCustomRoles.Manager; -namespace UncomplicatedCustomRoles.Commands +namespace UncomplicatedCustomRoles.Commands; + +internal class Generate : IUCRCommand { - internal class Generate : IUCRCommand - { - public string Name { get; } = "generate"; + public string Name { get; } = "generate"; - public string Description { get; } = "Generate another default Custom Role inside a given file, creating it"; + public string Description { get; } = "Generate another default Custom Role inside a given file, creating it"; - public string RequiredPermission { get; } = "ucr.generate"; + public string RequiredPermission { get; } = "ucr.generate"; - public bool Executor(List arguments, ICommandSender sender, out string response) + public bool Executor(List arguments, ICommandSender sender, out string response) + { + if (arguments.Count == 0) { - if (arguments.Count == 0) + response = "Unexpected number of args!\nUsage: ucr generate (FileName) (Server-port)"; + return false; + } + + var port = -1; + if (arguments.Count == 2) + { + if (!uint.TryParse(arguments[1], out var parsedPort)) { - response = "Unexpected number of args!\nUsage: ucr generate (FileName) (Server-port)"; + response = + $"'{arguments[1]}' is not a valid server port!\nUsage: ucr generate (FileName) (Server-port)"; return false; } - int port = -1; - if (arguments.Count == 2) - port = (int)uint.Parse(arguments[1]); + port = (int)parsedPort; + } - string path = FileConfigs.Dir; - if (port > 0) - path = Path.Combine(path, port.ToString()); + var path = FileConfigs.Dir; + if (port > 0) + path = Path.Combine(path, port.ToString()); - File.WriteAllText(Path.Combine(path, $"{arguments[0].Replace(".yml", "")}.yml"), YamlConfigParser.Serializer.Serialize(new CustomRole())); + Directory.CreateDirectory(path); - response = $"New default role generated at {path} but has not been loaded!"; - return true; - } + File.WriteAllText(Path.Combine(path, $"{arguments[0].Replace(".yml", "")}.yml"), + YamlConfigParser.Serializer.Serialize(new CustomRole())); + + response = $"New default role generated at {path} but has not been loaded!"; + return true; } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Commands/Info.cs b/UncomplicatedCustomRoles/Commands/Info.cs index d525d96..a8e9fe5 100644 --- a/UncomplicatedCustomRoles/Commands/Info.cs +++ b/UncomplicatedCustomRoles/Commands/Info.cs @@ -1,95 +1,99 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . */ -using CommandSystem; using System.Collections.Generic; -using MapGeneration; +using CommandSystem; using UncomplicatedCustomRoles.API.Enums; using UncomplicatedCustomRoles.API.Features; using UncomplicatedCustomRoles.API.Interfaces; using UncomplicatedCustomRoles.Extensions; using UncomplicatedCustomRoles.Manager; -namespace UncomplicatedCustomRoles.Commands +namespace UncomplicatedCustomRoles.Commands; + +public class Info : IUCRCommand { - public class Info : IUCRCommand - { - public string Name { get; } = "info"; + public string Name { get; } = "info"; - public string Description { get; } = "View info about a specific Custom Role"; + public string Description { get; } = "View info about a specific Custom Role"; - public string RequiredPermission { get; } = "ucr.info"; + public string RequiredPermission { get; } = "ucr.info"; - public bool Executor(List arguments, ICommandSender sender, out string response) + public bool Executor(List arguments, ICommandSender sender, out string response) + { + if (arguments.Count != 1) { - if (arguments.Count != 1) - { - response = "Usage: ucr info "; - return false; - } + response = "Usage: ucr info "; + return false; + } - if (!int.TryParse(arguments[0], out int id) || !CustomRole.TryGet(id, out ICustomRole role)) - { - response = $"Custom Role {arguments[0]} not found!"; - return false; - } + if (!int.TryParse(arguments[0], out var id) || !CustomRole.TryGet(id, out var role)) + { + response = $"Custom Role {arguments[0]} not found!"; + return false; + } - response = $"{role.Name}"; + response = $"{role.Name}"; - response += BuildInfo(role); + response += BuildInfo(role); - response += $"\nOwO"; + response += "\nOwO"; - return true; - } + return true; + } - public static string BuildInfo(ICustomRole role) + public static string BuildInfo(ICustomRole role) + { + Dictionary data = new() { - Dictionary data = new() + { "🔢 Id:", $"{role.Id}" }, + { "👤 Role:", $"{role.Role}" }, { - { "🔢 Id:", $"{role.Id}" }, - { "👤 Role:", $"{role.Role}" }, - { "💳 Badge:", $"{(role.BadgeName != null ? role.BadgeName.Replace("@hidden", string.Empty) : string.Empty)}{(role.BadgeName != null && role.BadgeName.EndsWith("@hidden") ? " [HIDDEN]" : string.Empty)}" }, - { "❤️ Health:", $"{role?.Health.Amount ?? 0}/{role?.Health.Maximum ?? 0}" }, - { "💉 AHP:", $"{role?.Ahp.Amount ?? 0}/{role?.Ahp.Limit ?? 0}" }, - { "🏃 Can escape:", $"{(role.CanEscape ? "true" : "false")}" }, - { "🎒 Inventory:", string.Join(", ", role?.Inventory ?? new List()) }, - { "🚗 Spawn type:", $"{(role.SpawnSettings != null ? role.SpawnSettings.Spawn.ToString() : "N/A")}" } - }; - - string response = string.Empty; - - if (role.SpawnSettings != null) + "💳 Badge:", + $"{(role.BadgeName != null ? role.BadgeName.Replace("@hidden", string.Empty) : string.Empty)}{(role.BadgeName != null && role.BadgeName.EndsWith("@hidden") ? " [HIDDEN]" : string.Empty)}" + }, + { "❤️ Health:", $"{role?.Health.Amount ?? 0}/{role?.Health.Maximum ?? 0}" }, + { "💉 AHP:", $"{role?.Ahp.Amount ?? 0}/{role?.Ahp.Limit ?? 0}" }, + { "🏃 Can escape:", $"{(role.CanEscape ? "true" : "false")}" }, + { "🎒 Inventory:", string.Join(", ", role?.Inventory ?? []) }, { - if (role.SpawnSettings.Spawn is SpawnType.RoomsSpawn) - data.Add("🚪 Spawn rooms:", - string.Join(", ", role?.SpawnSettings?.SpawnRooms ?? new List())); - else if (role.SpawnSettings.Spawn is SpawnType.ZoneSpawn) - data.Add("🚪 Spawn zones:", - string.Join(", ", role?.SpawnSettings?.SpawnZones ?? new List())); - else if (role.SpawnSettings.Spawn is SpawnType.SpawnPointSpawn) - data.Add("🚪 Spawn points:", - string.Join(", ", role?.SpawnSettings?.SpawnPoints ?? new List())); + "🚗 Spawn type:", + $"{(role.SpawnSettings != null ? role.SpawnSettings.Spawn.ToString() : "N/A")}" } + }; - if (role.CustomFlags is { Count: > 0 }) - { - var decodedFlags = YamlFlagsHandler.Decode(role.CustomFlags); - if (decodedFlags != null) - data.Add("🧩 Custom flags:", string.Join(", ", decodedFlags.Keys)); - } + var response = string.Empty; - foreach (KeyValuePair kvp in data) - response += $"\n{kvp.Key.GenerateWithBuffer(40)} {kvp.Value}"; + if (role.SpawnSettings != null) + { + if (role.SpawnSettings.Spawn is SpawnType.RoomsSpawn) + data.Add("🚪 Spawn rooms:", + string.Join(", ", role.SpawnSettings?.SpawnRooms ?? [])); + else if (role.SpawnSettings.Spawn is SpawnType.ZoneSpawn) + data.Add("🚪 Spawn zones:", + string.Join(", ", role.SpawnSettings?.SpawnZones ?? [])); + else if (role.SpawnSettings.Spawn is SpawnType.SpawnPointSpawn) + data.Add("🚪 Spawn points:", + string.Join(", ", role.SpawnSettings?.SpawnPoints ?? [])); + } - return response; + if (role.CustomFlags is { Count: > 0 }) + { + var decodedFlags = YamlFlagsHandler.Decode(role.CustomFlags); + if (decodedFlags != null) + data.Add("🧩 Custom flags:", string.Join(", ", decodedFlags.Keys)); } + + foreach (var kvp in data) + response += $"\n{kvp.Key.GenerateWithBuffer(40)} {kvp.Value}"; + + return response; } } \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Commands/List.cs b/UncomplicatedCustomRoles/Commands/List.cs index 4338266..8b4cc24 100644 --- a/UncomplicatedCustomRoles/Commands/List.cs +++ b/UncomplicatedCustomRoles/Commands/List.cs @@ -1,60 +1,64 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . */ -using CommandSystem; using System.Collections.Generic; using System.Linq; +using CommandSystem; using UncomplicatedCustomRoles.API.Features; using UncomplicatedCustomRoles.API.Interfaces; -using UncomplicatedCustomRoles.Compatibility; using UncomplicatedCustomRoles.Extensions; -namespace UncomplicatedCustomRoles.Commands +namespace UncomplicatedCustomRoles.Commands; + +public class List : IUCRCommand { - public class List : IUCRCommand - { - public string Name { get; } = "list"; + public string Name { get; } = "list"; - public string Description { get; } = "List all registered custom roles"; + public string Description { get; } = "List all registered custom roles"; - public string RequiredPermission { get; } = "ucr.list"; + public string RequiredPermission { get; } = "ucr.list"; - public bool Executor(List arguments, ICommandSender sender, out string response) - { - List> list = CustomRole.CustomRoles.ToList(); - if (arguments.Count > 0 && arguments[0].Length > 1) - list = list.Where(r => r.Value.Name.ToLower().Contains(arguments[0].ToLower())).ToList(); + public bool Executor(List arguments, ICommandSender sender, out string response) + { + var list = CustomRole.CustomRoles.ToList(); + if (arguments.Count > 0 && arguments[0].Length > 1) + list = list.Where(r => r.Value.Name.ToLower().Contains(arguments[0].ToLower())).ToList(); - response = "List of all registered CustomRoles:"; + response = "List of all registered CustomRoles:"; - foreach (KeyValuePair kvp in list) - if (kvp.Value is not null) - if (CustomRole.OutdatedRoles.FirstOrDefault(r => r.CustomRole.Id == kvp.Key) is not null) - response += $"\n✔ [{kvp.Key}] {kvp.Value?.Name}"; - else - response += $"\n✔ [{kvp.Key}] {kvp.Value?.Name}"; + foreach (var kvp in list) + if (kvp.Value is not null) + if (CustomRole.OutdatedRoles.FirstOrDefault(r => r.CustomRole.Id == kvp.Key) is not null) + response += + $"\n✔ [{kvp.Key}] {kvp.Value?.Name}"; + else + response += + $"\n✔ [{kvp.Key}] {kvp.Value?.Name}"; - foreach (ErrorCustomRole errorCustomRole in CustomRole.NotLoadedRoles) - response += $"\n❌ [{errorCustomRole?.Id}] {errorCustomRole?.Name}"; + foreach (var errorCustomRole in CustomRole.NotLoadedRoles) + response += + $"\n❌ [{errorCustomRole?.Id}] {errorCustomRole?.Name}"; - response += $"\n\n🔢 Showing {list.Count} of {CustomRole.CustomRoles.Count} CustomRoles"; + response += + $"\n\n🔢 Showing {list.Count} of {CustomRole.CustomRoles.Count} CustomRoles"; - if (CustomRole.OutdatedRoles.Count > 0) - response += $"\n⚠️ There {(CustomRole.OutdatedRoles.Count > 1 ? "are" : "is")} {CustomRole.OutdatedRoles.Count} CustomRole{(CustomRole.OutdatedRoles.Count > 1 ? "s" : string.Empty)} that are made for a previous version of the plugin!"; + if (CustomRole.OutdatedRoles.Count > 0) + response += + $"\n⚠️ There {(CustomRole.OutdatedRoles.Count > 1 ? "are" : "is")} {CustomRole.OutdatedRoles.Count} CustomRole{(CustomRole.OutdatedRoles.Count > 1 ? "s" : string.Empty)} that are made for a previous version of the plugin!"; - if (CustomRole.NotLoadedRoles.Count > 0) - response += $"\n❗ There {(CustomRole.NotLoadedRoles.Count > 1 ? "are" : "is")} {CustomRole.NotLoadedRoles.Count} CustomRole{(CustomRole.NotLoadedRoles.Count > 1 ? "s" : string.Empty)} not loaded!"; + if (CustomRole.NotLoadedRoles.Count > 0) + response += + $"\n❗ There {(CustomRole.NotLoadedRoles.Count > 1 ? "are" : "is")} {CustomRole.NotLoadedRoles.Count} CustomRole{(CustomRole.NotLoadedRoles.Count > 1 ? "s" : string.Empty)} not loaded!"; - response += "\nOwO"; + response += "\nOwO"; - return true; - } + return true; } } \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Commands/LogShare.cs b/UncomplicatedCustomRoles/Commands/LogShare.cs index f7f54f3..3a0890d 100644 --- a/UncomplicatedCustomRoles/Commands/LogShare.cs +++ b/UncomplicatedCustomRoles/Commands/LogShare.cs @@ -1,78 +1,86 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . */ -using CommandSystem; using System; -using System.Net; -using UncomplicatedCustomRoles.Manager; using System.Collections.Generic; +using System.Net; using System.Text.Json; using System.Threading.Tasks; +using CommandSystem; +using UncomplicatedCustomRoles.Manager; -namespace UncomplicatedCustomRoles.Commands +namespace UncomplicatedCustomRoles.Commands; + +[CommandHandler(typeof(GameConsoleCommandHandler))] +internal class LogShare : ParentCommand { - [CommandHandler(typeof(GameConsoleCommandHandler))] - internal class LogShare : ParentCommand + public LogShare() { - public LogShare() => LoadGeneratedCommands(); + LoadGeneratedCommands(); + } - public override string Command { get; } = "ucrlogs"; + public override string Command { get; } = "ucrlogs"; - public override string[] Aliases { get; } = new string[] { }; + public override string[] Aliases { get; } = []; - public override string Description { get; } = "Share the UCR Debug logs with the developers"; + public override string Description { get; } = "Share the UCR Debug logs with the developers"; - public override void LoadGeneratedCommands() { } + public override void LoadGeneratedCommands() + { + } - protected override bool ExecuteParent(ArraySegment arguments, ICommandSender sender, out string response) + protected override bool ExecuteParent(ArraySegment arguments, ICommandSender sender, out string response) + { + if (sender.LogName is not "SERVER CONSOLE") { - if (sender.LogName is not "SERVER CONSOLE") - { - response = "Sorry but this command is reserved to the game console!"; - return false; - } + response = "Sorry but this command is reserved to the game console!"; + return false; + } - long Start = DateTimeOffset.Now.ToUnixTimeMilliseconds(); - response = "Loading the JSON content to share with the developers..."; + var Start = DateTimeOffset.Now.ToUnixTimeMilliseconds(); + response = "Loading the JSON content to share with the developers..."; - bool online = arguments.Count < 1; - Task.Run(() => + var online = arguments.Count < 1; + Task.Run(() => + { + var Response = LogManager.SendReport(out var content, online); + try { - HttpStatusCode Response = LogManager.SendReport(out string content, online); - try - { - if (!online) - LogManager.Info("Logs saved to file successfully."); + if (!online) + LogManager.Info("Logs saved to file successfully."); - if (Response is HttpStatusCode.OK) + if (Response is HttpStatusCode.OK) + { + if (string.IsNullOrEmpty(content)) { - if (string.IsNullOrEmpty(content)) - { - LogManager.Error("Server returned OK but the response body was empty."); - return; - } - LogManager.Debug($"Received content: {content}"); - Dictionary Data = JsonSerializer.Deserialize>(content); - LogManager.Info($"Successfully shared the UCR logs with the developers!\nSend this Id to the developers: {Data["id"].GetString()}\n\nTook {DateTimeOffset.Now.ToUnixTimeMilliseconds() - Start}ms"); + LogManager.Error("Server returned OK but the response body was empty."); + return; } - else - LogManager.Info($"Failed to share the UCR logs with the developers: Server says: {Response}"); + + LogManager.Debug($"Received content: {content}"); + var Data = JsonSerializer.Deserialize>(content); + LogManager.Info( + $"Successfully shared the UCR logs with the developers!\nSend this Id to the developers: {Data["id"].GetString()}\n\nTook {DateTimeOffset.Now.ToUnixTimeMilliseconds() - Start}ms"); } - catch (Exception e) - { - LogManager.Error(e.ToString()); + else + { + LogManager.Info($"Failed to share the UCR logs with the developers: Server says: {Response}"); } - }); - + } + catch (Exception e) + { + LogManager.Error(e.ToString()); + } + }); - return true; - } + + return true; } } \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Commands/Owner.cs b/UncomplicatedCustomRoles/Commands/Owner.cs index 7e1bbf9..2b0a6cd 100644 --- a/UncomplicatedCustomRoles/Commands/Owner.cs +++ b/UncomplicatedCustomRoles/Commands/Owner.cs @@ -1,48 +1,46 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . */ +using System.Collections.Generic; using CommandSystem; using LabApi.Features.Wrappers; -using System.Collections.Generic; -using System.Net; using UncomplicatedCustomRoles.API.Interfaces; using UncomplicatedCustomRoles.Extensions; -namespace UncomplicatedCustomRoles.Commands +namespace UncomplicatedCustomRoles.Commands; + +public class Owner : IUCRCommand { - public class Owner : IUCRCommand - { - public string Name { get; } = "owner"; + public string Name { get; } = "owner"; + + public string Description { get; } = "Get the 'Server Owner' role on our Discord server"; - public string Description { get; } = "Get the 'Server Owner' role on our Discord server"; + public string RequiredPermission { get; } = "ucr.owner"; - public string RequiredPermission { get; } = "ucr.owner"; + public bool Executor(List arguments, ICommandSender sender, out string response) + { + if (arguments.Count != 1) + { + response = "Usage: ucr owner "; + return false; + } - public bool Executor(List arguments, ICommandSender sender, out string response) + if (!Player.TryGet(sender, out var player)) { - if (arguments.Count != 1) - { - response = "Usage: ucr owner "; - return false; - } - - if (!Player.TryGet(sender, out Player player)) - { - response = "This command can only be executed by a player."; - return false; - } - - HttpStatusCode code = Plugin.HttpManager.AddServerOwner(player, arguments[0]).GetStatusCode(out response); - - response = $"{code} - {response}"; - return true; + response = "This command can only be executed by a player."; + return false; } + + var code = Plugin.HttpManager.AddServerOwner(player, arguments[0]).GetStatusCode(out response); + + response = $"{code} - {response}"; + return true; } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Commands/Percentages.cs b/UncomplicatedCustomRoles/Commands/Percentages.cs index e57a333..87b10da 100644 --- a/UncomplicatedCustomRoles/Commands/Percentages.cs +++ b/UncomplicatedCustomRoles/Commands/Percentages.cs @@ -1,53 +1,70 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . */ -using CommandSystem; -using PlayerRoles; using System; using System.Collections.Generic; using System.Linq; +using CommandSystem; +using PlayerRoles; using UncomplicatedCustomRoles.API.Features; using UncomplicatedCustomRoles.API.Interfaces; using UncomplicatedCustomRoles.Extensions; -namespace UncomplicatedCustomRoles.Commands +namespace UncomplicatedCustomRoles.Commands; + +public class Percentages : IUCRCommand { - public class Percentages : IUCRCommand - { - public string Name { get; } = "percentages"; + public string Name { get; } = "percentages"; - public string Description { get; } = "See every spawn percentage of any role"; + public string Description { get; } = "See every spawn percentage of any role"; - public string RequiredPermission { get; } = "ucr.percentages"; + public string RequiredPermission { get; } = "ucr.percentages"; - public bool Executor(List args, ICommandSender sender, out string response) - { - bool detailed = args.Any() && args[0] is "details"; - response = "Spawn percentages for each base Role:"; + public bool Executor(List args, ICommandSender sender, out string response) + { + var detailed = args.Any() && args[0] is "details"; + response = "Spawn percentages for each base Role:"; - foreach (RoleTypeId role in Enum.GetValues(typeof(RoleTypeId))) + foreach (RoleTypeId role in Enum.GetValues(typeof(RoleTypeId))) + { + var roles = CustomRole.List.Where(r => + r.SpawnSettings?.CanReplaceRoles != null && r.SpawnSettings.CanReplaceRoles.Contains(role)); + var customRoles = roles.ToList(); + if (customRoles.Any()) { - IEnumerable manualRoles = CustomRole.List.Where(r => r.SpawnSettings?.CanReplaceRoles == null || !r.SpawnSettings.CanReplaceRoles.Any()); - if (manualRoles.Any()) - { - response += $"\n\nℹ️ Roles without a linked vanilla role ({manualRoles.Count()}) - spawned manually or by another plugin:"; - foreach (ICustomRole customRole in manualRoles) - response += customRole.SpawnSettings is not null && customRole.SpawnSettings.SpawnChance > 0 - ? $"\n ∟ {customRole} - {customRole.SpawnSettings.SpawnChance}%" - : $"\n ∟ {customRole}"; - } + var total = customRoles.Sum(r => r.SpawnSettings.SpawnChance); + response += + $"\n\n{(total >= 100 ? "❗" : "✔️")} {role.GetFullName()} ({customRoles.Count()})"; + response += + $"\nChance of spawning as a CustomRole: {total}%\nChance of spawning as a regular role: {100 - total}%"; + + if (detailed) + foreach (var customRole in customRoles.Where(r => r.SpawnSettings.SpawnChance > 0)) + response += $"\n ∟ {customRole} - {customRole.SpawnSettings.SpawnChance}%"; } - - response += "\nOwO"; // We want to render everything + } - return true; + var manualRoles = CustomRole.List.Where(r => + r.SpawnSettings?.CanReplaceRoles == null || !r.SpawnSettings.CanReplaceRoles.Any()); + if (manualRoles.Any()) + { + response += + $"\n\nℹ️ Roles without a linked vanilla role ({manualRoles.Count()}) - spawned manually or by another plugin:"; + foreach (var customRole in manualRoles) + response += customRole.SpawnSettings is not null && customRole.SpawnSettings.SpawnChance > 0 + ? $"\n ∟ {customRole} - {customRole.SpawnSettings.SpawnChance}%" + : $"\n ∟ {customRole}"; } + + response += "\nOwO"; // We want to render everything + + return true; } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Commands/Reload.cs b/UncomplicatedCustomRoles/Commands/Reload.cs index 6eee546..6393b83 100644 --- a/UncomplicatedCustomRoles/Commands/Reload.cs +++ b/UncomplicatedCustomRoles/Commands/Reload.cs @@ -1,60 +1,61 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . */ using System.Collections.Concurrent; -using CommandSystem; -using LabApi.Features.Wrappers; using System.Collections.Generic; using System.Linq; +using CommandSystem; +using LabApi.Features.Wrappers; using UncomplicatedCustomRoles.API.Features; using UncomplicatedCustomRoles.API.Interfaces; using UncomplicatedCustomRoles.Compatibility; using UncomplicatedCustomRoles.Extensions; using UncomplicatedCustomRoles.Manager; -namespace UncomplicatedCustomRoles.Commands +namespace UncomplicatedCustomRoles.Commands; + +public class Reload : IUCRCommand { - public class Reload : IUCRCommand - { - public string Name { get; } = "reload"; + public string Name { get; } = "reload"; + + public string Description { get; } = "Reload every custom role loaded and search for new"; - public string Description { get; } = "Reload every custom role loaded and search for new"; + public string RequiredPermission { get; } = "ucr.reload"; + + public bool Executor(List arguments, ICommandSender sender, out string response) + { + var oldRoles = CustomRole.CustomRoles.Clone(); - public string RequiredPermission { get; } = "ucr.reload"; + CustomRole.CustomRoles = new ConcurrentDictionary(); + CustomRole.NotLoadedRoles.Clear(); + CustomRole.OutdatedRoles.Clear(); + ImportManager.Unload(); - public bool Executor(List arguments, ICommandSender sender, out string response) - { - ConcurrentDictionary oldRoles = CustomRole.CustomRoles.Clone(); - - CustomRole.CustomRoles = new(); - CustomRole.NotLoadedRoles.Clear(); - CustomRole.OutdatedRoles.Clear(); - ImportManager.Unload(); + FileConfigs.LoadAll(); + FileConfigs.LoadAll(Server.Port.ToString()); + ImportManager.Reload(); - FileConfigs.LoadAll(); - FileConfigs.LoadAll(Server.Port.ToString()); - ImportManager.Reload(); - - foreach (KeyValuePair oldRole in oldRoles) - if (!CustomRole.CustomRoles.ContainsKey(oldRole.Key) && !CompatibilityManager.RolePaths.ContainsKey(oldRole.Value)) - CustomRole.Register(oldRole.Value); + foreach (var oldRole in oldRoles) + if (!CustomRole.CustomRoles.ContainsKey(oldRole.Key) && + !CompatibilityManager.RolePaths.ContainsKey(oldRole.Value)) + CustomRole.Register(oldRole.Value); - IEnumerable removedRoles = oldRoles.Keys.Except(CustomRole.CustomRoles.Keys); + var removedRoles = oldRoles.Keys.Except(CustomRole.CustomRoles.Keys).ToList(); - foreach (int role in removedRoles) - SummonedCustomRole.RemoveSpecificRole(role); + foreach (var role in removedRoles) + SummonedCustomRole.RemoveSpecificRole(role); - int added = CustomRole.CustomRoles.Count - (oldRoles.Count + removedRoles.Count()); + var added = CustomRole.CustomRoles.Keys.Except(oldRoles.Keys).Count(); - response = $"\nSuccessfully reloaded UncomplicatedCustomRoles\n➕ Added {(added <= 0 ? "0" : added)} Custom Roles\n➖ Removed {removedRoles.Count()} Custom Roles\n🔢 Loaded a total of {CustomRole.CustomRoles.Count} Custom Roles\n⚠️ If you have changed some stats of the Custom Roles such as health and inventory the changes won't took place on already spawned players with these custom roles!"; - return true; - } + response = + $"\nSuccessfully reloaded UncomplicatedCustomRoles\n➕ Added {added} Custom Roles\n➖ Removed {removedRoles.Count} Custom Roles\n🔢 Loaded a total of {CustomRole.CustomRoles.Count} Custom Roles\n⚠️ If you have changed some stats of the Custom Roles such as health and inventory the changes won't take place on already spawned players with these custom roles!"; + return true; } } \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Commands/Role.cs b/UncomplicatedCustomRoles/Commands/Role.cs index 480b6b8..9f4c562 100644 --- a/UncomplicatedCustomRoles/Commands/Role.cs +++ b/UncomplicatedCustomRoles/Commands/Role.cs @@ -1,68 +1,64 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . */ -using CommandSystem; -using LabApi.Features.Wrappers; using System.Collections.Generic; using System.Linq; -using UncomplicatedCustomRoles.API.Features; +using CommandSystem; +using LabApi.Features.Wrappers; using UncomplicatedCustomRoles.API.Interfaces; using UncomplicatedCustomRoles.Extensions; -namespace UncomplicatedCustomRoles.Commands +namespace UncomplicatedCustomRoles.Commands; + +public class Role : IUCRCommand { - public class Role : IUCRCommand - { - public string Name { get; } = "role"; + public string Name { get; } = "role"; + + public string Description { get; } = "List all players with a custom role or see a player's custom role"; - public string Description { get; } = "List all players with a custom role or see a player's custom role"; + public string RequiredPermission { get; } = "ucr.role"; - public string RequiredPermission { get; } = "ucr.role"; + public bool Executor(List arguments, ICommandSender sender, out string response) + { + if (arguments.Count > 1) + { + response = "Usage: ucr role (Player ID or Name)"; + return false; + } - public bool Executor(List arguments, ICommandSender sender, out string response) + if (arguments.Count == 1) { - if (arguments.Count > 1) + if (!Player.TryGet(arguments[0], out var player)) { - response = "Usage: ucr role (Player ID or Name)"; + response = $"Sorry but the player {arguments[0]} does not exists!"; return false; } - if (arguments.Count == 1) + if (player.TryGetSummonedInstance(out var summoned)) { - Player Player = Player.Get(int.Parse(arguments[0])); - - if (Player is null) - { - response = $"Sorry but the player {arguments[0]} does not exists!"; - return false; - } - - if (Player.TryGetSummonedInstance(out SummonedCustomRole summoned)) - { - response = $"Player {Player.Nickname} {Player.UserId} [{Player.PlayerId}] is the custom role {summoned.Role.Name} [{summoned.Role.Id}]"; - return true; - } - - response = $"Player {Player.Nickname} {Player.UserId} [{Player.PlayerId}] is not a custom role!"; - return true; - } - else - { - response = "Custom roles of every player:"; - foreach (Player Player in Player.ReadyList.Where(p => !p.IsHost)) - if (Player.TryGetSummonedInstance(out SummonedCustomRole summoned)) - response += $"\n - Player {Player.Nickname} {Player.UserId} [{Player.PlayerId}] is the custom role {summoned.Role.Name} [{summoned.Role.Id}]"; - else - response += $"\n - Player {Player.Nickname} {Player.UserId} [{Player.PlayerId}] is not a custom role!"; + response = + $"Player {player.Nickname} {player.UserId} [{player.PlayerId}] is the custom role {summoned.Role.Name} [{summoned.Role.Id}]"; return true; } + + response = $"Player {player.Nickname} {player.UserId} [{player.PlayerId}] is not a custom role!"; + return true; } + + response = "Custom roles of every player:"; + foreach (var Player in Player.ReadyList.Where(p => !p.IsHost)) + if (Player.TryGetSummonedInstance(out var summoned)) + response += + $"\n - Player {Player.Nickname} {Player.UserId} [{Player.PlayerId}] is the custom role {summoned.Role.Name} [{summoned.Role.Id}]"; + else + response += $"\n - Player {Player.Nickname} {Player.UserId} [{Player.PlayerId}] is not a custom role!"; + return true; } } \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Commands/Spawn.cs b/UncomplicatedCustomRoles/Commands/Spawn.cs index bd8acd7..04deb7f 100644 --- a/UncomplicatedCustomRoles/Commands/Spawn.cs +++ b/UncomplicatedCustomRoles/Commands/Spawn.cs @@ -1,100 +1,111 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . */ +using System; +using System.Collections.Generic; +using System.Linq; using CommandSystem; using LabApi.Features.Wrappers; using MEC; using PlayerRoles; -using System; -using System.Collections.Generic; -using System.Linq; using UncomplicatedCustomRoles.API.Features; using UncomplicatedCustomRoles.API.Interfaces; using UncomplicatedCustomRoles.Manager; -namespace UncomplicatedCustomRoles.Commands +namespace UncomplicatedCustomRoles.Commands; + +public class Spawn : IUCRCommand { - public class Spawn : IUCRCommand + public string Name { get; } = "spawn"; + + public string Description { get; } = "Spawn a player with a UCR Role"; + + public string RequiredPermission { get; } = "ucr.spawn"; + + public bool Executor(List arguments, ICommandSender sender, out string response) { - public string Name { get; } = "spawn"; + if (arguments.Count < 2) + { + response = "Usage: ucr spawn "; + return false; + } - public string Description { get; } = "Spawn a player with a UCR Role"; + if (!Round.IsRoundInProgress) + { + response = "Sorry but you can't use this command if the round is not started!"; + return false; + } - public string RequiredPermission { get; } = "ucr.spawn"; + List> players; + + if (arguments[0].Contains(",")) + players = arguments[0].Replace(" ", string.Empty).Split(',').Select(Resolve).ToList(); + else if (arguments[0] is "all") + players = Player.ReadyList.Select(p => new Tuple(null, p)).ToList(); + else if (arguments[0] is "spectators" or "spect") + players = Player.ReadyList.Where(p => p.Role is RoleTypeId.Spectator or RoleTypeId.None) + .Select(p => new Tuple(null, p)).ToList(); + else if (arguments[0] is "alive" or "al") + players = Player.ReadyList.Where(p => p.Role is not (RoleTypeId.Spectator or RoleTypeId.None)) + .Select(p => new Tuple(null, p)).ToList(); + else + players = [Resolve(arguments[0])]; + + if (!int.TryParse(arguments[1], out var id)) + { + response = + $"'{arguments[1]}' is not a valid Role Id! It must be a number - use 'ucr list' to see every registered role."; + return false; + } + + var result = string.Empty; + var sync = arguments.Count > 2 && arguments[2] == "sync"; + + foreach (var player in players) + result += $"{SpawnPlayer(player, id, sync)}\n"; - public bool Executor(List arguments, ICommandSender sender, out string response) + response = + $"Spawning {players.Count} players as CustomRole {(sync ? "synchronously" : "asynchronously")}\n{result}"; + return true; + + static Tuple Resolve(string idOrName) { - if (arguments.Count < 2) - { - response = "Usage: ucr spawn "; - return false; - } - - if (!LabApi.Features.Wrappers.Round.IsRoundInProgress) - { - response = "Sorry but you can't use this command if the round is not started!"; - return false; - } - - IEnumerable> players; - - if (arguments[0].Contains(",")) - players = arguments[0].Replace(" ", string.Empty).Split(',').Select(p => new Tuple(p, Player.Get(int.Parse(p)))); - else if (arguments[0] is "all") - players = Player.ReadyList.Select(p => new Tuple(null, p)); - else if (arguments[0] is "spectators" or "spect") - players = Player.ReadyList.Where(p => p.Role is RoleTypeId.Spectator or RoleTypeId.None).Select(p => new Tuple(null, p)); - else if (arguments[0] is "alive" or "al") - players = Player.ReadyList.Where(p => p.Role is not (RoleTypeId.Spectator or RoleTypeId.None)).Select(p => new Tuple(null, p)); - else - players = new[] { new Tuple(arguments[0], Player.Get(int.Parse(arguments[0]))) }; - - string result = string.Empty; - bool sync = arguments.Count > 2 && arguments[2] == "sync"; - - if (arguments[1] is not null && int.TryParse(arguments[1], out int id)) - foreach (Tuple player in players) - result += SpawnPlayer(player, id, sync); - - response = $"Spawning {players.Count()} players as CustomRole {(sync ? "synchronously" : "asynchronously")}\n{result}"; - return true; + return new Tuple(idOrName, + int.TryParse(idOrName, out var playerId) ? Player.Get(playerId) : null); } + } - private string SpawnPlayer(Tuple rawPlayer, int id, bool sync) + private static string SpawnPlayer(Tuple rawPlayer, int id, bool sync) + { + var player = rawPlayer.Item2; + + if (player is null) + return $"Player '{rawPlayer.Item1}' not found!"; + + LogManager.Debug($"Selected role Id as Int32: {id}"); + if (!CustomRole.CustomRoles.ContainsKey(id)) + return $"Role with the Id {id} was not found!"; + // Remove shit from the db + SpawnManager.ClearCustomTypes(player); + + if (sync) { - Player player = rawPlayer.Item2; - - if (player is null) - return $"Player '{rawPlayer.Item1}' not found!"; - - LogManager.Debug($"Selected role Id as Int32: {id}"); - if (!CustomRole.CustomRoles.ContainsKey(id)) - return $"Role with the Id {id} was not found!"; - else - { - // Remove shit from the db - SpawnManager.ClearCustomTypes(player); - - if (sync) - { - LogManager.Debug("Spawning player sync"); - SpawnManager.SummonCustomSubclass(player, id, true); - } - else - { - LogManager.Debug("Spawning player async"); - Timing.RunCoroutine(SpawnManager.AsyncPlayerSpawner(player, id)); - } - - return $"Successfully spawned player {player.Nickname} ({player.PlayerId}) as CustomRole {id}"; - } + LogManager.Debug("Spawning player sync"); + SpawnManager.SummonCustomSubclass(player, id); } + else + { + LogManager.Debug("Spawning player async"); + Timing.RunCoroutine(SpawnManager.AsyncPlayerSpawner(player, id)); + } + + return $"Successfully spawned player {player.Nickname} ({player.PlayerId}) as CustomRole {id}"; } } \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Commands/SpawnPoint.cs b/UncomplicatedCustomRoles/Commands/SpawnPoint.cs index c312348..dbb7ff4 100644 --- a/UncomplicatedCustomRoles/Commands/SpawnPoint.cs +++ b/UncomplicatedCustomRoles/Commands/SpawnPoint.cs @@ -1,219 +1,254 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . */ -using CommandSystem; -using LabApi.Features.Wrappers; using System.Collections.Generic; -using SpawnPointInstance = UncomplicatedCustomRoles.API.Features.SpawnPoint; -using UncomplicatedCustomRoles.Manager.NET; using System.Net; using System.Threading.Tasks; -using UncomplicatedCustomRoles.Manager; +using CommandSystem; +using LabApi.Features.Wrappers; using UncomplicatedCustomRoles.API.Interfaces; using UncomplicatedCustomRoles.Extensions; +using UncomplicatedCustomRoles.Manager; +using UncomplicatedCustomRoles.Manager.NET; +using SpawnPointInstance = UncomplicatedCustomRoles.API.Features.SpawnPoint; -namespace UncomplicatedCustomRoles.Commands +namespace UncomplicatedCustomRoles.Commands; + +internal class SpawnPoint : IUCRCommand { - internal class SpawnPoint : IUCRCommand + public const string CommandHeader = "UncomplicatedCustomRoles - SpawnPoint Feature\n"; + + public const string LocalError = + "Sorry but you can't perform that action while having your spawnpoints hosted in your local folder!"; + + public Dictionary> SubCommands = new() { - public string Name { get; } = "spawnpoint"; + { + "list", + new KeyValuePair("", "List every registered SpawnPoint") + }, + { + "create", + new KeyValuePair("(Name) ", "Create a new SpawnPoint at your current position") + }, + { + "delete", + new KeyValuePair("(Name) ", "Delete an existing SpawnPoint") + }, + { + "goto", + new KeyValuePair("(Name) ", "Teleport yourself to a SpawnPoint") + }, + { + "sync", + new KeyValuePair("", + "Update your local SpawnPoint list by downloading it from the UCS cloud") + }, + { + "migrate", + new KeyValuePair("(NewPort) ", "Migrate current SpawnPoints to another port (but same IP)") + }, + { + "download", + new KeyValuePair("", + "Get a link to download the current SpawnPoint list from the UCS cloud") + }, + { + "ip", + new KeyValuePair("", "Get your current IPv4/IPv6") + } + }; - public string Description { get; } = "Manage the UCR spawnpoints"; + public string Name { get; } = "spawnpoint"; - public string RequiredPermission { get; } = "ucr.spawnpoint"; + public string Description { get; } = "Manage the UCR spawnpoints"; - public const string CommandHeader = "UncomplicatedCustomRoles - SpawnPoint Feature\n"; + public string RequiredPermission { get; } = "ucr.spawnpoint"; - public const string LocalError = "Sorry but you can't perform that action while having your spawnpoints hosted in your local folder!"; + public bool Executor(List arguments, ICommandSender sender, out string response) + { + var player = Player.Get(sender); - public Dictionary> SubCommands = new() + if (player is null) { - { - "list", - new("", "List every registered SpawnPoint") - }, - { - "create", - new("(Name) ", "Create a new SpawnPoint at your current position") - }, - { - "delete", - new("(Name) ", "Delete an existing SpawnPoint") - }, - { - "goto", - new("(Name) ", "Teleport yourself to a SpawnPoint") - }, - { - "sync", - new("", "Update your local SpawnPoint list by downloading it from the UCS cloud") - }, - { - "migrate", - new("(NewPort) ", "Migrate current SpawnPoints to another port (but same IP)") - }, - { - "download", - new("", "Get a link to download the current SpawnPoint list from the UCS cloud") - }, - { - "ip", - new("", "Get your current IPv4/IPv6") - } - }; + response = "You need to be a player in order to execute this command!"; + return false; + } - public bool Executor(List arguments, ICommandSender sender, out string response) - { - Player Player = Player.Get(sender); + response = null; - if (Player is null) + if (arguments.Count == 0) + { + response = CommandHeader; + foreach (var command in SubCommands) + response += $"{command.Key} {command.Value.Key}-> {command.Value.Value}\n"; + } + else + { + switch (arguments[0]) { - response = "You need to be a player in order to execute this command!"; - return false; - } + case "list": + response = + $"{CommandHeader}Currently registered SpawnPoints ({SpawnPointInstance.List.Count}/{SpawnPointApiCommunicator.MaxSpawnPoints}):\n"; + + foreach (var SpawnPoint in SpawnPointInstance.List) + response += $"- {SpawnPoint}\n"; + + break; + case "create": + if (arguments.Count != 2) + { + response = "Wrong usage!\nucr spawnpoint create (Name)"; + return false; + } - response = null; + if (SpawnPointInstance.TryGet(arguments[1], out _)) + { + response = $"A SpawnPoint with the name '{arguments[1]}' is already registered!"; + return false; + } - if (arguments.Count == 0) - { - response = CommandHeader; - foreach (KeyValuePair> command in SubCommands) - response += $"{command.Key} {command.Value.Key}-> {command.Value.Value}\n"; - } - else - switch (arguments[0]) - { - case "list": - response = $"{CommandHeader}Currently registered SpawnPoints ({SpawnPointInstance.List.Count}/{SpawnPointApiCommunicator.MaxSpawnPoints}):\n"; - - foreach (SpawnPointInstance SpawnPoint in SpawnPointInstance.List) - response += $"- {SpawnPoint}\n"; - - break; - case "create": - if (arguments.Count != 2) - { - response = "Wrong usage!\nucr spawnpoint create (Name)"; - return false; - } + if (SpawnPointInstance.List.Count >= SpawnPointApiCommunicator.MaxSpawnPoints) + { + response = + $"You've reached the maximum number of SpawnPoints for this port!\nMaximum: {SpawnPointApiCommunicator.MaxSpawnPoints}"; + return false; + } - if (SpawnPointInstance.TryGet(arguments[1], out _)) - { - response = $"A SpawnPoint with the name '{arguments[1]}' is already registered!"; - return false; - } + new SpawnPointInstance(arguments[1], player); + SpawnPointApiCommunicator.AsyncPushSpawnPoints(); - if (SpawnPointInstance.List.Count >= SpawnPointApiCommunicator.MaxSpawnPoints) - { - response = $"You've reached the maximum number of SpawnPoints for this port!\nMaximum: {SpawnPointApiCommunicator.MaxSpawnPoints}"; - return false; - } + response = $"SpawnPoint {arguments[1]} successfully created!"; + break; + case "delete": + if (arguments.Count != 2) + { + response = "Wrong usage!\nucr spawnpoint delete (Name)"; + return false; + } - new SpawnPointInstance(arguments[1], Player); + if (SpawnPointInstance.TryGet(arguments[1], out var spawnPoint)) + { + spawnPoint.Destroy(); + response = "SpawnPoint successfully removed!"; SpawnPointApiCommunicator.AsyncPushSpawnPoints(); + } + else + { + response = $"SpawnPoint '{arguments[1]}' not found!"; + } + + break; + case "migrate": + if (SpawnPointApiCommunicator.Local) + { + response = LocalError; + return false; + } - response = $"SpawnPoint {arguments[1]} successfully created!"; - break; - case "delete": - if (arguments.Count != 2) - { - response = "Wrong usage!\nucr spawnpoint delete (Name)"; - return false; - } + if (arguments.Count < 2) + { + response = "Wrong usage!\nucr spawnpoint migrate (NewPort)"; + return false; + } - if (SpawnPointInstance.TryGet(arguments[1], out SpawnPointInstance spawnPoint)) - { - spawnPoint.Destroy(); - response = "SpawnPoint successfully removed!"; - SpawnPointApiCommunicator.AsyncPushSpawnPoints(); - } - else - response = $"SpawnPoint '{arguments[1]}' not found!"; - break; - case "migrate": - if (SpawnPointApiCommunicator.Local) - response = LocalError; + if (!int.TryParse(arguments[1], out var newPort)) + { + response = $"'{arguments[1]}' is not a valid port number!"; + return false; + } + if (arguments.Count == 2) + { + response = + $"Are you sure to migrate every SpawnPoint from port {Server.Port} to port {newPort}?\nIf yes do again the command:\nucr spawnpoint migrate {arguments[1]} yes"; + return true; + } - if (arguments.Count < 2) - { - response = "Wrong usage!\nucr spawnpoint migrate (NewPort)"; - return false; - } - else if (arguments.Count == 2) - { - response = $"Are you sure to migrate every SpawnPoint from port {Server.Port} to port {int.Parse(arguments[1])}?\nIf yes do again the command:\nucr spawnpoint migrate {arguments[1]} yes"; - return true; - } - else if (arguments.Count == 3) - { - HttpStatusCode Status = SpawnPointApiCommunicator.PushMigrationRequest(int.Parse(arguments[1])).GetStatusCode(out _); - - if (Status is HttpStatusCode.OK) - { - response = $"Migration completed!\nRefreshing the local database..."; - SpawnPointInstance.List.Clear(); - } - else - response = $"Migration failed!\nUCS cloud says: {Status}"; - } - break; - case "download": - if (SpawnPointApiCommunicator.Local) - response = LocalError; - - string url = SpawnPointApiCommunicator.AskDownloadUrl(); - LogManager.Info($"Download your SpawnPoint settings with this URL:\n{SpawnPointApiCommunicator.AskDownloadUrl()}"); - response = $"Download URL:\n{SpawnPointApiCommunicator.AskDownloadUrl()}"; - break; - case "goto": - if (arguments.Count != 2) - { - response = "Wrong usage!\nucr spawnpoint goto (Name)"; - return false; - } + if (arguments.Count == 3) + { + var Status = SpawnPointApiCommunicator.PushMigrationRequest(newPort).GetStatusCode(out _); - if (!Player.IsAlive) + if (Status is HttpStatusCode.OK) { - response = "You have to be alive..."; - return false; + response = "Migration completed!\nRefreshing the local database..."; + SpawnPointInstance.List.Clear(); } - - if (SpawnPointInstance.TryGet(arguments[1], out SpawnPointInstance spawn)) + else { - response = "Teleporting to spawnpoint..."; - spawn.Spawn(Player); + response = $"Migration failed!\nUCS cloud says: {Status}"; } - else - response = "SpawnPoint not found!"; - break; - case "ip": - if (SpawnPointApiCommunicator.Local) - response = LocalError; - - response = $"Your IPv4/IPv6 is: {SpawnPointApiCommunicator.AskIp()}"; - break; - case "sync": - if (SpawnPointApiCommunicator.Local) - response = LocalError; - - response = "Sync done!"; - Task.Run(SpawnPointApiCommunicator.LoadFromCloud); - break; - default: - response = $"SubCommand '{arguments[0]}' not found!"; + } + + break; + case "download": + if (SpawnPointApiCommunicator.Local) + { + response = LocalError; return false; - } + } + + var url = SpawnPointApiCommunicator.AskDownloadUrl(); + LogManager.Info($"Download your SpawnPoint settings with this URL:\n{url}"); + response = $"Download URL:\n{url}"; + break; + case "goto": + if (arguments.Count != 2) + { + response = "Wrong usage!\nucr spawnpoint goto (Name)"; + return false; + } - response ??= "Internal Plugin Error - 500"; - return true; + if (!player.IsAlive) + { + response = "You have to be alive..."; + return false; + } + + if (SpawnPointInstance.TryGet(arguments[1], out var spawn)) + { + response = "Teleporting to spawnpoint..."; + spawn.Spawn(player); + } + else + { + response = "SpawnPoint not found!"; + } + + break; + case "ip": + if (SpawnPointApiCommunicator.Local) + { + response = LocalError; + return false; + } + + response = $"Your IPv4/IPv6 is: {SpawnPointApiCommunicator.AskIp()}"; + break; + case "sync": + if (SpawnPointApiCommunicator.Local) + { + response = LocalError; + return false; + } + + response = "Sync started! The SpawnPoints are being downloaded in the background..."; + Task.Run(SpawnPointApiCommunicator.LoadFromCloud); + break; + default: + response = $"SubCommand '{arguments[0]}' not found!"; + return false; + } } + + response ??= "Internal Plugin Error - 500"; + return true; } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Commands/Update.cs b/UncomplicatedCustomRoles/Commands/Update.cs index e7146f0..4dec3e2 100644 --- a/UncomplicatedCustomRoles/Commands/Update.cs +++ b/UncomplicatedCustomRoles/Commands/Update.cs @@ -1,62 +1,68 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . */ -using CommandSystem; -using LabApi.Loader.Features.Yaml; using System.Collections.Generic; using System.IO; using System.Linq; +using CommandSystem; +using LabApi.Loader.Features.Yaml; using UncomplicatedCustomRoles.API.Features; using UncomplicatedCustomRoles.API.Interfaces; using UncomplicatedCustomRoles.Compatibility; -namespace UncomplicatedCustomRoles.Commands +namespace UncomplicatedCustomRoles.Commands; + +public class Update : IUCRCommand { - public class Update : IUCRCommand - { - public string Name { get; } = "update"; + public string Name { get; } = "update"; - public string Description { get; } = "Update one or more outdated (but loaded) CustomRole(s)"; + public string Description { get; } = "Update one or more outdated (but loaded) CustomRole(s)"; - public string RequiredPermission { get; } = "ucr.update"; + public string RequiredPermission { get; } = "ucr.update"; - public bool Executor(List arguments, ICommandSender sender, out string response) + public bool Executor(List arguments, ICommandSender sender, out string response) + { + response = null; + if (arguments.Count is 0) { - response = null; - if (arguments.Count is 0) - { - response = "Usage: ucr update "; - return false; - } + response = "Usage: ucr update "; + return false; + } - if (arguments[0].ToLower() is "all") - foreach (OutdatedCustomRole role in CustomRole.OutdatedRoles) - UpdateRole(role); - else + if (arguments[0].ToLower() is "all") + { + foreach (var role in CustomRole.OutdatedRoles) + UpdateRole(role); + } + else + { + if (int.TryParse(arguments[0], out var id)) { - if (int.TryParse(arguments[0], out int id)) - { - OutdatedCustomRole role = CustomRole.OutdatedRoles.FirstOrDefault(r => r.CustomRole.Id == id); - if (role is not null) - UpdateRole(role); - else - response = $"CustomRole {arguments[0]} not found!"; - } + var role = CustomRole.OutdatedRoles.FirstOrDefault(r => r.CustomRole.Id == id); + if (role is not null) + UpdateRole(role); else response = $"CustomRole {arguments[0]} not found!"; } - - response ??= "Successfully updated CustomRole(s)!"; - return true; + else + { + response = $"CustomRole {arguments[0]} not found!"; + } } - private static void UpdateRole(OutdatedCustomRole role) => File.WriteAllText(role.Path, YamlConfigParser.Serializer.Serialize(role.CustomRole)); + response ??= "Successfully updated CustomRole(s)!"; + return true; + } + + private static void UpdateRole(OutdatedCustomRole role) + { + File.WriteAllText(role.Path, YamlConfigParser.Serializer.Serialize(role.CustomRole)); } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Commands/Version.cs b/UncomplicatedCustomRoles/Commands/Version.cs index cab832b..dfd3cb0 100644 --- a/UncomplicatedCustomRoles/Commands/Version.cs +++ b/UncomplicatedCustomRoles/Commands/Version.cs @@ -1,45 +1,47 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . */ -using CommandSystem; using System.Collections.Generic; +using CommandSystem; using UncomplicatedCustomRoles.API.Interfaces; using UncomplicatedCustomRoles.Manager; -namespace UncomplicatedCustomRoles.Commands +namespace UncomplicatedCustomRoles.Commands; + +public class Version : IUCRCommand { - public class Version : IUCRCommand - { - public string Name { get; } = "version"; + public string Name { get; } = "version"; - public string Description { get; } = "Get the informations about the current version of UCR"; + public string Description { get; } = "Get the informations about the current version of UCR"; - public string RequiredPermission { get; } = "ucr.version"; + public string RequiredPermission { get; } = "ucr.version"; - public bool Executor(List arguments, ICommandSender sender, out string response) + public bool Executor(List arguments, ICommandSender sender, out string response) + { + if (VersionManager.VersionInfo is null) { - if (VersionManager.VersionInfo is null) - { - response = "Can't load VersionManager.VersionInfo: Failed to GET HTTPS"; - return false; - } + response = "Can't load VersionManager.VersionInfo: Failed to GET HTTPS"; + return false; + } - response = $"UncomplicatedCustomRoles\nAuthors: {Plugin.Instance.Author}\nVersion: {VersionManager.VersionInfo.Name}{(VersionManager.VersionInfo.CustomName is not null ? $" '{VersionManager.VersionInfo.CustomName}'" : string.Empty)} ({Plugin.Instance.Version})\nSource: {VersionManager.VersionInfo.Source} - {VersionManager.VersionInfo.SourceLink ?? string.Empty}\nPre release: {(VersionManager.VersionInfo.PreRelease != 0 ? "TRUE" : "FALSE")}\nForced debug: {(VersionManager.VersionInfo.ForceDebug != 0 ? "TRUE" : "FALSE")}\nHash: {(!VersionManager.CorrectHash ? "NOT MATCHING!" : "Matching")}"; + response = + $"UncomplicatedCustomRoles\nAuthors: {Plugin.Instance.Author}\nVersion: {VersionManager.VersionInfo.Name}{(VersionManager.VersionInfo.CustomName is not null ? $" '{VersionManager.VersionInfo.CustomName}'" : string.Empty)} ({Plugin.Instance.Version})\nSource: {VersionManager.VersionInfo.Source} - {VersionManager.VersionInfo.SourceLink ?? string.Empty}\nPre release: {(VersionManager.VersionInfo.PreRelease != 0 ? "TRUE" : "FALSE")}\nForced debug: {(VersionManager.VersionInfo.ForceDebug != 0 ? "TRUE" : "FALSE")}\nHash: {(!VersionManager.CorrectHash ? "NOT MATCHING!" : "Matching")}"; - if (!VersionManager.CorrectHash) - response += "\n\n⚠ WARNING!\nYou are using a NON-OFFICIAL version of the plugin!\nThis version might contain viruses and it's NOT ours!"; + if (!VersionManager.CorrectHash) + response += + "\n\n⚠ WARNING!\nYou are using a NON-OFFICIAL version of the plugin!\nThis version might contain viruses and it's NOT ours!"; - if (VersionManager.VersionInfo.Recall != 0) - response += $"\n\n⚠ WARNING!\nThis version has been RECALLED due to the following reason:\n{VersionManager.VersionInfo.RecallReason}\nYou are HIGHLY SUGGESTED to update the plugin to the last stable target: {VersionManager.VersionInfo.RecallTarget} {(VersionManager.VersionInfo.RecallImportant ?? true ? $"\nYou HAVE TO update it, otherwise bad things will happen!" : string.Empty)}"; + if (VersionManager.VersionInfo.Recall != 0) + response += + $"\n\n⚠ WARNING!\nThis version has been RECALLED due to the following reason:\n{VersionManager.VersionInfo.RecallReason}\nYou are HIGHLY SUGGESTED to update the plugin to the last stable target: {VersionManager.VersionInfo.RecallTarget} {(VersionManager.VersionInfo.RecallImportant ?? true ? "\nYou HAVE TO update it, otherwise bad things will happen!" : string.Empty)}"; - return true; - } + return true; } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Compatibility/CompatibilityManager.cs b/UncomplicatedCustomRoles/Compatibility/CompatibilityManager.cs index cd4e879..187a0cc 100644 --- a/UncomplicatedCustomRoles/Compatibility/CompatibilityManager.cs +++ b/UncomplicatedCustomRoles/Compatibility/CompatibilityManager.cs @@ -1,19 +1,18 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . */ -using LabApi.Loader.Features.Yaml; using System; using System.Collections.Generic; using System.IO; using System.Linq; -using System.Reflection; +using LabApi.Loader.Features.Yaml; using NorthwoodLib.Pools; using UncomplicatedCustomRoles.API.Enums; using UncomplicatedCustomRoles.API.Features; @@ -22,174 +21,193 @@ using UncomplicatedCustomRoles.Extensions; using UncomplicatedCustomRoles.Manager; -namespace UncomplicatedCustomRoles.Compatibility +namespace UncomplicatedCustomRoles.Compatibility; + +public class CompatibilityManager { - public class CompatibilityManager + private static readonly Dictionary previousVersionRoles = new() { - /// - /// Gets the location (path) of every CustomRole. - /// - public static Dictionary RolePaths { get; } = new(); + { typeof(BonolisCustomRole), new Version(7, 0, 0) }, + { typeof(FossuonCustomRole), new Version(6, 0, 0) }, + { typeof(PreviousVersionRole), new Version(5, 0, 0) } + }; - private static readonly Dictionary previousVersionRoles = new() - { - { typeof(BonolisCustomRole), new(7, 0, 0) }, - { typeof(FossuonCustomRole), new(6, 0, 0) }, - { typeof(PreviousVersionRole), new(5, 0, 0) }, - }; + private static readonly Dictionary outdatedCustomRoles = new(); - private static readonly Dictionary outdatedCustomRoles = new(); + private static readonly string prefix = "[Role Loader] "; - private static readonly string prefix = "[Role Loader] "; + /// + /// Gets the location (path) of every CustomRole. + /// + public static Dictionary RolePaths { get; } = new(); - public static void ParseAndLoadCustomRole(string file) - { - string content = File.ReadAllText(file); - CustomRole role = null; + public static void ParseAndLoadCustomRole(string file) + { + var content = File.ReadAllText(file); + CustomRole role = null; - try - { - /*if (!TypeCheck(content, out string error)) - throw new Exception(error);*/ + try + { + /*if (!TypeCheck(content, out string error)) + throw new Exception(error);*/ - role = YamlConfigParser.Deserializer.Deserialize(content); - } catch (Exception ex) - { - // Try to decode older roles in order to make everything work - foreach (KeyValuePair kvp in previousVersionRoles) - try + role = YamlConfigParser.Deserializer.Deserialize(content); + } + catch (Exception ex) + { + // Try to decode older roles in order to make everything work + foreach (var kvp in previousVersionRoles) + try + { + var data = YamlConfigParser.Deserializer.Deserialize(content, kvp.Key); + if (data is IPreviousVersionRole prevRole) { - object data = YamlConfigParser.Deserializer.Deserialize(content, kvp.Key); - if (data is IPreviousVersionRole prevRole) - { - role = prevRole.ToCustomRole(); - outdatedCustomRoles.Add(role, kvp.Value); - break; - } - } - catch - { } - - if (role is null) - throw ex; - } + role = prevRole.ToCustomRole(); + outdatedCustomRoles.Add(role, kvp.Value); + break; + } + } + catch + { + } - RolePaths.TryAdd(role, file); - RegisterCustomRole(role); + if (role is null) + throw ex; } - public static LoadStatusType RegisterCustomRole(ICustomRole role) + RolePaths.TryAdd(role, file); + RegisterCustomRole(role); + } + + public static LoadStatusType RegisterCustomRole(ICustomRole role) + { + var status = CustomRole.InternalRegister(role); + + if (status is LoadStatusType.SameId && Plugin.Instance.Config.UseIdFixer && + RolePaths.TryGetValue(role, out var rolePath)) { - LoadStatusType status = CustomRole.InternalRegister(role); - - if (status is LoadStatusType.SameId && Plugin.Instance.Config.UseIdFixer && RolePaths.TryGetValue(role, out string rolePath)) - { - string roleId = GetRoleFileElement(File.ReadAllLines(rolePath), "id:"); - role.Id = int.Parse(roleId); - LogManager.Info($"Updated ID for role at {rolePath} - New id: {role.Id} ({roleId})", ConsoleColor.DarkMagenta); + var roleId = GetRoleFileElement(File.ReadAllLines(rolePath), "id:"); + role.Id = int.Parse(roleId); + LogManager.Info($"Updated ID for role at {rolePath} - New id: {role.Id} ({roleId})", + ConsoleColor.DarkMagenta); - status = CustomRole.InternalRegister(role); - } + status = CustomRole.InternalRegister(role); + } - if (status is LoadStatusType.Success) - LogManager.Info($"{prefix}Successfully loaded CustomRole {role}!", ConsoleColor.DarkGray); - else if (status is LoadStatusType.ValidatorError) - { - CustomRole.Validate(role, out string error); - LogManager.Error($"{prefix}Failed to load CustomRole {role}: failed to validate the CustomRole\n{error}", "RL0001"); - } - else if (status is LoadStatusType.SameId) - { - LogManager.Error($"{prefix}Failed to load CustomRole {role}: there's already another CustomRole with the same Id!", "RL0002"); + if (status is LoadStatusType.Success) + { + LogManager.Info($"{prefix}Successfully loaded CustomRole {role}!", ConsoleColor.DarkGray); + } + else if (status is LoadStatusType.ValidatorError) + { + CustomRole.Validate(role, out var error); + LogManager.Error($"{prefix}Failed to load CustomRole {role}: failed to validate the CustomRole\n{error}", + "RL0001"); + } + else if (status is LoadStatusType.SameId) + { + LogManager.Error( + $"{prefix}Failed to load CustomRole {role}: there's already another CustomRole with the same Id!", + "RL0002"); - if (!RolePaths.TryGetValue(role, out string path)) - path = null; + if (!RolePaths.TryGetValue(role, out var path)) + path = null; - if (path is not null) - CustomRole.NotLoadedRoles.Add(new(path, File.ReadAllLines(path), null, $"There's already another CustomRole with the Id {role.Id}")); - } + if (path is not null) + CustomRole.NotLoadedRoles.Add(new ErrorCustomRole(path, File.ReadAllLines(path), null, + $"There's already another CustomRole with the Id {role.Id}")); + } - if (status is not LoadStatusType.SameId && outdatedCustomRoles.TryGetValue(role, out Version version)) - LogManager.Info($"{prefix}The loaded CustomRole is made for UCR v{version.ToString(3)}. Consider updating it :)", ConsoleColor.Gray); + if (status is not LoadStatusType.SameId && outdatedCustomRoles.TryGetValue(role, out var version)) + LogManager.Info( + $"{prefix}The loaded CustomRole is made for UCR v{version.ToString(3)}. Consider updating it :)", + ConsoleColor.Gray); - if (status is LoadStatusType.Success && outdatedCustomRoles.TryGetValue(role, out Version version2) && RolePaths.TryGetValue(role, out string path2)) - CustomRole.OutdatedRoles.Add(new(role, version2, path2)); + if (status is LoadStatusType.Success && outdatedCustomRoles.TryGetValue(role, out var version2) && + RolePaths.TryGetValue(role, out var path2)) + CustomRole.OutdatedRoles.Add(new OutdatedCustomRole(role, version2, path2)); - return status; - } + return status; + } - public static string GetRoleFileElement(string content, string rowPart, bool removeSpaces = true) => GetRoleFileElement(content.Split(new string[] { Environment.NewLine }, StringSplitOptions.None), rowPart, removeSpaces); + public static string GetRoleFileElement(string content, string rowPart, bool removeSpaces = true) + { + return GetRoleFileElement(content.Split( + [Environment.NewLine], StringSplitOptions.None), rowPart, removeSpaces); + } - public static string GetRoleFileElement(string[] pieces, string rowPart, bool removeSpaces = true) - { - string el = pieces.FirstOrDefault(l => l.Contains(rowPart)) ?? "N/D"; + public static string GetRoleFileElement(string[] pieces, string rowPart, bool removeSpaces = true) + { + var el = pieces.FirstOrDefault(l => l.Contains(rowPart)) ?? "N/D"; - if (removeSpaces) - el.Replace(" ", string.Empty); + if (removeSpaces) + el.Replace(" ", string.Empty); - return el.Replace($"{rowPart} ", string.Empty).Replace(rowPart, string.Empty); - } + return el.Replace($"{rowPart} ", string.Empty).Replace(rowPart, string.Empty); + } - public static string HandleErrorString(Exception ex, bool showErrorName = false) - { - string message = (showErrorName ? $"{ex.GetType().Name} " : string.Empty) + ex.Message; + public static string HandleErrorString(Exception ex, bool showErrorName = false) + { + var message = (showErrorName ? $"{ex.GetType().Name} " : string.Empty) + ex.Message; - if (ex.InnerException is not null) - message += $" -> {ex.InnerException.Message}"; + if (ex.InnerException is not null) + message += $" -> {ex.InnerException.Message}"; - if (ex.InnerException is not null && ex.InnerException.InnerException is not null) - message += $" -> {ex.InnerException.InnerException.Message}"; + if (ex.InnerException is not null && ex.InnerException.InnerException is not null) + message += $" -> {ex.InnerException.InnerException.Message}"; - return message; - } + return message; + } - internal static int GetFirstFreeId(int start = 1) - { - while (CustomRole.CustomRoles.ContainsKey(start)) - start++; + internal static int GetFirstFreeId(int start = 1) + { + while (CustomRole.CustomRoles.ContainsKey(start)) + start++; - return start; - } + return start; + } - private static bool TypeCheck(string content, out string error) + private static bool TypeCheck(string content, out string error) + { + error = null; + var data = YamlConfigParser.Deserializer.Deserialize>(content); + + foreach (var property in typeof(CustomRole).GetProperties() + .Where(p => p.CanWrite && p is not null && p.GetType() is not null)) { - error = null; - Dictionary data = YamlConfigParser.Deserializer.Deserialize>(content); + var snakeCaseName = ToSnakeCase(property.Name); + if (!data.ContainsKey(snakeCaseName)) + error = + $"Given CustomRole doesn't contain the required property '{snakeCaseName}' ({ToSnakeCase(property.PropertyType.Name)})"; - foreach (PropertyInfo property in typeof(CustomRole).GetProperties().Where(p => p.CanWrite && p is not null && p.GetType() is not null)) - { - string snakeCaseName = ToSnakeCase(property.Name); - if (!data.ContainsKey(snakeCaseName)) - error = $"Given CustomRole doesn't contain the required property '{snakeCaseName}' ({ToSnakeCase(property.PropertyType.Name)})"; + if (error is not null) + break; + } - if (error is not null) - break; - } + return error is null; + } - return error is null; - } + private static string ToSnakeCase(string name) + { + if (string.IsNullOrEmpty(name)) + return name; - private static string ToSnakeCase(string name) + var result = StringBuilderPool.Shared.Rent(); + for (var i = 0; i < name.Length; i++) { - if (string.IsNullOrEmpty(name)) - return name; - - var result = StringBuilderPool.Shared.Rent(); - for (int i = 0; i < name.Length; i++) + var c = name[i]; + if (char.IsUpper(c)) { - char c = name[i]; - if (char.IsUpper(c)) - { - if (i > 0) - result.Append('_'); - result.Append(char.ToLowerInvariant(c)); - } - else - { - result.Append(c); - } + if (i > 0) + result.Append('_'); + result.Append(char.ToLowerInvariant(c)); + } + else + { + result.Append(c); } - return StringBuilderPool.Shared.ToStringReturn(result); } + + return StringBuilderPool.Shared.ToStringReturn(result); } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Compatibility/ErrorCustomRole.cs b/UncomplicatedCustomRoles/Compatibility/ErrorCustomRole.cs index a699a2d..ab31305 100644 --- a/UncomplicatedCustomRoles/Compatibility/ErrorCustomRole.cs +++ b/UncomplicatedCustomRoles/Compatibility/ErrorCustomRole.cs @@ -1,68 +1,68 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . */ -using PlayerRoles; using System; +using PlayerRoles; + +namespace UncomplicatedCustomRoles.Compatibility; -namespace UncomplicatedCustomRoles.Compatibility +public class ErrorCustomRole { - public class ErrorCustomRole + internal ErrorCustomRole(string path, string[] content, Exception exception, string message = null) { - /// - /// Gets the path of the CustomRole with the error - /// - public string Path { get; } + Path = path; + Content = content; + Exception = exception; + Message = message ?? CompatibilityManager.HandleErrorString(exception, true); + } - /// - /// Gets the content of the CustomRole with the error - /// - public string[] Content { get; } + /// + /// Gets the path of the CustomRole with the error + /// + public string Path { get; } - /// - /// Gets the error - /// - public Exception Exception { get; } + /// + /// Gets the content of the CustomRole with the error + /// + public string[] Content { get; } - /// - /// Gets the error message.

- /// Auto-generated if none is put - ///
- public string Message { get; } + /// + /// Gets the error + /// + public Exception Exception { get; } - /// - /// Gets the CustomRole Id - /// - public string Id => CompatibilityManager.GetRoleFileElement(Content, "id:"); + /// + /// Gets the error message.

+ /// Auto-generated if none is put + ///
+ public string Message { get; } - /// - /// Gets the CustomRole Name - /// - public string Name => CompatibilityManager.GetRoleFileElement(Content, "name:", false).Replace("'", string.Empty).Replace("\"", string.Empty); + /// + /// Gets the CustomRole Id + /// + public string Id => CompatibilityManager.GetRoleFileElement(Content, "id:"); - /// - /// Gets the CustomRole raw Role - /// - public string RawRole => CompatibilityManager.GetRoleFileElement(Content, "role:"); + /// + /// Gets the CustomRole Name + /// + public string Name => CompatibilityManager.GetRoleFileElement(Content, "name:", false).Replace("'", string.Empty) + .Replace("\"", string.Empty); - /// - /// Gets the CustomRole Role as a .

- /// if not found - ///
- public RoleTypeId Role => Enum.TryParse(RawRole, out RoleTypeId role) ? role : RoleTypeId.None; + /// + /// Gets the CustomRole raw Role + /// + public string RawRole => CompatibilityManager.GetRoleFileElement(Content, "role:"); - internal ErrorCustomRole(string path, string[] content, Exception exception, string message = null) - { - Path = path; - Content = content; - Exception = exception; - Message = message ?? CompatibilityManager.HandleErrorString(exception, true); - } - } -} + /// + /// Gets the CustomRole Role as a .

+ /// if not found + ///
+ public RoleTypeId Role => Enum.TryParse(RawRole, out RoleTypeId role) ? role : RoleTypeId.None; +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Compatibility/OutdatedCustomRole.cs b/UncomplicatedCustomRoles/Compatibility/OutdatedCustomRole.cs index 69f15a5..f7a1593 100644 --- a/UncomplicatedCustomRoles/Compatibility/OutdatedCustomRole.cs +++ b/UncomplicatedCustomRoles/Compatibility/OutdatedCustomRole.cs @@ -1,8 +1,8 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . @@ -11,21 +11,20 @@ using System; using UncomplicatedCustomRoles.API.Interfaces; -namespace UncomplicatedCustomRoles.Compatibility +namespace UncomplicatedCustomRoles.Compatibility; + +public class OutdatedCustomRole { - public class OutdatedCustomRole + internal OutdatedCustomRole(ICustomRole customRole, Version version, string path) { - public ICustomRole CustomRole { get; } + CustomRole = customRole; + Version = version; + Path = path; + } - public Version Version { get; } + public ICustomRole CustomRole { get; } - public string Path { get; } + public Version Version { get; } - internal OutdatedCustomRole(ICustomRole customRole, Version version, string path) - { - CustomRole = customRole; - Version = version; - Path = path; - } - } -} + public string Path { get; } +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Compatibility/PreviousVersionElements/BonolisSpawnBehaviour.cs b/UncomplicatedCustomRoles/Compatibility/PreviousVersionElements/BonolisSpawnBehaviour.cs index e86b54b..d5c1dd6 100644 --- a/UncomplicatedCustomRoles/Compatibility/PreviousVersionElements/BonolisSpawnBehaviour.cs +++ b/UncomplicatedCustomRoles/Compatibility/PreviousVersionElements/BonolisSpawnBehaviour.cs @@ -1,50 +1,39 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . */ -using PlayerRoles; using System.Collections.Generic; +using PlayerRoles; using UncomplicatedCustomRoles.API.Enums; using UncomplicatedCustomRoles.Compatibility.PreviousVersionElements.Enums; -namespace UncomplicatedCustomRoles.Compatibility.PreviousVersionElements -{ +namespace UncomplicatedCustomRoles.Compatibility.PreviousVersionElements; #nullable enable - internal class BonolisSpawnBehaviour - { - public List CanReplaceRoles { get; set; } = new() - { - RoleTypeId.ClassD - }; +internal class BonolisSpawnBehaviour +{ + public List CanReplaceRoles { get; set; } = [RoleTypeId.ClassD]; - public int MaxPlayers { get; set; } = 10; + public int MaxPlayers { get; set; } = 10; - public int MinPlayers { get; set; } = 1; + public int MinPlayers { get; set; } = 1; - public float SpawnChance { get; set; } = 60; + public float SpawnChance { get; set; } = 60; - public SpawnType Spawn { get; set; } = SpawnType.RoomsSpawn; + public SpawnType Spawn { get; set; } = SpawnType.RoomsSpawn; - public List SpawnZones { get; set; } = new(); + public List SpawnZones { get; set; } = []; - public List SpawnRooms { get; set; } = new() - { - ExiledRoomType.LczClassDSpawn - }; + public List SpawnRooms { get; set; } = [ExiledRoomType.LczClassDSpawn]; - public List SpawnRoles { get; set; } = new() - { - RoleTypeId.ClassD - }; + public List SpawnRoles { get; set; } = [RoleTypeId.ClassD]; - public List SpawnPoints { get; set; } = new(); + public List SpawnPoints { get; set; } = []; - public string? RequiredPermission { get; set; } = string.Empty; - } -} + public string? RequiredPermission { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Compatibility/PreviousVersionElements/Enums/ExiledAmmoType.cs b/UncomplicatedCustomRoles/Compatibility/PreviousVersionElements/Enums/ExiledAmmoType.cs index 032119f..025f5ac 100644 --- a/UncomplicatedCustomRoles/Compatibility/PreviousVersionElements/Enums/ExiledAmmoType.cs +++ b/UncomplicatedCustomRoles/Compatibility/PreviousVersionElements/Enums/ExiledAmmoType.cs @@ -6,15 +6,14 @@ // ----------------------------------------------------------------------- -namespace UncomplicatedCustomRoles.Compatibility.PreviousVersionElements.Enums +namespace UncomplicatedCustomRoles.Compatibility.PreviousVersionElements.Enums; + +public enum ExiledAmmoType { - public enum ExiledAmmoType - { - None, - Nato556, - Nato762, - Nato9, - Ammo12Gauge, - Ammo44Cal, - } -} + None, + Nato556, + Nato762, + Nato9, + Ammo12Gauge, + Ammo44Cal +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Compatibility/PreviousVersionElements/Enums/ExiledRoomType.cs b/UncomplicatedCustomRoles/Compatibility/PreviousVersionElements/Enums/ExiledRoomType.cs index d5ed344..a3a0e39 100644 --- a/UncomplicatedCustomRoles/Compatibility/PreviousVersionElements/Enums/ExiledRoomType.cs +++ b/UncomplicatedCustomRoles/Compatibility/PreviousVersionElements/Enums/ExiledRoomType.cs @@ -5,73 +5,71 @@ // // ----------------------------------------------------------------------- -namespace UncomplicatedCustomRoles.Compatibility.PreviousVersionElements.Enums -{ - public enum ExiledRoomType - { - Unknown, - LczArmory, - LczCurve, - LczStraight, - Lcz914, - LczCrossing, - LczTCross, - LczCafe, - LczPlants, - LczToilets, - LczAirlock, - Lcz173, - LczClassDSpawn, - LczCheckpointB, - LczGlassBox, - LczCheckpointA, - Hcz079, - HczEzCheckpointA, - HczEzCheckpointB, - HczArmory, - Hcz939, - HczHid, - Hcz049, - HczCrossing, - Hcz106, - HczNuke, - HczTesla, - HczCurve, - Hcz096, - EzVent, - EzIntercom, - EzGateA, - EzDownstairsPcs, - EzCurve, - EzPcs, - EzCrossing, - EzCollapsedTunnel, - EzConference, - EzChef, - EzStraight, - EzStraightColumn, - EzCafeteria, - EzUpstairsPcs, - EzGateB, - EzShelter, - Pocket, - Surface, - HczStraight, - EzTCross, - Lcz330, - EzCheckpointHallwayA, - EzCheckpointHallwayB, - HczTestRoom, - HczElevatorA, - HczElevatorB, - HczCrossRoomWater, - HczCornerDeep, - HczIntersectionJunk, - HczIntersection, - HczStraightC, - HczStraightPipeRoom, - HczStraightVariant, - EzSmallrooms, +namespace UncomplicatedCustomRoles.Compatibility.PreviousVersionElements.Enums; - } -} +public enum ExiledRoomType +{ + Unknown, + LczArmory, + LczCurve, + LczStraight, + Lcz914, + LczCrossing, + LczTCross, + LczCafe, + LczPlants, + LczToilets, + LczAirlock, + Lcz173, + LczClassDSpawn, + LczCheckpointB, + LczGlassBox, + LczCheckpointA, + Hcz079, + HczEzCheckpointA, + HczEzCheckpointB, + HczArmory, + Hcz939, + HczHid, + Hcz049, + HczCrossing, + Hcz106, + HczNuke, + HczTesla, + HczCurve, + Hcz096, + EzVent, + EzIntercom, + EzGateA, + EzDownstairsPcs, + EzCurve, + EzPcs, + EzCrossing, + EzCollapsedTunnel, + EzConference, + EzChef, + EzStraight, + EzStraightColumn, + EzCafeteria, + EzUpstairsPcs, + EzGateB, + EzShelter, + Pocket, + Surface, + HczStraight, + EzTCross, + Lcz330, + EzCheckpointHallwayA, + EzCheckpointHallwayB, + HczTestRoom, + HczElevatorA, + HczElevatorB, + HczCrossRoomWater, + HczCornerDeep, + HczIntersectionJunk, + HczIntersection, + HczStraightC, + HczStraightPipeRoom, + HczStraightVariant, + EzSmallrooms +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Compatibility/PreviousVersionElements/Enums/ExiledZoneType.cs b/UncomplicatedCustomRoles/Compatibility/PreviousVersionElements/Enums/ExiledZoneType.cs index a438a10..8e9c5e4 100644 --- a/UncomplicatedCustomRoles/Compatibility/PreviousVersionElements/Enums/ExiledZoneType.cs +++ b/UncomplicatedCustomRoles/Compatibility/PreviousVersionElements/Enums/ExiledZoneType.cs @@ -7,18 +7,16 @@ using System; -namespace UncomplicatedCustomRoles.Compatibility.PreviousVersionElements.Enums -{ - [Flags] - public enum ExiledZoneType - { - Unspecified = 0, - LightContainment = 1, - HeavyContainment = 2, - Entrance = 4, - Surface = 8, - Pocket = 16, - Other = 32, +namespace UncomplicatedCustomRoles.Compatibility.PreviousVersionElements.Enums; - } -} +[Flags] +public enum ExiledZoneType +{ + Unspecified = 0, + LightContainment = 1, + HeavyContainment = 2, + Entrance = 4, + Surface = 8, + Pocket = 16, + Other = 32 +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Compatibility/PreviousVersionElements/FossuonHealthBehaviour.cs b/UncomplicatedCustomRoles/Compatibility/PreviousVersionElements/FossuonHealthBehaviour.cs index d8d60b6..a1d5371 100644 --- a/UncomplicatedCustomRoles/Compatibility/PreviousVersionElements/FossuonHealthBehaviour.cs +++ b/UncomplicatedCustomRoles/Compatibility/PreviousVersionElements/FossuonHealthBehaviour.cs @@ -1,25 +1,24 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . */ -namespace UncomplicatedCustomRoles.Compatibility.PreviousVersionElements +namespace UncomplicatedCustomRoles.Compatibility.PreviousVersionElements; + +public class FossuonHealthBehaviour { - public class FossuonHealthBehaviour - { - public int Amount { get; set; } = 100; + public int Amount { get; set; } = 100; - public int Maximum { get; set; } = 100; + public int Maximum { get; set; } = 100; - public int HumeShield { get; set; } = 0; + public int HumeShield { get; set; } = 0; - public float HumeShieldRegenerationAmount { get; set; } = 2; + public float HumeShieldRegenerationAmount { get; set; } = 2; - public float HumeShieldRegenerationDelay { get; set; } = 7.5f; - } -} + public float HumeShieldRegenerationDelay { get; set; } = 7.5f; +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Compatibility/PreviousVersionRoles/BonolisCustomRole.cs b/UncomplicatedCustomRoles/Compatibility/PreviousVersionRoles/BonolisCustomRole.cs index 76171cb..e4ef725 100644 --- a/UncomplicatedCustomRoles/Compatibility/PreviousVersionRoles/BonolisCustomRole.cs +++ b/UncomplicatedCustomRoles/Compatibility/PreviousVersionRoles/BonolisCustomRole.cs @@ -1,15 +1,15 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . */ -using PlayerRoles; using System.Collections.Generic; +using PlayerRoles; using UncomplicatedCustomRoles.API.Features; using UncomplicatedCustomRoles.API.Features.Behaviour; using UncomplicatedCustomRoles.Compatibility.PreviousVersionElements; @@ -18,129 +18,131 @@ using UncomplicatedCustomRoles.Manager; using UnityEngine; -namespace UncomplicatedCustomRoles.Compatibility.PreviousVersionRoles -{ +namespace UncomplicatedCustomRoles.Compatibility.PreviousVersionRoles; #nullable enable - internal class BonolisCustomRole : IPreviousVersionRole - { - public virtual int Id { get; set; } = 1; +internal class BonolisCustomRole : IPreviousVersionRole +{ + public virtual int Id { get; set; } = 1; - public virtual string Name { get; set; } = "Janitor"; + public virtual string Name { get; set; } = "Janitor"; - public virtual bool OverrideRoleName { get; set; } = false; + public virtual bool OverrideRoleName { get; set; } = false; - public virtual string? Nickname { get; set; } = "D-%dnumber%"; + public virtual string? Nickname { get; set; } = "D-%dnumber%"; - public virtual string CustomInfo { get; set; } = "Janitor"; + public virtual string CustomInfo { get; set; } = "Janitor"; - public virtual string BadgeName { get; set; } = "Janitor"; + public virtual string BadgeName { get; set; } = "Janitor"; - public virtual string BadgeColor { get; set; } = "pumpkin"; + public virtual string BadgeColor { get; set; } = "pumpkin"; - public virtual RoleTypeId Role { get; set; } = RoleTypeId.ClassD; + public virtual RoleTypeId Role { get; set; } = RoleTypeId.ClassD; - public virtual Team? Team { get; set; } = null; + public virtual Team? Team { get; set; } = null; - public virtual RoleTypeId RoleAppearance { get; set; } = RoleTypeId.ClassD; + public virtual RoleTypeId RoleAppearance { get; set; } = RoleTypeId.ClassD; - public virtual List IsFriendOf { get; set; } = new(); + public virtual List IsFriendOf { get; set; } = []; - public virtual HealthBehaviour Health { get; set; } = new(); + public virtual HealthBehaviour Health { get; set; } = new(); - public virtual AhpBehaviour Ahp { get; set; } = new(); + public virtual AhpBehaviour Ahp { get; set; } = new(); - public virtual HumeShieldBehaviour HumeShield { get; set; } = new(); + public virtual HumeShieldBehaviour HumeShield { get; set; } = new(); - public virtual List? Effects { get; set; } = new(); + public virtual List? Effects { get; set; } = []; - public virtual StaminaBehaviour Stamina { get; set; } = new(); + public virtual StaminaBehaviour Stamina { get; set; } = new(); - public virtual int MaxScp330Candies { get; set; } = 2; + public virtual int MaxScp330Candies { get; set; } = 2; - public virtual bool CanEscape { get; set; } = true; + public virtual bool CanEscape { get; set; } = true; - public virtual Dictionary RoleAfterEscape { get; set; } = new() + public virtual Dictionary RoleAfterEscape { get; set; } = new() + { { - { - "default", - "InternalRole Spectator" - }, - { - "cuffed by InternalTeam ChaosInsurgency", - "InternalRole ClassD" - } - }; + "default", + "InternalRole Spectator" + }, + { + "cuffed by InternalTeam ChaosInsurgency", + "InternalRole ClassD" + } + }; - public virtual Vector3 Scale { get; set; } = Vector3.one; + public virtual Vector3 Scale { get; set; } = Vector3.one; - public virtual string SpawnBroadcast { get; set; } = "You are a Janitor!\nClean the Light Containment Zone!"; + public virtual string SpawnBroadcast { get; set; } = + "You are a Janitor!\nClean the Light Containment Zone!"; - public virtual ushort SpawnBroadcastDuration { get; set; } = 5; + public virtual ushort SpawnBroadcastDuration { get; set; } = 5; - public virtual string SpawnHint { get; set; } = "This hint will be shown when you will spawn as a Janitor!"; + public virtual string SpawnHint { get; set; } = "This hint will be shown when you will spawn as a Janitor!"; - public virtual float SpawnHintDuration { get; set; } = 5; + public virtual float SpawnHintDuration { get; set; } = 5; - public virtual Dictionary CustomInventoryLimits { get; set; } = new(); + public virtual Dictionary CustomInventoryLimits { get; set; } = new(); - public virtual List Inventory { get; set; } = new() - { - ItemType.Flashlight, - ItemType.KeycardJanitor - }; + public virtual List Inventory { get; set; } = + [ + ItemType.Flashlight, + ItemType.KeycardJanitor + ]; - public virtual List CustomItemsInventory { get; set; } = new(); + public virtual List CustomItemsInventory { get; set; } = []; - public virtual Dictionary Ammo { get; set; } = new() + public virtual Dictionary Ammo { get; set; } = new() + { { - { - ExiledAmmoType.Nato9, - 10 - } - }; + ExiledAmmoType.Nato9, + 10 + } + }; - public virtual float DamageMultiplier { get; set; } = 1; + public virtual float DamageMultiplier { get; set; } = 1; - public virtual BonolisSpawnBehaviour? SpawnSettings { get; set; } = new(); + public virtual BonolisSpawnBehaviour? SpawnSettings { get; set; } = new(); - public virtual List? CustomFlags { get; set; } = null; + public virtual List? CustomFlags { get; set; } = null; - public virtual bool IgnoreSpawnSystem { get; set; } = false; + public virtual bool IgnoreSpawnSystem { get; set; } = false; - public CustomRole ToCustomRole() + public CustomRole ToCustomRole() + { + return new CustomRole { - return new() - { - Id = Id, - Name = Name, - OverrideRoleName = OverrideRoleName, - Nickname = Nickname, - CustomInfo = CustomInfo, - BadgeName = BadgeName, - BadgeColor = BadgeColor, - Role = Role, - Team = Team, - RoleAppearance = RoleAppearance, - IsFriendOf = IsFriendOf, - Health = Health, - Ahp = Ahp, - HumeShield = HumeShield, - Effects = Effects, - Stamina = Stamina, - MaxScp330Candies = MaxScp330Candies, - CanEscape = CanEscape, - RoleAfterEscape = RoleAfterEscape, - Scale = Scale, - SpawnBroadcast = SpawnBroadcast, - SpawnBroadcastDuration = SpawnBroadcastDuration, - SpawnHint = SpawnHint, - SpawnHintDuration = SpawnHintDuration, - CustomInventoryLimits = CustomInventoryLimits, - Inventory = Inventory, - CustomItemsInventory = CustomItemsInventory, - Ammo = Ammo.ConvertItemTypes(), - DamageMultiplier = DamageMultiplier, - SpawnSettings = SpawnSettings is null ? null : new() + Id = Id, + Name = Name, + OverrideRoleName = OverrideRoleName, + Nickname = Nickname, + CustomInfo = CustomInfo, + BadgeName = BadgeName, + BadgeColor = BadgeColor, + Role = Role, + Team = Team, + RoleAppearance = RoleAppearance, + IsFriendOf = IsFriendOf, + Health = Health, + Ahp = Ahp, + HumeShield = HumeShield, + Effects = Effects, + Stamina = Stamina, + MaxScp330Candies = MaxScp330Candies, + CanEscape = CanEscape, + RoleAfterEscape = RoleAfterEscape, + Scale = Scale, + SpawnBroadcast = SpawnBroadcast, + SpawnBroadcastDuration = SpawnBroadcastDuration, + SpawnHint = SpawnHint, + SpawnHintDuration = SpawnHintDuration, + CustomInventoryLimits = CustomInventoryLimits, + Inventory = Inventory, + CustomItemsInventory = CustomItemsInventory, + Ammo = Ammo.ConvertItemTypes(), + DamageMultiplier = DamageMultiplier, + SpawnSettings = SpawnSettings is null + ? null + : new SpawnBehaviour { CanReplaceRoles = SpawnSettings.CanReplaceRoles, MaxPlayers = SpawnSettings.MaxPlayers, @@ -153,9 +155,8 @@ public CustomRole ToCustomRole() SpawnPoints = SpawnSettings.SpawnPoints, RequiredPermission = new object() }, - CustomFlags = CustomFlags, - IgnoreSpawnSystem = IgnoreSpawnSystem - }; - } + CustomFlags = CustomFlags, + IgnoreSpawnSystem = IgnoreSpawnSystem + }; } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Compatibility/PreviousVersionRoles/FossuonCustomRole.cs b/UncomplicatedCustomRoles/Compatibility/PreviousVersionRoles/FossuonCustomRole.cs index f0f30c7..26dc001 100644 --- a/UncomplicatedCustomRoles/Compatibility/PreviousVersionRoles/FossuonCustomRole.cs +++ b/UncomplicatedCustomRoles/Compatibility/PreviousVersionRoles/FossuonCustomRole.cs @@ -1,15 +1,15 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . */ -using PlayerRoles; using System.Collections.Generic; +using PlayerRoles; using UncomplicatedCustomRoles.API.Features; using UncomplicatedCustomRoles.API.Features.Behaviour; using UncomplicatedCustomRoles.Compatibility.PreviousVersionElements; @@ -18,151 +18,150 @@ using UncomplicatedCustomRoles.Manager; using UnityEngine; -namespace UncomplicatedCustomRoles.Compatibility.PreviousVersionRoles -{ +namespace UncomplicatedCustomRoles.Compatibility.PreviousVersionRoles; #nullable enable - /// - /// Custom Role of the version v6.0.0 "Fossuon" - /// - public class FossuonCustomRole : IPreviousVersionRole - { - public virtual int Id { get; set; } = 1; +/// +/// Custom Role of the version v6.0.0 "Fossuon" +/// +public class FossuonCustomRole : IPreviousVersionRole +{ + public virtual int Id { get; set; } = 1; - public virtual string Name { get; set; } = "Janitor"; + public virtual string Name { get; set; } = "Janitor"; - public virtual bool OverrideRoleName { get; set; } = false; + public virtual bool OverrideRoleName { get; set; } = false; - public virtual string? Nickname { get; set; } = "D-%dnumber%"; + public virtual string? Nickname { get; set; } = "D-%dnumber%"; - public virtual string CustomInfo { get; set; } = "Janitor"; + public virtual string CustomInfo { get; set; } = "Janitor"; - public virtual string BadgeName { get; set; } = "Janitor"; + public virtual string BadgeName { get; set; } = "Janitor"; - public virtual string BadgeColor { get; set; } = "pumpkin"; + public virtual string BadgeColor { get; set; } = "pumpkin"; - public virtual RoleTypeId Role { get; set; } = RoleTypeId.ClassD; + public virtual RoleTypeId Role { get; set; } = RoleTypeId.ClassD; - public virtual Team? Team { get; set; } = null; + public virtual Team? Team { get; set; } = null; - public virtual RoleTypeId RoleAppearance { get; set; } = RoleTypeId.ClassD; + public virtual RoleTypeId RoleAppearance { get; set; } = RoleTypeId.ClassD; - public virtual List IsFriendOf { get; set; } = new(); + public virtual List IsFriendOf { get; set; } = []; - public virtual FossuonHealthBehaviour Health { get; set; } = new(); + public virtual FossuonHealthBehaviour Health { get; set; } = new(); - public virtual AhpBehaviour Ahp { get; set; } = new(); + public virtual AhpBehaviour Ahp { get; set; } = new(); - public virtual List? Effects { get; set; } = new(); + public virtual List? Effects { get; set; } = []; - public virtual StaminaBehaviour Stamina { get; set; } = new(); + public virtual StaminaBehaviour Stamina { get; set; } = new(); - public virtual int MaxScp330Candies { get; set; } = 2; + public virtual int MaxScp330Candies { get; set; } = 2; - public virtual bool CanEscape { get; set; } = true; + public virtual bool CanEscape { get; set; } = true; - public virtual Dictionary RoleAfterEscape { get; set; } = new() + public virtual Dictionary RoleAfterEscape { get; set; } = new() + { { - { - "default", - "InternalRole Spectator" - }, - { - "cuffed by InternalTeam ChaosInsurgency", - "InternalRole ClassD" - } - }; + "default", + "InternalRole Spectator" + }, + { + "cuffed by InternalTeam ChaosInsurgency", + "InternalRole ClassD" + } + }; - public virtual Vector3 Scale { get; set; } = Vector3.one; + public virtual Vector3 Scale { get; set; } = Vector3.one; - public virtual string SpawnBroadcast { get; set; } = "You are a Janitor!\nClean the Light Containment Zone!"; + public virtual string SpawnBroadcast { get; set; } = + "You are a Janitor!\nClean the Light Containment Zone!"; - public virtual ushort SpawnBroadcastDuration { get; set; } = 5; + public virtual ushort SpawnBroadcastDuration { get; set; } = 5; - public virtual string SpawnHint { get; set; } = "This hint will be shown when you will spawn as a Janitor!"; + public virtual string SpawnHint { get; set; } = "This hint will be shown when you will spawn as a Janitor!"; - public virtual float SpawnHintDuration { get; set; } = 5; + public virtual float SpawnHintDuration { get; set; } = 5; - public virtual Dictionary CustomInventoryLimits { get; set; } = new() + public virtual Dictionary CustomInventoryLimits { get; set; } = new() + { { - { - ItemCategory.Medical, - 2 - } - }; + ItemCategory.Medical, + 2 + } + }; - public virtual List Inventory { get; set; } = new() - { - ItemType.Flashlight, - ItemType.KeycardJanitor - }; + public virtual List Inventory { get; set; } = + [ + ItemType.Flashlight, + ItemType.KeycardJanitor + ]; - public virtual List CustomItemsInventory { get; set; } = new(); + public virtual List CustomItemsInventory { get; set; } = []; - public virtual Dictionary Ammo { get; set; } = new() + public virtual Dictionary Ammo { get; set; } = new() + { { - { - ExiledAmmoType.Nato9, - 10 - } - }; + ExiledAmmoType.Nato9, + 10 + } + }; - public virtual float DamageMultiplier { get; set; } = 1; + public virtual float DamageMultiplier { get; set; } = 1; - public virtual SpawnBehaviour? SpawnSettings { get; set; } = new(); + public virtual SpawnBehaviour? SpawnSettings { get; set; } = new(); - public virtual List? CustomFlags { get; set; } = null; + public virtual List? CustomFlags { get; set; } = null; - public virtual bool IgnoreSpawnSystem { get; set; } = false; + public virtual bool IgnoreSpawnSystem { get; set; } = false; - public CustomRole ToCustomRole() + public CustomRole ToCustomRole() + { + return new CustomRole { - return new() + Id = Id, + Name = Name, + OverrideRoleName = OverrideRoleName, + Nickname = Nickname, + CustomInfo = CustomInfo, + BadgeName = BadgeName, + BadgeColor = BadgeColor, + Role = Role, + Team = Team, + RoleAppearance = RoleAppearance, + IsFriendOf = IsFriendOf, + Health = new HealthBehaviour { - Id = Id, - Name = Name, - OverrideRoleName = OverrideRoleName, - Nickname = Nickname, - CustomInfo = CustomInfo, - BadgeName = BadgeName, - BadgeColor = BadgeColor, - Role = Role, - Team = Team, - RoleAppearance = RoleAppearance, - IsFriendOf = IsFriendOf, - Health = new() - { - Amount = Health.Amount, - Maximum = Health.Maximum - }, - Ahp = Ahp, - HumeShield = new() - { - Amount = Health.HumeShield, - Maximum = Health.HumeShield, - RegenerationAmount = Health.HumeShieldRegenerationAmount, - RegenerationDelay = Health.HumeShieldRegenerationDelay, - RegenerationSpeed = 0f - }, - Effects = Effects, - Stamina = Stamina, - MaxScp330Candies = MaxScp330Candies, - CanEscape = CanEscape, - RoleAfterEscape = RoleAfterEscape, - Scale = Scale, - SpawnBroadcast = SpawnBroadcast, - SpawnBroadcastDuration = SpawnBroadcastDuration, - SpawnHint = SpawnHint, - SpawnHintDuration = SpawnHintDuration, - CustomInventoryLimits = CustomInventoryLimits, - Inventory = Inventory, - CustomItemsInventory = CustomItemsInventory, - Ammo = Ammo.ConvertItemTypes(), - DamageMultiplier = DamageMultiplier, - SpawnSettings = SpawnSettings, - CustomFlags = CustomFlags, - IgnoreSpawnSystem = IgnoreSpawnSystem - }; - } + Amount = Health.Amount, + Maximum = Health.Maximum + }, + Ahp = Ahp, + HumeShield = new HumeShieldBehaviour + { + Amount = Health.HumeShield, + Maximum = Health.HumeShield, + RegenerationAmount = Health.HumeShieldRegenerationAmount, + RegenerationDelay = Health.HumeShieldRegenerationDelay, + RegenerationSpeed = 0f + }, + Effects = Effects, + Stamina = Stamina, + MaxScp330Candies = MaxScp330Candies, + CanEscape = CanEscape, + RoleAfterEscape = RoleAfterEscape, + Scale = Scale, + SpawnBroadcast = SpawnBroadcast, + SpawnBroadcastDuration = SpawnBroadcastDuration, + SpawnHint = SpawnHint, + SpawnHintDuration = SpawnHintDuration, + CustomInventoryLimits = CustomInventoryLimits, + Inventory = Inventory, + CustomItemsInventory = CustomItemsInventory, + Ammo = Ammo.ConvertItemTypes(), + DamageMultiplier = DamageMultiplier, + SpawnSettings = SpawnSettings, + CustomFlags = CustomFlags, + IgnoreSpawnSystem = IgnoreSpawnSystem + }; } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Compatibility/PreviousVersionRoles/IPreviousVersionRole.cs b/UncomplicatedCustomRoles/Compatibility/PreviousVersionRoles/IPreviousVersionRole.cs index 951bc83..2944b99 100644 --- a/UncomplicatedCustomRoles/Compatibility/PreviousVersionRoles/IPreviousVersionRole.cs +++ b/UncomplicatedCustomRoles/Compatibility/PreviousVersionRoles/IPreviousVersionRole.cs @@ -1,8 +1,8 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . @@ -10,10 +10,9 @@ using UncomplicatedCustomRoles.API.Features; -namespace UncomplicatedCustomRoles.Compatibility.PreviousVersionRoles +namespace UncomplicatedCustomRoles.Compatibility.PreviousVersionRoles; + +internal interface IPreviousVersionRole { - interface IPreviousVersionRole - { - public CustomRole ToCustomRole(); - } -} + public CustomRole ToCustomRole(); +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Compatibility/PreviousVersionRoles/PreviousVersionRole.cs b/UncomplicatedCustomRoles/Compatibility/PreviousVersionRoles/PreviousVersionRole.cs index c90d3fb..577eb22 100644 --- a/UncomplicatedCustomRoles/Compatibility/PreviousVersionRoles/PreviousVersionRole.cs +++ b/UncomplicatedCustomRoles/Compatibility/PreviousVersionRoles/PreviousVersionRole.cs @@ -1,168 +1,167 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . */ -using PlayerRoles; -using System.Collections.Generic; using System; +using System.Collections.Generic; +using PlayerRoles; using UncomplicatedCustomRoles.API.Enums; -using UncomplicatedCustomRoles.API.Features.Behaviour; -using UnityEngine; -using UncomplicatedCustomRoles.Manager; using UncomplicatedCustomRoles.API.Features; +using UncomplicatedCustomRoles.API.Features.Behaviour; using UncomplicatedCustomRoles.Compatibility.PreviousVersionElements.Enums; using UncomplicatedCustomRoles.Extensions; +using UncomplicatedCustomRoles.Manager; +using UnityEngine; -namespace UncomplicatedCustomRoles.Compatibility.PreviousVersionRoles -{ +namespace UncomplicatedCustomRoles.Compatibility.PreviousVersionRoles; #nullable enable - internal class PreviousVersionRole : IPreviousVersionRole - { - public virtual int Id { get; set; } = 1; +internal class PreviousVersionRole : IPreviousVersionRole +{ + public virtual int Id { get; set; } = 1; - public virtual string Name { get; set; } = "Janitor"; + public virtual string Name { get; set; } = "Janitor"; - public virtual bool OverrideRoleName { get; set; } = false; + public virtual bool OverrideRoleName { get; set; } = false; - public virtual string? Nickname { get; set; } = "D-%dnumber%"; + public virtual string? Nickname { get; set; } = "D-%dnumber%"; - public virtual string CustomInfo { get; set; } = "Janitor"; + public virtual string CustomInfo { get; set; } = "Janitor"; - public virtual string BadgeName { get; set; } = "Janitor"; + public virtual string BadgeName { get; set; } = "Janitor"; - public virtual string BadgeColor { get; set; } = "pumpkin"; + public virtual string BadgeColor { get; set; } = "pumpkin"; - public virtual RoleTypeId Role { get; set; } = RoleTypeId.ClassD; + public virtual RoleTypeId Role { get; set; } = RoleTypeId.ClassD; - public virtual Team? Team { get; set; } = null; + public virtual Team? Team { get; set; } = null; - public virtual RoleTypeId RoleAppearance { get; set; } = RoleTypeId.ClassD; + public virtual RoleTypeId RoleAppearance { get; set; } = RoleTypeId.ClassD; - public virtual List IsFriendOf { get; set; } = new(); + public virtual List IsFriendOf { get; set; } = []; - public virtual HealthBehaviour Health { get; set; } = new(); + public virtual HealthBehaviour Health { get; set; } = new(); - public virtual AhpBehaviour Ahp { get; set; } = new(); + public virtual AhpBehaviour Ahp { get; set; } = new(); - public virtual List? Effects { get; set; } = new(); + public virtual List? Effects { get; set; } = []; - public virtual StaminaBehaviour Stamina { get; set; } = new(); + public virtual StaminaBehaviour Stamina { get; set; } = new(); - public virtual int MaxScp330Candies { get; set; } = 2; + public virtual int MaxScp330Candies { get; set; } = 2; - public virtual bool CanEscape { get; set; } = true; + public virtual bool CanEscape { get; set; } = true; - public virtual Dictionary RoleAfterEscape { get; set; } = new() + public virtual Dictionary RoleAfterEscape { get; set; } = new() + { { - { - "default", - "InternalRole Spectator" - }, - { - "cuffed by InternalTeam ChaosInsurgency", - "InternalRole ClassD" - } - }; + "default", + "InternalRole Spectator" + }, + { + "cuffed by InternalTeam ChaosInsurgency", + "InternalRole ClassD" + } + }; - public virtual Vector3 Scale { get; set; } = Vector3.one; + public virtual Vector3 Scale { get; set; } = Vector3.one; - public virtual string SpawnBroadcast { get; set; } = "You are a Janitor!\nClean the Light Containment Zone!"; + public virtual string SpawnBroadcast { get; set; } = + "You are a Janitor!\nClean the Light Containment Zone!"; - public virtual ushort SpawnBroadcastDuration { get; set; } = 5; + public virtual ushort SpawnBroadcastDuration { get; set; } = 5; - public virtual string SpawnHint { get; set; } = "This hint will be shown when you will spawn as a Janitor!"; + public virtual string SpawnHint { get; set; } = "This hint will be shown when you will spawn as a Janitor!"; - public virtual float SpawnHintDuration { get; set; } = 5; + public virtual float SpawnHintDuration { get; set; } = 5; - public virtual Dictionary CustomInventoryLimits { get; set; } = new() + public virtual Dictionary CustomInventoryLimits { get; set; } = new() + { { - { - ItemCategory.Medical, - 2 - } - }; + ItemCategory.Medical, + 2 + } + }; - public virtual List Inventory { get; set; } = new() - { - ItemType.Flashlight, - ItemType.KeycardJanitor - }; + public virtual List Inventory { get; set; } = + [ + ItemType.Flashlight, + ItemType.KeycardJanitor + ]; - public virtual List CustomItemsInventory { get; set; } = new(); + public virtual List CustomItemsInventory { get; set; } = []; - public virtual Dictionary Ammo { get; set; } = new() + public virtual Dictionary Ammo { get; set; } = new() + { { - { - ExiledAmmoType.Nato9, - 10 - } - }; + ExiledAmmoType.Nato9, + 10 + } + }; - public virtual float DamageMultiplier { get; set; } = 1; + public virtual float DamageMultiplier { get; set; } = 1; - public virtual SpawnBehaviour? SpawnSettings { get; set; } = new(); + public virtual SpawnBehaviour? SpawnSettings { get; set; } = new(); - public virtual CustomFlags? CustomFlags { get; set; } = null; + public virtual CustomFlags? CustomFlags { get; set; } = null; - public virtual bool IgnoreSpawnSystem { get; set; } = false; + public virtual bool IgnoreSpawnSystem { get; set; } = false; - public CustomRole ToCustomRole() + public CustomRole ToCustomRole() + { + return new CustomRole { - return new() - { - Id = Id, - Name = Name, - OverrideRoleName = OverrideRoleName, - Nickname = Nickname, - CustomInfo = CustomInfo, - BadgeName = BadgeName, - BadgeColor = BadgeColor, - Role = Role, - Team = Team, - RoleAppearance = RoleAppearance, - IsFriendOf = IsFriendOf, - Health = Health, - Ahp = Ahp, - Effects = Effects, - Stamina = Stamina, - MaxScp330Candies = MaxScp330Candies, - CanEscape = CanEscape, - RoleAfterEscape = RoleAfterEscape, - Scale = Scale, - SpawnBroadcast = SpawnBroadcast, - SpawnBroadcastDuration = SpawnBroadcastDuration, - SpawnHint = SpawnHint, - SpawnHintDuration = SpawnHintDuration, - CustomInventoryLimits = CustomInventoryLimits, - Inventory = Inventory, - CustomItemsInventory = CustomItemsInventory, - Ammo = Ammo.ConvertItemTypes(), - DamageMultiplier = DamageMultiplier, - SpawnSettings = SpawnSettings, - CustomFlags = CustomFlagsConversion(), - IgnoreSpawnSystem = IgnoreSpawnSystem - }; - } + Id = Id, + Name = Name, + OverrideRoleName = OverrideRoleName, + Nickname = Nickname, + CustomInfo = CustomInfo, + BadgeName = BadgeName, + BadgeColor = BadgeColor, + Role = Role, + Team = Team, + RoleAppearance = RoleAppearance, + IsFriendOf = IsFriendOf, + Health = Health, + Ahp = Ahp, + Effects = Effects, + Stamina = Stamina, + MaxScp330Candies = MaxScp330Candies, + CanEscape = CanEscape, + RoleAfterEscape = RoleAfterEscape, + Scale = Scale, + SpawnBroadcast = SpawnBroadcast, + SpawnBroadcastDuration = SpawnBroadcastDuration, + SpawnHint = SpawnHint, + SpawnHintDuration = SpawnHintDuration, + CustomInventoryLimits = CustomInventoryLimits, + Inventory = Inventory, + CustomItemsInventory = CustomItemsInventory, + Ammo = Ammo.ConvertItemTypes(), + DamageMultiplier = DamageMultiplier, + SpawnSettings = SpawnSettings, + CustomFlags = CustomFlagsConversion(), + IgnoreSpawnSystem = IgnoreSpawnSystem + }; + } - private List? CustomFlagsConversion() - { - if (CustomFlags is null) - return null; + private List? CustomFlagsConversion() + { + if (CustomFlags is null) + return null; - List flags = new(); - foreach (CustomFlags flag in Enum.GetValues(typeof(CustomFlags))) - if ((CustomFlags & flag) == flag) - flags.Add(flag.ToString()); + List flags = []; + foreach (CustomFlags flag in Enum.GetValues(typeof(CustomFlags))) + if ((CustomFlags & flag) == flag) + flags.Add(flag.ToString()); - flags.Remove("None"); - return flags; - } + flags.Remove("None"); + return flags; } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Config.cs b/UncomplicatedCustomRoles/Config.cs index 817d255..b9fc71a 100644 --- a/UncomplicatedCustomRoles/Config.cs +++ b/UncomplicatedCustomRoles/Config.cs @@ -1,8 +1,8 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . @@ -11,59 +11,64 @@ using System.Collections.Generic; using System.ComponentModel; -namespace UncomplicatedCustomRoles +namespace UncomplicatedCustomRoles; + +internal class Config { - internal class Config - { - [Description("Is the debug mode enabled?")] - public bool Debug { get; set; } = false; + [Description("Is the debug mode enabled?")] + public bool Debug { get; set; } = false; + + [Description( + "Ignore spawns that are not included in waves and initial spawn? So when you do a forcelass an UCR role won't spawn in any case")] + public bool AllowOnlyNaturalSpawns { get; set; } = true; - [Description("Ignore spawns that are not included in waves and initial spawn? So when you do a forcelass an UCR role won't spawn in any case")] - public bool AllowOnlyNaturalSpawns { get; set; } = true; + [Description( + "If true the plugin will apply the 'nickname' param in each role config to every player. Disable this if you encounter problems or bugs!")] + public bool AllowNicknameEdit { get; set; } = true; - [Description("If true the plugin will apply the 'nickname' param in each role config to every player. Disable this if you encounter problems or bugs!")] - public bool AllowNicknameEdit { get; set; } = true; + [Description("If true UCR will override the name given by RPNames")] + public bool OverrideRpNames { get; set; } = true; - [Description("If true UCR will override the name given by RPNames")] - public bool OverrideRpNames { get; set; } = true; + [Description("Do enable the basic UCR logs?")] + public bool EnableBasicLogs { get; set; } = true; - [Description("Do enable the basic UCR logs?")] - public bool EnableBasicLogs { get; set; } = true; + [Description( + "If true the UCS credit tag system won't be activated. PLEASE DON'T DEACTIVATE IT as LOTS OF PEOPLE WORKED ON THIS PLUGIN completly for FREE!")] + public bool EnableCreditTags { get; set; } = true; - [Description("If true the UCS credit tag system won't be activated. PLEASE DON'T DEACTIVATE IT as LOTS OF PEOPLE WORKED ON THIS PLUGIN completly for FREE!")] - public bool EnableCreditTags { get; set; } = true; - - [Description("If true the plugin will send anonymous data to our central server every 60 sec.")] - public bool EnableTelemetry { get; set; } = true; + [Description("If true the plugin will send anonymous data to our central server every 60 sec.")] + public bool EnableTelemetry { get; set; } = true; - [Description("Whether the NPCs can naturally spawn custom roles")] - public bool IgnoreNpcs { get; set; } = true; + [Description("Whether the NPCs can naturally spawn custom roles")] + public bool IgnoreNpcs { get; set; } = true; - [Description("Whether you want your spawnpoints to be hosted inside our central server or locally in the configs folder")] - public bool LocalSpawnPoints { get; set; } = false; + [Description( + "Whether you want your spawnpoints to be hosted inside our central server or locally in the configs folder")] + public bool LocalSpawnPoints { get; set; } = false; - [Description("Auto load the Custom Role ID from the file, bypassing YAML")] - public bool UseIdFixer { get; set; } = false; - - [Description("The content that will be replaced instead of {CUSTOM_ROLE} on your RespawnTimer display config if the current spectated player is a custom role. %customrole% is the role name")] - public string RespawnTimerContent { get; set; } = "Player has custom role %customrole%"; + [Description("Auto load the Custom Role ID from the file, bypassing YAML")] + public bool UseIdFixer { get; set; } = false; - [Description("The content that will be replaced instead of {CUSTOM_ROLE} on your RespawnTimer display config if the current spectated player is not a custom role.")] - public string RespawnTimerContentEmpty { get; set; } = "Player has no custom role"; + [Description( + "The content that will be replaced instead of {CUSTOM_ROLE} on your RespawnTimer display config if the current spectated player is a custom role. %customrole% is the role name")] + public string RespawnTimerContent { get; set; } = "Player has custom role %customrole%"; - [Description("If the role Id is here UCR won't take the role name but the following config")] - public Dictionary HiddenRolesId { get; set; } = new() - { - { 1, new HiddenRoleInformation() } - }; - } + [Description( + "The content that will be replaced instead of {CUSTOM_ROLE} on your RespawnTimer display config if the current spectated player is not a custom role.")] + public string RespawnTimerContentEmpty { get; set; } = "Player has no custom role"; - public class HiddenRoleInformation + [Description("If the role Id is here UCR won't take the role name but the following config")] + public Dictionary HiddenRolesId { get; set; } = new() { - [Description("This custom role will be visible only for those who are in Overwatch.")] - public bool OnlyVisibleOnOverwatch { get; set; } = false; + { 1, new HiddenRoleInformation() } + }; +} + +public class HiddenRoleInformation +{ + [Description("This custom role will be visible only for those who are in Overwatch.")] + public bool OnlyVisibleOnOverwatch { get; set; } = false; - [Description("Empty to get the current display role.")] - public string RoleNameWhenHidden { get; set; } = ""; - } + [Description("Empty to get the current display role.")] + public string RoleNameWhenHidden { get; set; } = ""; } \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Events/EventHandlerBase.cs b/UncomplicatedCustomRoles/Events/EventHandlerBase.cs index 1bb7621..3475f3f 100644 --- a/UncomplicatedCustomRoles/Events/EventHandlerBase.cs +++ b/UncomplicatedCustomRoles/Events/EventHandlerBase.cs @@ -8,56 +8,59 @@ * If not, see . */ -using System.Collections.Concurrent; using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using UncomplicatedCustomRoles.API.Features.CustomModules; -namespace UncomplicatedCustomRoles.Events +namespace UncomplicatedCustomRoles.Events; + +internal abstract class EventHandlerBase { - internal abstract class EventHandlerBase - { - internal static ConcurrentDictionary, Dictionary, bool>> RespawnInventoryQueue { get; } = new(); + private static readonly List _list = []; - internal static HashSet RagdollAppearanceQueue { get; } = new(); + internal static ConcurrentDictionary, Dictionary, bool>> + RespawnInventoryQueue { get; } = new(); - internal static HashSet FirstRoundPlayers { get; } = new(); + internal static HashSet RagdollAppearanceQueue { get; } = []; - internal static ConcurrentDictionary> TerminationQueue { get; } = new(); + internal static HashSet FirstRoundPlayers { get; } = []; - internal static bool Started { get; set; } = false; + internal static ConcurrentDictionary> TerminationQueue { get; } = + new(); - private static readonly List _list = new(); + internal static bool Started { get; set; } = false; - public static void Register(EventHandlerBase e) - { - _list.Add(e); - e.OnRegistered(); - } + public static void Register(EventHandlerBase e) + { + _list.Add(e); + e.OnRegistered(); + } - public static void Register(IEnumerable e) - { - foreach (EventHandlerBase eventHandlerBase in e) - Register(eventHandlerBase); - } + public static void Register(IEnumerable e) + { + foreach (var eventHandlerBase in e) + Register(eventHandlerBase); + } - public static void Unregister(EventHandlerBase e) - { - e.OnUnregistered(); - _list.Remove(e); - } + public static void Unregister(EventHandlerBase e) + { + e.OnUnregistered(); + _list.Remove(e); + } - public static void UnregisterAll() - { - foreach (EventHandlerBase e in _list.ToList()) - Unregister(e); - } + public static void UnregisterAll() + { + foreach (var e in _list.ToList()) + Unregister(e); + } - internal virtual void OnRegistered() - { } + internal virtual void OnRegistered() + { + } - internal virtual void OnUnregistered() - { } + internal virtual void OnUnregistered() + { } } \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Events/PlayerEventHandler.cs b/UncomplicatedCustomRoles/Events/PlayerEventHandler.cs index ea4d41b..c21815c 100644 --- a/UncomplicatedCustomRoles/Events/PlayerEventHandler.cs +++ b/UncomplicatedCustomRoles/Events/PlayerEventHandler.cs @@ -1,441 +1,472 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . */ -using CustomPlayerEffects; -using PlayerRoles; using System; using System.Collections.Generic; using System.Linq; +using CustomPlayerEffects; using LabApi.Events.Arguments.PlayerEvents; using LabApi.Events.Handlers; using LabApi.Features.Wrappers; +using MEC; +using PlayerRoles; using PlayerRoles.PlayableScps.Scp079; using PlayerStatsSystem; -using UncomplicatedCustomRoles.API.Features.CustomModules; using UncomplicatedCustomRoles.API.Features; -using UncomplicatedCustomRoles.API.Interfaces; -using UncomplicatedCustomRoles.Manager; -using UnityEngine; +using UncomplicatedCustomRoles.API.Features.CustomModules; using UncomplicatedCustomRoles.Extensions; using UncomplicatedCustomRoles.Integrations; -using MEC; +using UncomplicatedCustomRoles.Manager; +using UnityEngine; +using Object = UnityEngine.Object; + +namespace UncomplicatedCustomRoles.Events; -namespace UncomplicatedCustomRoles.Events +internal class PlayerEventHandler : EventHandlerBase { - internal class PlayerEventHandler : EventHandlerBase + private static Scp079Recontainer _recontainer; + internal static PlayerEventHandler Instance { get; private set; } + + internal override void OnRegistered() { - internal static PlayerEventHandler Instance { get; private set; } = null; + PlayerEvents.ActivatingGenerator += OnGenerator; + PlayerEvents.Dying += OnDying; + PlayerEvents.Death += OnDeath; + PlayerEvents.SpawningRagdoll += OnRagdollSpawn; + PlayerEvents.ChangingRole += OnChangingRole; + PlayerEvents.UpdatingEffect += OnUpdatingEffect; + PlayerEvents.Escaping += OnEscaping; + PlayerEvents.UsedItem += OnItemUsed; + PlayerEvents.Hurting += OnHurting; + PlayerEvents.Hurt += OnHurt; + PlayerEvents.PickingUpItem += OnPickingUpItem; + PlayerEvents.Joined += OnJoined; + PlayerEvents.DamagingWindow += OnDamagingWindow; + PlayerEvents.UnlockingWarheadButton += OnUnlockingWarheadButton; + PlayerEvents.RequestedRaPlayerInfo += OnPlayerRequestedRaPlayerInfo; + PlayerEvents.RaPlayerListAddingPlayer += OnPlayerRaPlayerListAddingPlayer; + PlayerEvents.ChangedNickname += OnChangedNickname; + PlayerEvents.PickingUpArmor += OnPickingUpArmor; + PlayerEvents.PickingUpScp330 += OnPickingUpScp330; + PlayerEvents.InteractingScp330 += OnInteractingScp330; + + Instance = this; + } - internal override void OnRegistered() - { - PlayerEvents.ActivatingGenerator += OnGenerator; - PlayerEvents.Dying += OnDying; - PlayerEvents.Death += OnDeath; - PlayerEvents.SpawningRagdoll += OnRagdollSpawn; - PlayerEvents.ChangingRole += OnChangingRole; - PlayerEvents.UpdatingEffect += OnUpdatingEffect; - PlayerEvents.Escaping += OnEscaping; - PlayerEvents.UsedItem += OnItemUsed; - PlayerEvents.Hurting += OnHurting; - PlayerEvents.Hurt += OnHurt; - PlayerEvents.PickingUpItem += OnPickingUpItem; - PlayerEvents.Joined += OnJoined; - PlayerEvents.DamagingWindow += OnDamagingWindow; - PlayerEvents.UnlockingWarheadButton += OnUnlockingWarheadButton; - PlayerEvents.RequestedRaPlayerInfo += OnPlayerRequestedRaPlayerInfo; - PlayerEvents.RaPlayerListAddingPlayer += OnPlayerRaPlayerListAddingPlayer; - PlayerEvents.ChangedNickname += OnChangedNickname; - PlayerEvents.PickingUpArmor += OnPickingUpArmor; - PlayerEvents.PickingUpScp330 += OnPickingUpScp330; - PlayerEvents.InteractingScp330 += OnInteractingScp330; - - Instance = this; - } + internal override void OnUnregistered() + { + Instance = null; + + PlayerEvents.ActivatingGenerator -= OnGenerator; + PlayerEvents.Dying -= OnDying; + PlayerEvents.Death -= OnDeath; + PlayerEvents.SpawningRagdoll -= OnRagdollSpawn; + PlayerEvents.ChangingRole -= OnChangingRole; + PlayerEvents.UpdatingEffect -= OnUpdatingEffect; + PlayerEvents.Escaping -= OnEscaping; + PlayerEvents.UsedItem -= OnItemUsed; + PlayerEvents.Hurting -= OnHurting; + PlayerEvents.Hurt -= OnHurt; + PlayerEvents.PickingUpItem -= OnPickingUpItem; + PlayerEvents.Joined -= OnJoined; + PlayerEvents.DamagingWindow -= OnDamagingWindow; + PlayerEvents.UnlockingWarheadButton -= OnUnlockingWarheadButton; + PlayerEvents.RequestedRaPlayerInfo -= OnPlayerRequestedRaPlayerInfo; + PlayerEvents.RaPlayerListAddingPlayer -= OnPlayerRaPlayerListAddingPlayer; + PlayerEvents.ChangedNickname -= OnChangedNickname; + PlayerEvents.PickingUpArmor -= OnPickingUpArmor; + PlayerEvents.PickingUpScp330 -= OnPickingUpScp330; + PlayerEvents.InteractingScp330 -= OnInteractingScp330; + } - internal override void OnUnregistered() - { - Instance = null; - - PlayerEvents.ActivatingGenerator -= OnGenerator; - PlayerEvents.Dying -= OnDying; - PlayerEvents.Death -= OnDeath; - PlayerEvents.SpawningRagdoll -= OnRagdollSpawn; - PlayerEvents.ChangingRole -= OnChangingRole; - PlayerEvents.UpdatingEffect -= OnUpdatingEffect; - PlayerEvents.Escaping -= OnEscaping; - PlayerEvents.UsedItem -= OnItemUsed; - PlayerEvents.Hurting -= OnHurting; - PlayerEvents.Hurt -= OnHurt; - PlayerEvents.PickingUpItem -= OnPickingUpItem; - PlayerEvents.Joined -= OnJoined; - PlayerEvents.DamagingWindow -= OnDamagingWindow; - PlayerEvents.UnlockingWarheadButton -= OnUnlockingWarheadButton; - PlayerEvents.RequestedRaPlayerInfo -= OnPlayerRequestedRaPlayerInfo; - PlayerEvents.RaPlayerListAddingPlayer -= OnPlayerRaPlayerListAddingPlayer; - PlayerEvents.ChangedNickname -= OnChangedNickname; - PlayerEvents.PickingUpArmor -= OnPickingUpArmor; - PlayerEvents.PickingUpScp330 -= OnPickingUpScp330; - PlayerEvents.InteractingScp330 -= OnInteractingScp330; - } + public void OnJoined(PlayerJoinedEventArgs ev) + { + FirstRoundPlayers.Add(ev.Player.PlayerId); - public void OnJoined(PlayerJoinedEventArgs ev) - { - FirstRoundPlayers.Add(ev.Player.PlayerId); + // Sync role appearance - LabApiExtensions handles it if available + if (!LabApiExtensions.IsAvailable) + foreach (var role in SummonedCustomRole.List.Values.Where(role => role.Appearance != RoleTypeId.None)) + role.Player.ChangeAppearance(role.Appearance, [ev.Player]); - // Sync role appearance - LabApiExtensions handles it if available - if (!LabApiExtensions.IsAvailable) - foreach (SummonedCustomRole role in SummonedCustomRole.List.Values.Where(role => role.Appearance != RoleTypeId.None)) - role.Player.ChangeAppearance(role.Appearance, new Player[] { ev.Player }); + foreach (var role in SummonedCustomRole.List.Values.Where(role => role.Scale != Vector3.one)) + role.Player.Scale = role.Scale; + } - foreach (SummonedCustomRole role in SummonedCustomRole.List.Values.Where(role => role.Scale != Vector3.one)) - role.Player.Scale = role.Scale; - } + public void OnUpdatingEffect(PlayerEffectUpdatingEventArgs ev) + { + if (ev.Player is null) + return; - public void OnUpdatingEffect(PlayerEffectUpdatingEventArgs ev) - { - if (ev.Player is null) - return; + if (!ev.IsAllowed) + return; - if (!ev.IsAllowed) - return; - - if (ev.Player.TryGetSummonedInstance(out SummonedCustomRole role)) - switch (ev.Effect) - { - case CardiacArrest when role.Role.IsFriendOf is not null && role.Role.IsFriendOf.Contains(Team.SCPs): - case AmnesiaVision or AmnesiaItems when role.HasModule(): - ev.IsAllowed = false; - break; - } - } + if (ev.Player.TryGetSummonedInstance(out var role)) + switch (ev.Effect) + { + case CardiacArrest when role.Role.IsFriendOf is not null && role.Role.IsFriendOf.Contains(Team.SCPs): + case AmnesiaVision or AmnesiaItems when role.HasModule(): + ev.IsAllowed = false; + break; + } + } - public void OnGenerator(PlayerActivatingGeneratorEventArgs ev) - { - if (ev.Player.ReferenceHub.GetTeam() == Team.SCPs) - ev.IsAllowed = false; - } + public void OnGenerator(PlayerActivatingGeneratorEventArgs ev) + { + if (SummonedCustomRole.TryGetCustomTeam(ev.Player.ReferenceHub) == Team.SCPs) + ev.IsAllowed = false; + } - public void OnUnlockingWarheadButton(PlayerUnlockingWarheadButtonEventArgs ev) - { - if (ev.Player.ReferenceHub.GetTeam() == Team.SCPs) - ev.IsAllowed = false; - } + public void OnUnlockingWarheadButton(PlayerUnlockingWarheadButtonEventArgs ev) + { + if (SummonedCustomRole.TryGetCustomTeam(ev.Player.ReferenceHub) == Team.SCPs) + ev.IsAllowed = false; + } - public void OnDamagingWindow(PlayerDamagingWindowEventArgs ev) - { - if (ev.Player.Team == Team.SCPs && ev.Window.name == UnityEngine.Object.FindAnyObjectByType()?._activatorGlass.name) - ev.IsAllowed = false; - } + public void OnDamagingWindow(PlayerDamagingWindowEventArgs ev) + { + if (SummonedCustomRole.TryGetCustomTeam(ev.Player.ReferenceHub) != Team.SCPs) + return; + + if (_recontainer == null) + _recontainer = Object.FindAnyObjectByType(); + + if (_recontainer != null && ev.Window.name == _recontainer._activatorGlass.name) + ev.IsAllowed = false; + } - public void OnDying(PlayerDyingEventArgs ev) + public void OnDying(PlayerDyingEventArgs ev) + { + if (ev.Player.TryGetSummonedInstance(out var customRole)) { - if (ev.Player.TryGetSummonedInstance(out SummonedCustomRole customRole)) - { - if (customRole.HasModule()) - RagdollAppearanceQueue.Add(ev.Player.PlayerId); + if (customRole.HasModule()) + RagdollAppearanceQueue.Add(ev.Player.PlayerId); - if (customRole.TryGetModule(out CustomScpAnnouncer announcer) && ev.Player.ReferenceHub.GetTeam() is not Team.SCPs) - TerminationQueue[ev.Player.PlayerId] = new(announcer, DateTimeOffset.Now); + if (customRole.TryGetModule(out CustomScpAnnouncer announcer) && + ev.Player.ReferenceHub.GetTeam() is not Team.SCPs) + TerminationQueue[ev.Player.PlayerId] = + new Tuple(announcer, DateTimeOffset.Now); - if (customRole.HasModule()) - ev.Player.ClearInventory(); - } + if (customRole.HasModule()) + ev.Player.ClearInventory(); } + } - public void OnDeath(PlayerDeathEventArgs ev) - { - if (TerminationQueue.TryGetValue(ev.Player.PlayerId, out Tuple data) && (DateTimeOffset.Now - data.Item2).TotalMilliseconds < 1300) - SpawnManager.AnnounceScpTermination(ev.Player.ReferenceHub, ev.DamageHandler); + public void OnDeath(PlayerDeathEventArgs ev) + { + if (TerminationQueue.TryGetValue(ev.Player.PlayerId, out var data) && + (DateTimeOffset.Now - data.Item2).TotalMilliseconds < 1300) + SpawnManager.AnnounceScpTermination(ev.Player.ReferenceHub, ev.DamageHandler); - TerminationQueue.TryRemove(ev.Player.PlayerId, out _); + TerminationQueue.TryRemove(ev.Player.PlayerId, out _); - SpawnManager.ClearCustomTypes(ev.Player); + SpawnManager.ClearCustomTypes(ev.Player); - // Try change appearance of the killer - if (ev.Attacker.TryGetSummonedInstance(out SummonedCustomRole attackerCustomRole) && attackerCustomRole.TryGetModule(out ChangeAppearanceOnKill changeAppearanceOnKill)) - { - if (changeAppearanceOnKill.Forever && changeAppearanceOnKill.AlreadyChanged) - return; + // Try change appearance of the killer + if (ev.Attacker.TryGetSummonedInstance(out var attackerCustomRole) && + attackerCustomRole.TryGetModule(out ChangeAppearanceOnKill changeAppearanceOnKill)) + { + if (changeAppearanceOnKill.Forever && changeAppearanceOnKill.AlreadyChanged) + return; + + changeAppearanceOnKill.AlreadyChanged = true; - changeAppearanceOnKill.AlreadyChanged = true; + // Change + if (LabApiExtensions.IsAvailable) + LabApiExtensions.AddFakeRole(attackerCustomRole.Player, changeAppearanceOnKill.NewAppearance); + else + attackerCustomRole.Player.ChangeAppearance(changeAppearanceOnKill.NewAppearance); - // Change - if (LabApiExtensions.IsAvailable) - LabApiExtensions.AddFakeRole(attackerCustomRole.Player, changeAppearanceOnKill.NewAppearance); - else - attackerCustomRole.Player.ChangeAppearance(changeAppearanceOnKill.NewAppearance); + if (!changeAppearanceOnKill.Forever) + Timing.CallDelayed(changeAppearanceOnKill.Duration, () => + { + if (attackerCustomRole.Player is null || !attackerCustomRole.Player.IsAlive) + return; - if (!changeAppearanceOnKill.Forever) - Timing.CallDelayed(changeAppearanceOnKill.Duration, () => + if (LabApiExtensions.IsAvailable) { - if (attackerCustomRole.Player is null || !attackerCustomRole.Player.IsAlive) - return; - - if (LabApiExtensions.IsAvailable) - { - if (attackerCustomRole.Appearance != RoleTypeId.None) - LabApiExtensions.AddFakeRole(attackerCustomRole.Player, attackerCustomRole.Role.RoleAppearance); - else - LabApiExtensions.RemoveFakeRole(attackerCustomRole.Player); - } + if (attackerCustomRole.Appearance != RoleTypeId.None) + LabApiExtensions.AddFakeRole(attackerCustomRole.Player, + attackerCustomRole.Role.RoleAppearance); else - { - attackerCustomRole.Player.ChangeAppearance(attackerCustomRole.Role.RoleAppearance); - } - }); - } - - // DON'T DO ANYTHING HERE AS THERE ARE TWO return UP THERE! + LabApiExtensions.RemoveFakeRole(attackerCustomRole.Player); + } + else + { + attackerCustomRole.Player.ChangeAppearance(attackerCustomRole.Role.RoleAppearance); + } + }); } - public void OnRagdollSpawn(PlayerSpawningRagdollEventArgs ev) - { - if (ev.Player is null) - return; - - if (!RagdollAppearanceQueue.Contains(ev.Player.PlayerId)) - return; - - ev.IsAllowed = false; - RagdollAppearanceQueue.Remove(ev.Player.PlayerId); + // DON'T DO ANYTHING HERE AS THERE ARE TWO return UP THERE! + } - Ragdoll.SpawnRagdoll(RoleTypeId.Tutorial, ev.RagdollPrefab.Position, ev.RagdollPrefab.Rotation, ev.DamageHandler, ev. Player.Nickname); - } + public void OnRagdollSpawn(PlayerSpawningRagdollEventArgs ev) + { + if (ev.Player is null) + return; - public void OnChangingRole(PlayerChangingRoleEventArgs ev) - { - if (ev.Player is null) - return; + if (!RagdollAppearanceQueue.Contains(ev.Player.PlayerId)) + return; - // Let's clear for custom types - SpawnManager.ClearCustomTypes(ev.Player); + ev.IsAllowed = false; + RagdollAppearanceQueue.Remove(ev.Player.PlayerId); - if (!ev.IsAllowed) - return; + Ragdoll.SpawnRagdoll(RoleTypeId.Tutorial, ev.RagdollPrefab.Position, ev.RagdollPrefab.Rotation, + ev.DamageHandler, ev.Player.Nickname); + } - if (!LabApi.Features.Wrappers.Round.IsRoundStarted) - return; + public void OnChangingRole(PlayerChangingRoleEventArgs ev) + { + if (ev.Player is null) + return; - if (ev.NewRole is RoleTypeId.Spectator or RoleTypeId.None or RoleTypeId.Filmmaker) - return; + // Let's clear for custom types + SpawnManager.ClearCustomTypes(ev.Player); - if (Spawn.Spawning.Contains(ev.Player.PlayerId)) - return; + if (!ev.IsAllowed) + return; - if (ev.Player.HasCustomRole()) - return; + if (!Round.IsRoundStarted) + return; - if (Plugin.Instance.Config.IgnoreNpcs && ev.Player.IsNpc) - return; + if (ev.NewRole is RoleTypeId.Spectator or RoleTypeId.None or RoleTypeId.Filmmaker) + return; - if (Started || !FirstRoundPlayers.Contains(ev.Player.PlayerId)) - { - if (Plugin.Instance.Config.AllowOnlyNaturalSpawns && !Spawn.SpawnQueue.Contains(ev.Player.PlayerId)) - { - LogManager.Debug("The player is not in the queue for respawning!"); - return; - } + if (Spawn.Spawning.Contains(ev.Player.PlayerId)) + return; - if (Spawn.SpawnQueue.Contains(ev.Player.PlayerId)) - { - Spawn.SpawnQueue.Remove(ev.Player.PlayerId); - } - } + if (ev.Player.HasCustomRole()) + return; - ICustomRole Role = SpawnManager.DoEvaluateSpawnForPlayer(ev.Player, ev.NewRole); + if (Plugin.Instance.Config.IgnoreNpcs && ev.Player.IsNpc) + return; - if (Role is not null) + if (Started || !FirstRoundPlayers.Contains(ev.Player.PlayerId)) + { + if (Plugin.Instance.Config.AllowOnlyNaturalSpawns && !Spawn.SpawnQueue.Contains(ev.Player.PlayerId)) { - LogManager.Debug($"Summoning player {ev.Player.Nickname} ({ev.Player.PlayerId}) as {Role.Name} ({Role.Id})"); - SpawnManager.SummonCustomSubclass(ev.Player, Role.Id); - ev.IsAllowed = false; + LogManager.Debug("The player is not in the queue for respawning!"); + return; } - LogManager.Debug($"No CustomRole found for player {ev.Player.Nickname}, allowing natural spawn with {ev.NewRole}"); + if (Spawn.SpawnQueue.Contains(ev.Player.PlayerId)) Spawn.SpawnQueue.Remove(ev.Player.PlayerId); } - public void OnHurting(PlayerHurtingEventArgs Hurting) + var Role = SpawnManager.DoEvaluateSpawnForPlayer(ev.Player, ev.NewRole); + + if (Role is not null) { - if (!Hurting.IsAllowed) - return; + LogManager.Debug( + $"Summoning player {ev.Player.Nickname} ({ev.Player.PlayerId}) as {Role.Name} ({Role.Id})"); + SpawnManager.SummonCustomSubclass(ev.Player, Role.Id); + ev.IsAllowed = false; + } + else + { + LogManager.Debug( + $"No CustomRole found for player {ev.Player.Nickname}, allowing natural spawn with {ev.NewRole}"); + } + } - if (Hurting.Player is not null && Hurting.Attacker is not null && Hurting.Player.IsAlive && Hurting.Attacker.IsAlive) + public void OnHurting(PlayerHurtingEventArgs Hurting) + { + if (!Hurting.IsAllowed) + return; + + if (Hurting.Player is not null && Hurting.Attacker is not null && Hurting.Player.IsAlive && + Hurting.Attacker.IsAlive) + { + if (Hurting.Attacker.TryGetSummonedInstance(out var attackerCustomRole)) { - if (Hurting.Attacker.TryGetSummonedInstance(out SummonedCustomRole attackerCustomRole)) + if (attackerCustomRole.Role.IsFriendOf is not null && + attackerCustomRole.Role.IsFriendOf.Contains(Hurting.Player.ReferenceHub.GetTeam())) { - if (attackerCustomRole.Role.IsFriendOf is not null && attackerCustomRole.Role.IsFriendOf.Contains(Hurting.Player.ReferenceHub.GetTeam())) - { - Hurting.IsAllowed = false; - LogManager.Silent("Rejected the event request of Hurting because of is_friend_of - FROM ATTACKER"); - return; - } + Hurting.IsAllowed = false; + LogManager.Silent("Rejected the event request of Hurting because of is_friend_of - FROM ATTACKER"); + return; + } - if (attackerCustomRole?.HasModule() ?? false) - attackerCustomRole.RemoveModules(); + if (attackerCustomRole?.HasModule() ?? false) + attackerCustomRole.RemoveModules(); - if (Hurting.DamageHandler is StandardDamageHandler standardDamageHandler) - standardDamageHandler.Damage *= attackerCustomRole.Role.DamageMultiplier; - } + if (Hurting.DamageHandler is StandardDamageHandler standardDamageHandler) + standardDamageHandler.Damage *= attackerCustomRole.Role.DamageMultiplier; + } - // Divided because they can be both CR - if (Hurting.Player.TryGetSummonedInstance(out SummonedCustomRole playerCustomRole)) + // Divided because they can be both CR + if (Hurting.Player.TryGetSummonedInstance(out var playerCustomRole)) + { + if (playerCustomRole.Role.IsFriendOf is not null && + playerCustomRole.Role.IsFriendOf.Contains(Hurting.Attacker.ReferenceHub.GetTeam())) { - if (playerCustomRole.Role.IsFriendOf is not null && playerCustomRole.Role.IsFriendOf.Contains(Hurting.Attacker.ReferenceHub.GetTeam())) - { - Hurting.IsAllowed = false; - LogManager.Silent("Rejected the event request of Hurting because of is_friend_of - FROM HURTED"); - return; - } - - if (playerCustomRole?.HasModule() ?? false) - Hurting.IsAllowed = false; + Hurting.IsAllowed = false; + LogManager.Silent("Rejected the event request of Hurting because of is_friend_of - FROM HURTED"); + return; } + + if (playerCustomRole?.HasModule() ?? false) + Hurting.IsAllowed = false; } } + } + + public void OnHurt(PlayerHurtEventArgs ev) + { + if (ev.Player is not null && ev.Player.IsAlive && ev.Player.TryGetSummonedInstance(out var playerCustomRole)) + playerCustomRole.LastDamageTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + + if (ev.Attacker is not null && ev.Attacker.IsAlive && + ev.Attacker.TryGetSummonedInstance(out var attackerCustomRole) && + attackerCustomRole.TryGetModule(out LifeStealer lifeStealer) && + ev.DamageHandler is StandardDamageHandler standardDamageHandler) + ev.Attacker.Heal(standardDamageHandler.Damage * (lifeStealer.Percentage / 100f)); + } - public void OnHurt(PlayerHurtEventArgs ev) + public void OnEscaping(PlayerEscapingEventArgs Escaping) + { + if (Escaping.Player.TryGetSummonedInstance(out var summoned)) { - if (ev.Player is not null && ev.Player.IsAlive && ev.Player.TryGetSummonedInstance(out SummonedCustomRole playerCustomRole)) - playerCustomRole.LastDamageTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + if (summoned.Role.CanEscape) + LogManager.Debug( + $"Player {Escaping.Player.Nickname} triggered the escaping event as {Escaping.Player.Role.ToString()}"); - if (ev.Attacker is not null && ev.Attacker.IsAlive && ev.Attacker.TryGetSummonedInstance(out SummonedCustomRole attackerCustomRole) && attackerCustomRole.TryGetModule(out LifeStealer lifeStealer) && ev.DamageHandler is StandardDamageHandler standardDamageHandler) - ev.Attacker.Heal(standardDamageHandler.Damage * (lifeStealer.Percentage / 100f)); - } + LogManager.Debug($"Player IS a custom role: {summoned.Role.Name}"); - public void OnEscaping(PlayerEscapingEventArgs Escaping) - { - if (Escaping.Player.TryGetSummonedInstance(out SummonedCustomRole summoned)) + if (!summoned.Role.CanEscape) { - if (summoned.Role.CanEscape) - LogManager.Debug($"Player {Escaping.Player.Nickname} triggered the escaping event as {Escaping.Player.Role.ToString()}"); + LogManager.Debug( + $"Player with the role {summoned.Role.Id} ({summoned.Role.Name}) can't escape, so nuh uh!"); + Escaping.IsAllowed = false; + return; + } - LogManager.Debug($"Player IS a custom role: {summoned.Role.Name}"); + if (summoned.Role.CanEscape && + (summoned.Role.RoleAfterEscape is null || summoned.Role.RoleAfterEscape.Count < 1)) + { + LogManager.Debug( + $"Player with the role {summoned.Role.Id} ({summoned.Role.Name}) evaluated for a natural respawn!"); + Escaping.IsAllowed = true; + return; + } - if (!summoned.Role.CanEscape) - { - LogManager.Debug($"Player with the role {summoned.Role.Id} ({summoned.Role.Name}) can't escape, so nuh uh!"); - Escaping.IsAllowed = false; - return; - } + // Try to set the role + var newRole = SpawnManager.ParseEscapeRole(summoned.Role.RoleAfterEscape, Escaping.Player); - if (summoned.Role.CanEscape && (summoned.Role.RoleAfterEscape is null || summoned.Role.RoleAfterEscape.Count < 1)) - { - LogManager.Debug($"Player with the role {summoned.Role.Id} ({summoned.Role.Name}) evaluated for a natural respawn!"); - Escaping.IsAllowed = true; - return; - } - - // Try to set the role - KeyValuePair? newRole = SpawnManager.ParseEscapeRole(summoned.Role.RoleAfterEscape, Escaping.Player); + if (newRole is null) + { + Escaping.IsAllowed = false; + return; + } - if (newRole is null) - { - Escaping.IsAllowed = false; - return; - } + var NewRole = (KeyValuePair)newRole; - KeyValuePair NewRole = (KeyValuePair)newRole; + if (NewRole.Value is null) + { + Escaping.IsAllowed = true; + return; + } - if (NewRole.Value is null) + if (!NewRole.Key) + { + // Natural role, let's try to parse it + if (Enum.TryParse(NewRole.Value.ToString(), out RoleTypeId role)) + if (role is not RoleTypeId.None) + { + Escaping.NewRole = role; + Escaping.IsAllowed = true; + } + } + else + { + LogManager.Silent($"Trying to find CustomRole with Id {NewRole.Value}"); + if (int.TryParse(NewRole.Value.ToString(), out var id) && CustomRole.TryGet(id, out var role)) { - Escaping.IsAllowed = true; - return; - } + LogManager.Silent("Role found!"); - if (!NewRole.Key) - { - // Natural role, let's try to parse it - if (Enum.TryParse(NewRole.Value.ToString(), out RoleTypeId role)) + if (summoned.TryGetModule(out KeepInventoryOnEscape module)) + RespawnInventoryQueue.TryAdd(Escaping.Player.PlayerId, + new Tuple, Dictionary, bool>( + [..Escaping.Player.Items.Select(i => i.Type)], + new Dictionary(Escaping.Player.Ammo), module.DropItems)); + + Escaping.IsAllowed = false; + if (!API.Features.Escape.Bucket.Contains(Escaping.Player.PlayerId)) { - if (role is not RoleTypeId.None) - { - Escaping.NewRole = role; - Escaping.IsAllowed = true; - } + LogManager.Silent( + "Successfully activated the call to method SpawnManager::SummonCustomSubclass(<...>) as the player is not inside the Escape::Bucket bucket! - Adding it..."); + API.Features.Escape.Bucket.Add(Escaping.Player.PlayerId); + SpawnManager.SummonCustomSubclass(Escaping.Player, role.Id); } - } - else - { - LogManager.Silent($"Trying to find CustomRole with Id {NewRole.Value}"); - if (int.TryParse(NewRole.Value.ToString(), out int id) && CustomRole.TryGet(id, out ICustomRole role)) + else { - LogManager.Silent($"Role found!"); - - // Save the inventory if needed - if (summoned.TryGetModule(out KeepInventoryOnEscape module)) - RespawnInventoryQueue.TryAdd(Escaping.Player.PlayerId, new(new(Escaping.Player.Items.Select(i => i.Type)), Escaping.Player.Ammo, module.DropItems)); - - Escaping.IsAllowed = false; - if (!API.Features.Escape.Bucket.Contains(Escaping.Player.PlayerId)) - { - LogManager.Silent($"Successfully activated the call to method SpawnManager::SummonCustomSubclass(<...>) as the player is not inside the Escape::Bucket bucket! - Adding it..."); - API.Features.Escape.Bucket.Add(Escaping.Player.PlayerId); - SpawnManager.SummonCustomSubclass(Escaping.Player, role.Id); - } - else - LogManager.Silent($"Canceled call to method SpawnManager::SummonCustomSubclass(<...>) due to the presence of the player inside the Escape::Bucket! - Event already fired!"); + LogManager.Silent( + "Canceled call to method SpawnManager::SummonCustomSubclass(<...>) due to the presence of the player inside the Escape::Bucket! - Event already fired!"); } - } } } + } - public void OnItemUsed(PlayerUsedItemEventArgs ev) - { - if (ev.Player is not null && ev.Player.TryGetSummonedInstance(out SummonedCustomRole summoned) && ev.UsableItem.Type is ItemType.SCP500) - summoned?.InfiniteEffects.RemoveAll(effect => effect is not null && effect.Removable); - } + public void OnItemUsed(PlayerUsedItemEventArgs ev) + { + if (ev.Player is not null && ev.Player.TryGetSummonedInstance(out var summoned) && + ev.UsableItem.Type is ItemType.SCP500) + summoned?.InfiniteEffects.RemoveAll(effect => effect is not null && effect.Removable); + } - public void OnPickingUpItem(PlayerPickingUpItemEventArgs ev) - { - if (ev.Player.TryGetSummonedInstance(out SummonedCustomRole summonedInstance) && summonedInstance.TryGetModule(out ItemBan itemBan)) - ev.IsAllowed = !itemBan.Items.Contains(ev.Pickup.Type); - } - - public void OnPickingUpArmor(PlayerPickingUpArmorEventArgs ev) - { - if (ev.Player.TryGetSummonedInstance(out SummonedCustomRole summonedInstance) && summonedInstance.TryGetModule(out ItemBan itemBan)) - ev.IsAllowed = !itemBan.Items.Contains(ev.BodyArmorPickup.Type); - } - - public void OnPickingUpScp330(PlayerPickingUpScp330EventArgs ev) - { - if (ev.Player.TryGetSummonedInstance(out SummonedCustomRole summonedInstance) && summonedInstance.TryGetModule(out ItemBan itemBan)) - ev.IsAllowed = !itemBan.Items.Contains(ev.CandyPickup.Type); - } - - public void OnInteractingScp330(PlayerInteractingScp330EventArgs ev) - { - if (ev.Player.TryGetSummonedInstance(out SummonedCustomRole summonedInstance) && summonedInstance.TryGetModule(out ItemBan itemBan)) - ev.IsAllowed = !itemBan.Items.Contains(ItemType.SCP330); - } - - public void OnPlayerRequestedRaPlayerInfo(PlayerRequestedRaPlayerInfoEventArgs ev) - { - SummonedCustomRole.TryParseRemoteAdmin(ev.Target.ReferenceHub, ev.InfoBuilder); - } + public void OnPickingUpItem(PlayerPickingUpItemEventArgs ev) + { + if (ev.Player.TryGetSummonedInstance(out var summonedInstance) && + summonedInstance.TryGetModule(out ItemBan itemBan)) + ev.IsAllowed = !itemBan.Items.Contains(ev.Pickup.Type); + } - public void OnPlayerRaPlayerListAddingPlayer(PlayerRaPlayerListAddingPlayerEventArgs ev) - { - if (SummonedCustomRole.TryGet(ev.Target.ReferenceHub, out SummonedCustomRole customRole)) - if (customRole.TryGetModule(out ColorfulRaName colorfulRaName)) - ev.Body = ev.Body.Replace("{RA_ClassColor}", $"#{colorfulRaName.Color.TrimStart('#')}"); - } + public void OnPickingUpArmor(PlayerPickingUpArmorEventArgs ev) + { + if (ev.Player.TryGetSummonedInstance(out var summonedInstance) && + summonedInstance.TryGetModule(out ItemBan itemBan)) + ev.IsAllowed = !itemBan.Items.Contains(ev.BodyArmorPickup.Type); + } - public void OnChangedNickname(PlayerChangedNicknameEventArgs ev) - { - if (ev.Player.ReferenceHub is null) - return; + public void OnPickingUpScp330(PlayerPickingUpScp330EventArgs ev) + { + if (ev.Player.TryGetSummonedInstance(out var summonedInstance) && + summonedInstance.TryGetModule(out ItemBan itemBan)) + ev.IsAllowed = !itemBan.Items.Contains(ev.CandyPickup.Type); + } - if (SummonedCustomRole.TryGet(ev.Player.ReferenceHub, out SummonedCustomRole customRole) && customRole.CustomInfo is not null) - customRole.CustomInfo.Nickname = ev.NewNickname ?? ev.Player.Nickname; - } + public void OnInteractingScp330(PlayerInteractingScp330EventArgs ev) + { + if (ev.Player.TryGetSummonedInstance(out var summonedInstance) && + summonedInstance.TryGetModule(out ItemBan itemBan)) + ev.IsAllowed = !itemBan.Items.Contains(ItemType.SCP330); + } + + public void OnPlayerRequestedRaPlayerInfo(PlayerRequestedRaPlayerInfoEventArgs ev) + { + SummonedCustomRole.TryParseRemoteAdmin(ev.Target.ReferenceHub, ev.InfoBuilder); + } + + public void OnPlayerRaPlayerListAddingPlayer(PlayerRaPlayerListAddingPlayerEventArgs ev) + { + if (SummonedCustomRole.TryGet(ev.Target.ReferenceHub, out var customRole)) + if (customRole.TryGetModule(out ColorfulRaName colorfulRaName)) + ev.Body = ev.Body.Replace("{RA_ClassColor}", $"#{colorfulRaName.Color.TrimStart('#')}"); + } + + public void OnChangedNickname(PlayerChangedNicknameEventArgs ev) + { + if (ev.Player.ReferenceHub is null) + return; + + if (SummonedCustomRole.TryGet(ev.Player.ReferenceHub, out var customRole) && customRole.CustomInfo is not null) + customRole.CustomInfo.Nickname = ev.NewNickname ?? ev.Player.Nickname; } } \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Events/ScpEventHandler.cs b/UncomplicatedCustomRoles/Events/ScpEventHandler.cs index d2ed7a8..bfc16fa 100644 --- a/UncomplicatedCustomRoles/Events/ScpEventHandler.cs +++ b/UncomplicatedCustomRoles/Events/ScpEventHandler.cs @@ -4,90 +4,88 @@ using LabApi.Events.Arguments.Scp914Events; using LabApi.Events.Handlers; using PlayerRoles; -using UncomplicatedCustomRoles.API.Features.CustomModules; using UncomplicatedCustomRoles.API.Features; -using UncomplicatedCustomRoles.API.Interfaces; +using UncomplicatedCustomRoles.API.Features.CustomModules; using UncomplicatedCustomRoles.Extensions; using UncomplicatedCustomRoles.Manager; -namespace UncomplicatedCustomRoles.Events +namespace UncomplicatedCustomRoles.Events; + +internal class ScpEventHandler : EventHandlerBase { - internal class ScpEventHandler : EventHandlerBase + internal override void OnRegistered() { - internal override void OnRegistered() - { - // SCP-049 - Scp049Events.ResurrectingBody += OnResurrectingBody; + // SCP-049 + Scp049Events.ResurrectingBody += OnResurrectingBody; - // SCP-096 - Scp096Events.AddingTarget += OnAddingTarget; + // SCP-096 + Scp096Events.AddingTarget += OnAddingTarget; - // SCP-330 - PlayerEvents.InteractingScp330 += OnInteractingScp330; - - // SCP-914 - Scp914Events.ProcessedInventoryItem += OnProcessedInventoryItem; - } - - internal override void OnUnregistered() - { - // SCP-049 - Scp049Events.ResurrectingBody -= OnResurrectingBody; + // SCP-330 + PlayerEvents.InteractingScp330 += OnInteractingScp330; - // SCP-096 - Scp096Events.AddingTarget -= OnAddingTarget; + // SCP-914 + Scp914Events.ProcessedInventoryItem += OnProcessedInventoryItem; + } - // SCP-330 - PlayerEvents.InteractingScp330 -= OnInteractingScp330; - - // SCP-914 - Scp914Events.ProcessedInventoryItem -= OnProcessedInventoryItem; - } + internal override void OnUnregistered() + { + // SCP-049 + Scp049Events.ResurrectingBody -= OnResurrectingBody; - public void OnAddingTarget(Scp096AddingTargetEventArgs ev) - { - if (!ev.IsAllowed) - return; + // SCP-096 + Scp096Events.AddingTarget -= OnAddingTarget; - if (ev.Target.TryGetSummonedInstance(out SummonedCustomRole summonedInstance)) - { - if (ev.Target.ReferenceHub.GetTeam() is Team.SCPs) - ev.IsAllowed = false; + // SCP-330 + PlayerEvents.InteractingScp330 -= OnInteractingScp330; - if (summonedInstance.HasModule()) - ev.IsAllowed = false; + // SCP-914 + Scp914Events.ProcessedInventoryItem -= OnProcessedInventoryItem; + } - if (summonedInstance.HasModule()) - ev.IsAllowed = false; - } - } + public void OnAddingTarget(Scp096AddingTargetEventArgs ev) + { + if (!ev.IsAllowed) + return; - public void OnResurrectingBody(Scp049ResurrectingBodyEventArgs ev) + if (ev.Target.TryGetSummonedInstance(out var summonedInstance)) { - ICustomRole Role = SpawnManager.DoEvaluateSpawnForPlayer(ev.Target, RoleTypeId.Scp0492); - LogManager.Silent($"{ev.Target} recalled by {ev.Player}, found {Role?.Id} {Role?.Name}"); + if (ev.Target.ReferenceHub.GetTeam() is Team.SCPs) + ev.IsAllowed = false; - if (Role is not null) - { + if (summonedInstance.HasModule()) ev.IsAllowed = false; - ev.Target.SetCustomRole(Role); - } - } - public void OnInteractingScp330(PlayerInteractingScp330EventArgs ev) - { - if (!ev.IsAllowed) - return; - - if (SummonedCustomRole.TryGet(ev.Player, out SummonedCustomRole role)) - ev.AllowPunishment = ev.Uses > role.Role.MaxScp330Candies; + if (summonedInstance.HasModule()) + ev.IsAllowed = false; } + } + + public void OnResurrectingBody(Scp049ResurrectingBodyEventArgs ev) + { + var Role = SpawnManager.DoEvaluateSpawnForPlayer(ev.Target, RoleTypeId.Scp0492); + LogManager.Silent($"{ev.Target} recalled by {ev.Player}, found {Role?.Id} {Role?.Name}"); - public void OnProcessedInventoryItem(Scp914ProcessedInventoryItemEventArgs ev) + if (Role is not null) { - if (ev.Player.TryGetSummonedInstance(out SummonedCustomRole summonedInstance) && - summonedInstance.TryGetModule(out ItemBan itemBan) && itemBan.Items.Contains(ev.Item.Type)) - ev.Player.DropItem(ev.Item); + ev.IsAllowed = false; + ev.Target.SetCustomRole(Role); } } + + public void OnInteractingScp330(PlayerInteractingScp330EventArgs ev) + { + if (!ev.IsAllowed) + return; + + if (SummonedCustomRole.TryGet(ev.Player, out var role)) + ev.AllowPunishment = ev.Uses > role.Role.MaxScp330Candies; + } + + public void OnProcessedInventoryItem(Scp914ProcessedInventoryItemEventArgs ev) + { + if (ev.Player.TryGetSummonedInstance(out var summonedInstance) && + summonedInstance.TryGetModule(out ItemBan itemBan) && itemBan.Items.Contains(ev.Item.Type)) + ev.Player.DropItem(ev.Item); + } } \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Events/ServerEventHandler.cs b/UncomplicatedCustomRoles/Events/ServerEventHandler.cs index cb3e17c..17e9c24 100644 --- a/UncomplicatedCustomRoles/Events/ServerEventHandler.cs +++ b/UncomplicatedCustomRoles/Events/ServerEventHandler.cs @@ -1,83 +1,83 @@ using LabApi.Events.Arguments.ServerEvents; using LabApi.Events.Arguments.WarheadEvents; using LabApi.Events.Handlers; -using LabApi.Features.Wrappers; using PlayerRoles; using PlayerRoles.RoleAssign; using UncomplicatedCustomRoles.API.Features; using UncomplicatedCustomRoles.Manager; using Announcer = UncomplicatedCustomRoles.Patches.Announcer; -namespace UncomplicatedCustomRoles.Events +namespace UncomplicatedCustomRoles.Events; + +internal class ServerEventHandler : EventHandlerBase { - internal class ServerEventHandler : EventHandlerBase + internal override void OnRegistered() { - internal override void OnRegistered() - { - ServerEvents.WaveRespawning += OnWaveRespawning; - RoleAssigner.OnPlayersSpawned += OnPlayersSpawned; - ServerEvents.RoundEnded += OnRoundEnded; - ServerEvents.WaitingForPlayers += OnWaitingForPlayers; - ServerEvents.RoundRestarted += OnRoundRestarted; + ServerEvents.WaveRespawning += OnWaveRespawning; + RoleAssigner.OnPlayersSpawned += OnPlayersSpawned; + ServerEvents.RoundEnded += OnRoundEnded; + ServerEvents.WaitingForPlayers += OnWaitingForPlayers; + ServerEvents.RoundRestarted += OnRoundRestarted; + + // Warhead + WarheadEvents.Starting += OnWarheadStarting; + } - // Warhead - WarheadEvents.Starting += OnWarheadStarting; - } + internal override void OnUnregistered() + { + ServerEvents.WaveRespawning -= OnWaveRespawning; + RoleAssigner.OnPlayersSpawned -= OnPlayersSpawned; + ServerEvents.RoundEnded -= OnRoundEnded; + ServerEvents.WaitingForPlayers -= OnWaitingForPlayers; + ServerEvents.RoundRestarted -= OnRoundRestarted; - internal override void OnUnregistered() - { - ServerEvents.WaveRespawning -= OnWaveRespawning; - RoleAssigner.OnPlayersSpawned -= OnPlayersSpawned; - ServerEvents.RoundEnded -= OnRoundEnded; - ServerEvents.WaitingForPlayers -= OnWaitingForPlayers; - ServerEvents.RoundRestarted -= OnRoundRestarted; + // Warhead + WarheadEvents.Starting -= OnWarheadStarting; + } - // Warhead - WarheadEvents.Starting -= OnWarheadStarting; - } + public void OnWaitingForPlayers() + { + Started = false; + Plugin.Instance.OnFinishedLoadingPlugins(); + MapSpawnValidator.ValidateAll(); + } - public void OnWaitingForPlayers() - { - Started = false; - Plugin.Instance.OnFinishedLoadingPlugins(); - } + public void OnPlayersSpawned() + { + Started = true; + FirstRoundPlayers.Clear(); - public void OnPlayersSpawned() - { - Started = true; - FirstRoundPlayers.Clear(); + // Starts the infinite effect thing + InfiniteEffect.Stop(); + InfiniteEffect.EffectAssociationAllowed = true; + InfiniteEffect.Start(); + } - // Starts the infinite effect thing - InfiniteEffect.Stop(); - InfiniteEffect.EffectAssociationAllowed = true; - InfiniteEffect.Start(); - } - - public void OnRoundEnded(RoundEndedEventArgs _) - { - Started = false; - InfiniteEffect.Terminate(); - } + public void OnRoundEnded(RoundEndedEventArgs _) + { + Started = false; + InfiniteEffect.Terminate(); + } - public void OnRoundRestarted() - { - Announcer.SavedCustomAnnouncements.Clear(); - } + public void OnRoundRestarted() + { + Announcer.SavedCustomAnnouncements.Clear(); + } - public void OnWaveRespawning(WaveRespawningEventArgs ev) - { - LogManager.Silent("Respawning wave"); - if (Spawn.DoHandleWave) - foreach (Player player in ev.SpawningPlayers) - Spawn.SpawnQueue.Add(player.PlayerId); - else - Spawn.DoHandleWave = true; - } + public void OnWaveRespawning(WaveRespawningEventArgs ev) + { + LogManager.Silent("Respawning wave"); + if (Spawn.DoHandleWave) + foreach (var player in ev.SpawningPlayers) + Spawn.SpawnQueue.Add(player.PlayerId); + else + Spawn.DoHandleWave = true; + } - public void OnWarheadStarting(WarheadStartingEventArgs ev) - { - if (ev.Player.ReferenceHub.GetTeam() == Team.SCPs) - ev.IsAllowed = false; - } + public void OnWarheadStarting(WarheadStartingEventArgs ev) + { + if (ev.Player?.ReferenceHub is not null && + SummonedCustomRole.TryGetCustomTeam(ev.Player.ReferenceHub) == Team.SCPs) + ev.IsAllowed = false; } } \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Extensions/CompatibilityExtension.cs b/UncomplicatedCustomRoles/Extensions/CompatibilityExtension.cs index 3f7210d..947ee0f 100644 --- a/UncomplicatedCustomRoles/Extensions/CompatibilityExtension.cs +++ b/UncomplicatedCustomRoles/Extensions/CompatibilityExtension.cs @@ -1,22 +1,24 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . */ -using MapGeneration; using System.Collections.Generic; +using MapGeneration; using UncomplicatedCustomRoles.Compatibility.PreviousVersionElements.Enums; -namespace UncomplicatedCustomRoles.Extensions +namespace UncomplicatedCustomRoles.Extensions; + +public static class CompatibilityExtension { - public static class CompatibilityExtension + public static ItemType GetItemType(this ExiledAmmoType type) { - public static ItemType GetItemType(this ExiledAmmoType type) => type switch + return type switch { ExiledAmmoType.None => ItemType.None, ExiledAmmoType.Nato556 => ItemType.Ammo556x45, @@ -24,30 +26,33 @@ public static class CompatibilityExtension ExiledAmmoType.Ammo44Cal => ItemType.Ammo44cal, ExiledAmmoType.Ammo12Gauge => ItemType.Ammo12gauge, ExiledAmmoType.Nato9 => ItemType.Ammo9x19, - _ => ItemType.None, + _ => ItemType.None }; + } - public static List GetItemTypes(this IEnumerable types) - { - List items = new(); + public static List GetItemTypes(this IEnumerable types) + { + List items = []; - foreach (ExiledAmmoType ammoType in types) - items.Add(ammoType.GetItemType()); + foreach (var ammoType in types) + items.Add(ammoType.GetItemType()); - return items; - } + return items; + } - public static Dictionary ConvertItemTypes(this Dictionary data) - { - Dictionary items = new(); + public static Dictionary ConvertItemTypes(this Dictionary data) + { + Dictionary items = new(); - foreach (KeyValuePair item in data) - items.Add(item.Key.GetItemType(), item.Value); + foreach (var item in data) + items.Add(item.Key.GetItemType(), item.Value); - return items; - } + return items; + } - public static string GetRoomType(this ExiledRoomType type) => type switch + public static string GetRoomType(this ExiledRoomType type) + { + return type switch { ExiledRoomType.Pocket => "PocketWorld", ExiledRoomType.Surface => "Outside", @@ -113,18 +118,21 @@ public static Dictionary ConvertItemTypes(this Dictionary "HCZ_EZ_Checkpoint Part", _ => "Unknown" }; + } - public static List ConvertRoomTypes(this IEnumerable types) - { - List result = new(); + public static List ConvertRoomTypes(this IEnumerable types) + { + List result = []; - foreach (ExiledRoomType type in types) - result.Add(type.GetRoomType()); + foreach (var type in types) + result.Add(type.GetRoomType()); - return result; - } + return result; + } - public static FacilityZone GetFacilityZone(this ExiledZoneType zone) => zone switch + public static FacilityZone GetFacilityZone(this ExiledZoneType zone) + { + return zone switch { ExiledZoneType.Unspecified => FacilityZone.None, ExiledZoneType.Other => FacilityZone.Other, @@ -135,15 +143,15 @@ public static List ConvertRoomTypes(this IEnumerable typ ExiledZoneType.Pocket => FacilityZone.Other, _ => FacilityZone.None }; + } - public static List ConvertZoneTypes(this IEnumerable types) - { - List result = new(); + public static List ConvertZoneTypes(this IEnumerable types) + { + List result = []; - foreach (ExiledZoneType type in types) - result.Add(type.GetFacilityZone()); + foreach (var type in types) + result.Add(type.GetFacilityZone()); - return result; - } + return result; } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Extensions/DictionaryExtension.cs b/UncomplicatedCustomRoles/Extensions/DictionaryExtension.cs index c3ef9a1..c6442ec 100644 --- a/UncomplicatedCustomRoles/Extensions/DictionaryExtension.cs +++ b/UncomplicatedCustomRoles/Extensions/DictionaryExtension.cs @@ -1,8 +1,8 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . @@ -12,95 +12,96 @@ using System.Collections.Concurrent; using System.Collections.Generic; -namespace UncomplicatedCustomRoles.Extensions +namespace UncomplicatedCustomRoles.Extensions; + +public static class DictionaryExtension { - public static class DictionaryExtension + public static void TryAdd(this Dictionary dictionary, TKey Key, TValue value) { - public static void TryAdd(this Dictionary dictionary, TKey Key, TValue value) - { - if (dictionary is null) - throw new ArgumentNullException(nameof(dictionary)); + if (dictionary is null) + throw new ArgumentNullException(nameof(dictionary)); - if (dictionary.ContainsKey(Key)) - dictionary[Key] = value; - else - dictionary.Add(Key, value); - } + if (dictionary.ContainsKey(Key)) + dictionary[Key] = value; + else + dictionary.Add(Key, value); + } - public static TValue TryGetElement(this Dictionary dictionary, TKey key, TValue ifNot) - { - if (dictionary is null) - throw new ArgumentNullException(nameof(dictionary)); + public static TValue TryGetElement(this Dictionary dictionary, TKey key, TValue ifNot) + { + if (dictionary is null) + throw new ArgumentNullException(nameof(dictionary)); - if (dictionary.ContainsKey(key)) - return dictionary[key]; + if (dictionary.ContainsKey(key)) + return dictionary[key]; + + return ifNot; + } + + public static void TryRemove(this Dictionary dictionary, TKey key) + { + if (dictionary is null) + throw new ArgumentNullException(nameof(dictionary)); + + if (dictionary.ContainsKey(key)) + dictionary.Remove(key); + } - return ifNot; - } + public static string ToRealString(this Dictionary dictionary) + { + if (dictionary is null) + return string.Empty; - public static void TryRemove(this Dictionary dictionary, TKey key) - { - if (dictionary is null) - throw new ArgumentNullException(nameof(dictionary)); + var Data = + $"[{dictionary.GetType().FullName}] Dictionary<{dictionary.GetType().GetGenericArguments()[0].FullName}, {dictionary.GetType().GetGenericArguments()[1].FullName}> ({dictionary.Count}) [\n"; - if (dictionary.ContainsKey(key)) - dictionary.Remove(key); - } + foreach (var kvp in dictionary) + Data += $"{kvp.Key}: {kvp.Value},\n"; - public static string ToRealString(this Dictionary dictionary) - { - if (dictionary is null) - return string.Empty; + Data += "];"; - string Data = $"[{dictionary.GetType().FullName}] Dictionary<{dictionary.GetType().GetGenericArguments()[0].FullName}, {dictionary.GetType().GetGenericArguments()[1].FullName}> ({dictionary.Count}) [\n"; + return Data; + } - foreach (KeyValuePair kvp in dictionary) - Data += $"{kvp.Key}: {kvp.Value},\n"; - Data += "];"; + public static Dictionary ConvertKeyToString(this Dictionary dictionary) + { + Dictionary result = new(); - return Data; - } + foreach (var kvp in dictionary) + result.Add(kvp.Key.ToString(), kvp.Value); - - public static Dictionary ConvertKeyToString(this Dictionary dictionary) - { - Dictionary result = new(); + return result; + } - foreach (KeyValuePair kvp in dictionary) - result.Add(kvp.Key.ToString(), kvp.Value); + public static Dictionary ConvertToString(this Dictionary dictionary) + { + Dictionary result = new(); - return result; - } - - public static Dictionary ConvertToString(this Dictionary dictionary) - { - Dictionary result = new(); + foreach (var kvp in dictionary) + result.Add(kvp.Key.ToString(), kvp.Value.ToString()); - foreach (KeyValuePair kvp in dictionary) - result.Add(kvp.Key.ToString(), kvp.Value.ToString()); + return result; + } - return result; - } + public static Dictionary Clone(this Dictionary dictionary) + { + Dictionary newDictionary = new(); - public static Dictionary Clone(this Dictionary dictionary) - { - Dictionary newDictionary = new(); + foreach (var kvp in dictionary) + newDictionary.Add(kvp.Key, kvp.Value); - foreach (KeyValuePair kvp in dictionary) - newDictionary.Add(kvp.Key, kvp.Value); + return newDictionary; + } - return newDictionary; - } - - public static ConcurrentDictionary Clone(this ConcurrentDictionary dictionary) - { - ConcurrentDictionary newDictionary = new(); + public static ConcurrentDictionary Clone( + this ConcurrentDictionary dictionary) + { + ConcurrentDictionary newDictionary = new(); - foreach (KeyValuePair kvp in dictionary) - newDictionary[kvp.Key] = kvp.Value; + foreach (var kvp in dictionary) + newDictionary[kvp.Key] = kvp.Value; - return newDictionary; - } + return newDictionary; } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Extensions/ListExtension.cs b/UncomplicatedCustomRoles/Extensions/ListExtension.cs index fe0ccf3..417dbbc 100644 --- a/UncomplicatedCustomRoles/Extensions/ListExtension.cs +++ b/UncomplicatedCustomRoles/Extensions/ListExtension.cs @@ -1,8 +1,8 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . @@ -11,39 +11,40 @@ using System; using System.Collections.Generic; using System.Linq; +using Random = UnityEngine.Random; -namespace UncomplicatedCustomRoles.Extensions +namespace UncomplicatedCustomRoles.Extensions; + +public static class ListExtension { - public static class ListExtension + public static void TryAdd(this List list, T item) { - public static void TryAdd(this List list, T item) - { - if (list == null) - throw new ArgumentNullException("list"); + if (list == null) + throw new ArgumentNullException("list"); - if (!list.Contains(item)) - list.Add(item); - } + if (!list.Contains(item)) + list.Add(item); + } - public static string ToRealString(this List list) - { - if (list is null) - return "null value"; + public static string ToRealString(this List list) + { + if (list is null) + return "null value"; - string data = $"[{list.GetType().FullName}] List<{list.GetType().GetGenericArguments()[0].FullName}> ({list.Count}) [\n"; + var data = + $"[{list.GetType().FullName}] List<{list.GetType().GetGenericArguments()[0].FullName}> ({list.Count}) [\n"; - foreach (T element in list) - data += $"{element},\n"; + foreach (var element in list) + data += $"{element},\n"; - data += "];"; + data += "];"; - return data; - } + return data; + } - public static T RandomValue(this IEnumerable list) - { - IList enumerable = list as IList ?? list.ToList(); - return enumerable.Count < 1 ? default : enumerable[UnityEngine.Random.Range(0, enumerable.Count)]; - } + public static T RandomValue(this IEnumerable list) + { + var enumerable = list as IList ?? list.ToList(); + return enumerable.Count < 1 ? default : enumerable[Random.Range(0, enumerable.Count)]; } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Extensions/MirrorExtension.cs b/UncomplicatedCustomRoles/Extensions/MirrorExtension.cs index dffc63e..961451c 100644 --- a/UncomplicatedCustomRoles/Extensions/MirrorExtension.cs +++ b/UncomplicatedCustomRoles/Extensions/MirrorExtension.cs @@ -5,547 +5,599 @@ // // ----------------------------------------------------------------------- -namespace UncomplicatedCustomRoles.Extensions +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; +using LabApi.Features.Wrappers; +using Mirror; +using PlayerRoles; +using PlayerRoles.FirstPersonControl; +using PlayerRoles.PlayableScps.Scp049.Zombies; +using PlayerRoles.PlayableScps.Scp1507; +using PlayerRoles.Voice; +using RelativePositioning; +using UnityEngine; +using Logger = LabApi.Features.Console.Logger; + +namespace UncomplicatedCustomRoles.Extensions; + +/// +/// A set of extensions for Networking. +/// +public static class MirrorExtensions { - using System; - using System.Collections.Generic; - using System.Collections.ObjectModel; - using System.Linq; - using System.Reflection; - using System.Reflection.Emit; - using LabApi.Features.Wrappers; - using Mirror; - - using PlayerRoles; - using PlayerRoles.FirstPersonControl; - using PlayerRoles.PlayableScps.Scp049.Zombies; - using PlayerRoles.PlayableScps.Scp1507; - using PlayerRoles.Voice; - using RelativePositioning; - using UnityEngine; + private static readonly Dictionary WriterExtensionsValue = new(); + private static readonly Dictionary SyncVarDirtyBitsValue = new(); + private static readonly Dictionary RpcFullNamesValue = new(); + + private static readonly ReadOnlyDictionary ReadOnlyWriterExtensionsValue = + new(WriterExtensionsValue); + + private static readonly ReadOnlyDictionary + ReadOnlySyncVarDirtyBitsValue = new(SyncVarDirtyBitsValue); + + private static readonly ReadOnlyDictionary ReadOnlyRpcFullNamesValue = new(RpcFullNamesValue); + private static MethodInfo setDirtyBitsMethodInfoValue; + private static MethodInfo sendSpawnMessageMethodInfoValue; /// - /// A set of extensions for Networking. + /// Gets corresponding to . /// - public static class MirrorExtensions + public static ReadOnlyDictionary WriterExtensions { - private static readonly Dictionary WriterExtensionsValue = new(); - private static readonly Dictionary SyncVarDirtyBitsValue = new(); - private static readonly Dictionary RpcFullNamesValue = new(); - private static readonly ReadOnlyDictionary ReadOnlyWriterExtensionsValue = new(WriterExtensionsValue); - private static readonly ReadOnlyDictionary ReadOnlySyncVarDirtyBitsValue = new(SyncVarDirtyBitsValue); - private static readonly ReadOnlyDictionary ReadOnlyRpcFullNamesValue = new(RpcFullNamesValue); - private static MethodInfo setDirtyBitsMethodInfoValue; - private static MethodInfo sendSpawnMessageMethodInfoValue; - - /// - /// Gets corresponding to . - /// - public static ReadOnlyDictionary WriterExtensions + get { - get + if (WriterExtensionsValue.Count == 0) { - if (WriterExtensionsValue.Count == 0) - { - foreach (MethodInfo method in typeof(NetworkWriterExtensions).GetMethods().Where(x => !x.IsGenericMethod && x.GetCustomAttribute(typeof(ObsoleteAttribute)) == null && (x.GetParameters()?.Length == 2))) - WriterExtensionsValue.Add(method.GetParameters().First(x => x.ParameterType != typeof(NetworkWriter)).ParameterType, method); - - Type fuckNorthwood = Assembly.GetAssembly(typeof(RoleTypeId)).GetType("Mirror.GeneratedNetworkCode"); - foreach (MethodInfo method in fuckNorthwood.GetMethods().Where(x => !x.IsGenericMethod && (x.GetParameters()?.Length == 2) && (x.ReturnType == typeof(void)))) - WriterExtensionsValue.Add(method.GetParameters().First(x => x.ParameterType != typeof(NetworkWriter)).ParameterType, method); - - foreach (Type serializer in typeof(ServerConsole).Assembly.GetTypes().Where(x => x.Name.EndsWith("Serializer"))) - { - foreach (MethodInfo method in serializer.GetMethods().Where(x => (x.ReturnType == typeof(void)) && x.Name.StartsWith("Write"))) - WriterExtensionsValue.Add(method.GetParameters().First(x => x.ParameterType != typeof(NetworkWriter)).ParameterType, method); - } - } - - return ReadOnlyWriterExtensionsValue; + foreach (var method in typeof(NetworkWriterExtensions).GetMethods().Where(x => + !x.IsGenericMethod && x.GetCustomAttribute(typeof(ObsoleteAttribute)) == null && + x.GetParameters()?.Length == 2)) + WriterExtensionsValue.Add( + method.GetParameters().First(x => x.ParameterType != typeof(NetworkWriter)).ParameterType, + method); + + var fuckNorthwood = Assembly.GetAssembly(typeof(RoleTypeId)).GetType("Mirror.GeneratedNetworkCode"); + foreach (var method in fuckNorthwood.GetMethods().Where(x => + !x.IsGenericMethod && x.GetParameters()?.Length == 2 && x.ReturnType == typeof(void))) + WriterExtensionsValue.Add( + method.GetParameters().First(x => x.ParameterType != typeof(NetworkWriter)).ParameterType, + method); + + foreach (var serializer in typeof(ServerConsole).Assembly.GetTypes() + .Where(x => x.Name.EndsWith("Serializer"))) + foreach (var method in serializer.GetMethods() + .Where(x => x.ReturnType == typeof(void) && x.Name.StartsWith("Write"))) + WriterExtensionsValue.Add( + method.GetParameters().First(x => x.ParameterType != typeof(NetworkWriter)).ParameterType, + method); } + + return ReadOnlyWriterExtensionsValue; } + } - /// - /// Gets a all DirtyBit from (format:classname.methodname). - /// - public static ReadOnlyDictionary SyncVarDirtyBits + /// + /// Gets a all DirtyBit from (format:classname.methodname). + /// + public static ReadOnlyDictionary SyncVarDirtyBits + { + get { - get - { - if (SyncVarDirtyBitsValue.Count == 0) + if (SyncVarDirtyBitsValue.Count == 0) + foreach (var property in typeof(ServerConsole).Assembly.GetTypes() + .SelectMany(x => x.GetProperties()) + .Where(m => m.Name.StartsWith("Network"))) { - foreach (PropertyInfo property in typeof(ServerConsole).Assembly.GetTypes() - .SelectMany(x => x.GetProperties()) - .Where(m => m.Name.StartsWith("Network"))) - { - MethodInfo setMethod = property.GetSetMethod(); + var setMethod = property.GetSetMethod(); - if (setMethod is null) - continue; + if (setMethod is null) + continue; - MethodBody methodBody = setMethod.GetMethodBody(); + var methodBody = setMethod.GetMethodBody(); - if (methodBody is null) - continue; + if (methodBody is null) + continue; - byte[] bytecodes = methodBody.GetILAsByteArray(); + var bytecodes = methodBody.GetILAsByteArray(); - if (!SyncVarDirtyBitsValue.ContainsKey($"{property.ReflectedType.Name}.{property.Name}")) - SyncVarDirtyBitsValue.Add($"{property.ReflectedType.Name}.{property.Name}", bytecodes[Array.LastIndexOf(bytecodes, (byte)OpCodes.Ldc_I8.Value) + 1]); - } + if (!SyncVarDirtyBitsValue.ContainsKey($"{property.ReflectedType.Name}.{property.Name}")) + SyncVarDirtyBitsValue.Add($"{property.ReflectedType.Name}.{property.Name}", + bytecodes[Array.LastIndexOf(bytecodes, (byte)OpCodes.Ldc_I8.Value) + 1]); } - return ReadOnlySyncVarDirtyBitsValue; - } + return ReadOnlySyncVarDirtyBitsValue; } + } - /// - /// Gets Rpc's FullName corresponding to (format:classname.methodname). - /// - public static ReadOnlyDictionary RpcFullNames + /// + /// Gets Rpc's FullName corresponding to + /// (format:classname.methodname). + /// + public static ReadOnlyDictionary RpcFullNames + { + get { - get - { - if (RpcFullNamesValue.Count == 0) + if (RpcFullNamesValue.Count == 0) + foreach (var method in typeof(ServerConsole).Assembly.GetTypes() + .SelectMany(x => + x.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)) + .Where(m => m.GetCustomAttributes(typeof(ClientRpcAttribute), false).Length > 0 || + m.GetCustomAttributes(typeof(TargetRpcAttribute), false).Length > 0)) { - foreach (MethodInfo method in typeof(ServerConsole).Assembly.GetTypes() - .SelectMany(x => x.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)) - .Where(m => m.GetCustomAttributes(typeof(ClientRpcAttribute), false).Length > 0 || m.GetCustomAttributes(typeof(TargetRpcAttribute), false).Length > 0)) - { - MethodBody methodBody = method.GetMethodBody(); + var methodBody = method.GetMethodBody(); - if (methodBody is null) - continue; + if (methodBody is null) + continue; - byte[] bytecodes = methodBody.GetILAsByteArray(); + var bytecodes = methodBody.GetILAsByteArray(); - if (!RpcFullNamesValue.ContainsKey($"{method.ReflectedType.Name}.{method.Name}")) - RpcFullNamesValue.Add($"{method.ReflectedType.Name}.{method.Name}", method.Module.ResolveString(BitConverter.ToInt32(bytecodes, Array.IndexOf(bytecodes, (byte)OpCodes.Ldstr.Value) + 1))); - } + if (!RpcFullNamesValue.ContainsKey($"{method.ReflectedType.Name}.{method.Name}")) + RpcFullNamesValue.Add($"{method.ReflectedType.Name}.{method.Name}", + method.Module.ResolveString(BitConverter.ToInt32(bytecodes, + Array.IndexOf(bytecodes, (byte)OpCodes.Ldstr.Value) + 1))); } - return ReadOnlyRpcFullNamesValue; - } + return ReadOnlyRpcFullNamesValue; } + } - /// - /// Gets a 's . - /// - public static MethodInfo SetDirtyBitsMethodInfo => setDirtyBitsMethodInfoValue ??= typeof(NetworkBehaviour).GetMethod(nameof(NetworkBehaviour.SetSyncVarDirtyBit)); - - /// - /// Gets a NetworkServer.SendSpawnMessage's . - /// - public static MethodInfo SendSpawnMessageMethodInfo => sendSpawnMessageMethodInfoValue ??= typeof(NetworkServer).GetMethod("SendSpawnMessage", BindingFlags.NonPublic | BindingFlags.Static); - - /// - /// Plays a beep sound that only the target can hear. - /// - /// Target to play sound to. - public static void PlayBeepSound(this Player player) => SendFakeTargetRpc(player, ReferenceHub._hostHub.networkIdentity, typeof(AmbientSoundPlayer), nameof(AmbientSoundPlayer.RpcPlaySound), 7); - - /// - /// Set on the player that only the can see. - /// - /// Only this player can see info. - /// Target to set info. - /// Setting info. - public static void SetPlayerInfoForTargetOnly(this Player player, Player target, string info) => player.SendFakeSyncVar(target.ReferenceHub.networkIdentity, typeof(NicknameSync), nameof(NicknameSync.Network_customPlayerInfoString), info); - - /// - /// Sets that only the player can see. - /// - /// Only this player can see Display Text. - /// Text displayed to the player. - public static void SetIntercomDisplayTextForTargetOnly(this Player target, string text) => target.SendFakeSyncVar(IntercomDisplay._singleton.netIdentity, typeof(IntercomDisplay), nameof(IntercomDisplay.Network_overrideText), text); - - /// - /// Resync . - /// - public static void ResetIntercomDisplayText() => ResyncSyncVar(IntercomDisplay._singleton.netIdentity, typeof(IntercomDisplay), nameof(IntercomDisplay.Network_overrideText)); - - /// - /// Change character model for appearance. - /// It will continue until 's changes. - /// - /// Player to change. - /// Model type. - /// Whether to skip the little jump that works around an invisibility issue. - /// The UnitNameId to use for the player's new role, if the player's new role uses unit names. (is NTF). - public static void ChangeAppearance(this Player player, RoleTypeId type, bool skipJump = false, byte unitId = 0) => ChangeAppearance(player, type, Player.ReadyList.Where(x => x != player), skipJump, unitId); - - /// - /// Change character model for appearance. - /// It will continue until 's changes. - /// - /// Player to change. - /// Model type. - /// The players who should see the changed appearance. - /// Whether to skip the little jump that works around an invisibility issue. - /// The UnitNameId to use for the player's new role, if the player's new role uses unit names. (is NTF). - public static void ChangeAppearance(this Player player, RoleTypeId type, IEnumerable playersToAffect, bool skipJump = false, byte unitId = 0) - { - if (!player.Connection.isReady || !RoleExtension.TryGetRoleBase(type, out PlayerRoleBase roleBase)) - return; - - bool isRisky = type.GetTeam() is Team.Dead || !player.IsAlive; - - NetworkWriterPooled writer = NetworkWriterPool.Get(); - writer.WriteUShort(38952); - writer.WriteUInt(player.NetworkId); - writer.WriteRoleType(type); + /// + /// Gets a 's . + /// + public static MethodInfo SetDirtyBitsMethodInfo => setDirtyBitsMethodInfoValue ??= + typeof(NetworkBehaviour).GetMethod(nameof(NetworkBehaviour.SetSyncVarDirtyBit)); - if (roleBase is HumanRole humanRole && humanRole.UsesUnitNames) - { - if (player.RoleBase is not HumanRole) - isRisky = true; - writer.WriteByte(unitId); - } + /// + /// Gets a NetworkServer.SendSpawnMessage's . + /// + public static MethodInfo SendSpawnMessageMethodInfo => sendSpawnMessageMethodInfoValue ??= + typeof(NetworkServer).GetMethod("SendSpawnMessage", BindingFlags.NonPublic | BindingFlags.Static); - if (roleBase is ZombieRole) - { - if (player.RoleBase is not ZombieRole) - isRisky = true; + /// + /// Plays a beep sound that only the target can hear. + /// + /// Target to play sound to. + public static void PlayBeepSound(this Player player) + { + SendFakeTargetRpc(player, ReferenceHub._hostHub.networkIdentity, typeof(AmbientSoundPlayer), + nameof(AmbientSoundPlayer.RpcPlaySound), 7); + } - writer.WriteUShort((ushort)Mathf.Clamp(Mathf.CeilToInt(player.MaxHealth), ushort.MinValue, ushort.MaxValue)); - writer.WriteBool(true); - } + /// + /// Set on the player that only the + /// can see. + /// + /// Only this player can see info. + /// Target to set info. + /// Setting info. + public static void SetPlayerInfoForTargetOnly(this Player player, Player target, string info) + { + player.SendFakeSyncVar(target.ReferenceHub.networkIdentity, typeof(NicknameSync), + nameof(NicknameSync.Network_customPlayerInfoString), info); + } - if (roleBase is Scp1507Role) - { - if (player.RoleBase is not Scp1507Role) - isRisky = true; + /// + /// Sets that only the player can see. + /// + /// Only this player can see Display Text. + /// Text displayed to the player. + public static void SetIntercomDisplayTextForTargetOnly(this Player target, string text) + { + target.SendFakeSyncVar(IntercomDisplay._singleton.netIdentity, typeof(IntercomDisplay), + nameof(IntercomDisplay.Network_overrideText), text); + } - writer.WriteByte((byte)player.RoleBase.ServerSpawnReason); - } + /// + /// Resync . + /// + public static void ResetIntercomDisplayText() + { + ResyncSyncVar(IntercomDisplay._singleton.netIdentity, typeof(IntercomDisplay), + nameof(IntercomDisplay.Network_overrideText)); + } - if (roleBase is FpcStandardRoleBase fpc) - { - if (player.RoleBase is not FpcStandardRoleBase playerfpc) - isRisky = true; - else - fpc = playerfpc; - - ushort value = 0; - fpc?.FpcModule.MouseLook.GetSyncValues(0, out value, out ushort _); - writer.WriteRelativePosition(new(player.Position)); - writer.WriteUShort(value); - } + /// + /// Change character model for appearance. + /// It will continue until 's changes. + /// + /// Player to change. + /// Model type. + /// Whether to skip the little jump that works around an invisibility issue. + /// + /// The UnitNameId to use for the player's new role, if the player's new role uses unit names. (is + /// NTF). + /// + public static void ChangeAppearance(this Player player, RoleTypeId type, bool skipJump = false, byte unitId = 0) + { + player.ChangeAppearance(type, Player.ReadyList.Where(x => x != player), skipJump, unitId); + } - foreach (Player target in playersToAffect) - { - if (target != player || !isRisky) - target.Connection.Send(writer.ToArraySegment()); - else - LabApi.Features.Console.Logger.Error($"Prevent Seld-Desync of {player.Nickname} with {type}"); - } + /// + /// Change character model for appearance. + /// It will continue until 's changes. + /// + /// Player to change. + /// Model type. + /// The players who should see the changed appearance. + /// Whether to skip the little jump that works around an invisibility issue. + /// + /// The UnitNameId to use for the player's new role, if the player's new role uses unit names. (is + /// NTF). + /// + public static void ChangeAppearance(this Player player, RoleTypeId type, IEnumerable playersToAffect, + bool skipJump = false, byte unitId = 0) + { + if (!player.Connection.isReady || !type.TryGetRoleBase(out var roleBase)) + return; - NetworkWriterPool.Return(writer); + var isRisky = type.GetTeam() is Team.Dead || !player.IsAlive; - // To counter a bug that makes the player invisible until they move after changing their appearance, we will teleport them upwards slightly to force a new position update for all clients. - if (!skipJump) - player.Position += Vector3.up * 0.25f; - } + var writer = NetworkWriterPool.Get(); + writer.WriteUShort(38952); + writer.WriteUInt(player.NetworkId); + writer.WriteRoleType(type); - // todo: Later check this - /* - /// - /// Send CASSIE announcement that only can hear. - /// - /// Target to send. - /// Announcement words. - /// Same on 's isHeld. - /// Same on 's isNoisy. - /// Same on 's isSubtitles. - public static void PlayCassieAnnouncement(this Player player, string words, bool makeHold = false, bool makeNoise = true, bool isSubtitles = false) + if (roleBase is HumanRole humanRole && humanRole.UsesUnitNames) { - foreach (RespawnEffectsController controller in RespawnEffectsController.AllControllers) - { - if (controller != null) - { - SendFakeTargetRpc(player, controller.netIdentity, typeof(RespawnEffectsController), nameof(RespawnEffectsController.RpcCassieAnnouncement), words, makeHold, makeNoise, isSubtitles); - } - } + if (player.RoleBase is not HumanRole) + isRisky = true; + writer.WriteByte(unitId); } - /// - /// Send CASSIE announcement with custom subtitles for translation that only can hear and see it. - /// - /// Target to send. - /// The message to be reproduced. - /// The translation should be show in the subtitles. - /// Same on 's isHeld. - /// Same on 's isNoisy. - /// Same on 's isSubtitles. - public static void MessageTranslated(this Player player, string words, string translation, bool makeHold = false, bool makeNoise = true, bool isSubtitles = true) + if (roleBase is ZombieRole) { - StringBuilder announcement = new(); - - string[] cassies = words.Split('\n'); - string[] translations = translation.Split('\n'); + if (player.RoleBase is not ZombieRole) + isRisky = true; - for (int i = 0; i < cassies.Length; i++) - announcement.Append($"{translations[i].Replace(' ', ' ')} {cassies[i]} "); - - string message = announcement.ToString(); + writer.WriteUShort((ushort)Mathf.Clamp(Mathf.CeilToInt(player.MaxHealth), ushort.MinValue, + ushort.MaxValue)); + writer.WriteBool(true); + } - foreach (RespawnEffectsController controller in RespawnEffectsController.AllControllers) - { - if (controller != null) - { - SendFakeTargetRpc(player, controller.netIdentity, typeof(RespawnEffectsController), nameof(RespawnEffectsController.RpcCassieAnnouncement), message, makeHold, makeNoise, isSubtitles); - } - } - }*/ - - /// - /// Moves object for the player. - /// - /// Target to send. - /// The to move. - /// The position to change. - public static void MoveNetworkIdentityObject(this Player player, NetworkIdentity identity, Vector3 pos) + if (roleBase is Scp1507Role) { - identity.gameObject.transform.position = pos; - ObjectDestroyMessage objectDestroyMessage = new() - { - netId = identity.netId, - }; + if (player.RoleBase is not Scp1507Role) + isRisky = true; - player.Connection.Send(objectDestroyMessage, 0); - SendSpawnMessageMethodInfo?.Invoke(null, new object[] { identity, player.Connection }); + writer.WriteByte((byte)player.RoleBase.ServerSpawnReason); } - /// - /// Scales an object for the specified player. - /// - /// Target to send. - /// The to scale. - /// The scale the object needs to be set to. - public static void ScaleNetworkIdentityObject(this Player player, NetworkIdentity identity, Vector3 scale) + if (roleBase is FpcStandardRoleBase fpc) { - identity.gameObject.transform.localScale = scale; - ObjectDestroyMessage objectDestroyMessage = new() - { - netId = identity.netId, - }; + if (player.RoleBase is not FpcStandardRoleBase playerfpc) + isRisky = true; + else + fpc = playerfpc; - player.Connection.Send(objectDestroyMessage, 0); - SendSpawnMessageMethodInfo?.Invoke(null, new object[] { identity, player.Connection }); + ushort value = 0; + fpc?.FpcModule.MouseLook.GetSyncValues(0, out value, out var _); + writer.WriteRelativePosition(new RelativePosition(player.Position)); + writer.WriteUShort(value); } - /// - /// Moves object for all the players. - /// - /// The to move. - /// The position to change. - public static void MoveNetworkIdentityObject(this NetworkIdentity identity, Vector3 pos) - { - identity.gameObject.transform.position = pos; - ObjectDestroyMessage objectDestroyMessage = new() - { - netId = identity.netId, - }; + foreach (var target in playersToAffect) + if (target != player || !isRisky) + target.Connection.Send(writer.ToArraySegment()); + else + Logger.Error($"Prevent Seld-Desync of {player.Nickname} with {type}"); - foreach (Player ply in Player.ReadyList) - { - ply.Connection.Send(objectDestroyMessage, 0); - SendSpawnMessageMethodInfo?.Invoke(null, new object[] { identity, ply.Connection }); - } - } + NetworkWriterPool.Return(writer); - /// - /// Scales an object for all players. - /// - /// The to scale. - /// The scale the object needs to be set to. - public static void ScaleNetworkIdentityObject(this NetworkIdentity identity, Vector3 scale) - { - identity.gameObject.transform.localScale = scale; - ObjectDestroyMessage objectDestroyMessage = new() - { - netId = identity.netId, - }; + // To counter a bug that makes the player invisible until they move after changing their appearance, we will teleport them upwards slightly to force a new position update for all clients. + if (!skipJump) + player.Position += Vector3.up * 0.25f; + } - foreach (Player ply in Player.ReadyList) + // todo: Later check this + /* + /// + /// Send CASSIE announcement that only can hear. + /// + /// Target to send. + /// Announcement words. + /// Same on 's isHeld. + /// Same on 's isNoisy. + /// Same on 's isSubtitles. + public static void PlayCassieAnnouncement(this Player player, string words, bool makeHold = false, bool makeNoise = true, bool isSubtitles = false) + { + foreach (RespawnEffectsController controller in RespawnEffectsController.AllControllers) + { + if (controller != null) { - ply.Connection.Send(objectDestroyMessage, 0); - SendSpawnMessageMethodInfo?.Invoke(null, new object[] { identity, ply.Connection }); + SendFakeTargetRpc(player, controller.netIdentity, typeof(RespawnEffectsController), nameof(RespawnEffectsController.RpcCassieAnnouncement), words, makeHold, makeNoise, isSubtitles); } } + } - /// - /// Send fake values to client's . - /// - /// Target SyncVar property type. - /// Target to send. - /// of object that owns . - /// 's type. - /// Property name starting with Network. - /// Value of send to target. - public static void SendFakeSyncVar(this Player target, NetworkIdentity behaviorOwner, Type targetType, string propertyName, T value) - { - if (!target.Connection.isReady) - return; + /// + /// Send CASSIE announcement with custom subtitles for translation that only can hear and see it. + /// + /// Target to send. + /// The message to be reproduced. + /// The translation should be show in the subtitles. + /// Same on 's isHeld. + /// Same on 's isNoisy. + /// Same on 's isSubtitles. + public static void MessageTranslated(this Player player, string words, string translation, bool makeHold = false, bool makeNoise = true, bool isSubtitles = true) + { + StringBuilder announcement = new(); - NetworkWriterPooled writer = NetworkWriterPool.Get(); - NetworkWriterPooled writer2 = NetworkWriterPool.Get(); - MakeCustomSyncWriter(behaviorOwner, targetType, null, CustomSyncVarGenerator, writer, writer2); - target.Connection.Send(new EntityStateMessage - { - netId = behaviorOwner.netId, - payload = writer.ToArraySegment(), - }); + string[] cassies = words.Split('\n'); + string[] translations = translation.Split('\n'); - NetworkWriterPool.Return(writer); - NetworkWriterPool.Return(writer2); - void CustomSyncVarGenerator(NetworkWriter targetWriter) + for (int i = 0; i < cassies.Length; i++) + announcement.Append($"{translations[i].Replace(' ', ' ')} {cassies[i]} "); + + string message = announcement.ToString(); + + foreach (RespawnEffectsController controller in RespawnEffectsController.AllControllers) + { + if (controller != null) { - targetWriter.WriteULong(SyncVarDirtyBits[$"{targetType.Name}.{propertyName}"]); - WriterExtensions[typeof(T)]?.Invoke(null, new object[2] { targetWriter, value }); + SendFakeTargetRpc(player, controller.netIdentity, typeof(RespawnEffectsController), nameof(RespawnEffectsController.RpcCassieAnnouncement), message, makeHold, makeNoise, isSubtitles); } } + }*/ - /// - /// Force resync to client's . - /// - /// of object that owns . - /// 's type. - /// Property name starting with Network. - public static void ResyncSyncVar(NetworkIdentity behaviorOwner, Type targetType, string propertyName) => SetDirtyBitsMethodInfo.Invoke(behaviorOwner.gameObject.GetComponent(targetType), new object[] { SyncVarDirtyBits[$"{targetType.Name}.{propertyName}"] }); - - /// - /// Send fake values to client's . - /// - /// Target to send. - /// of object that owns . - /// 's type. - /// Property name starting with Rpc. - /// Values of send to target. - public static void SendFakeTargetRpc(Player target, NetworkIdentity behaviorOwner, Type targetType, string rpcName, params object[] values) + /// + /// Moves object for the player. + /// + /// Target to send. + /// The to move. + /// The position to change. + public static void MoveNetworkIdentityObject(this Player player, NetworkIdentity identity, Vector3 pos) + { + identity.gameObject.transform.position = pos; + ObjectDestroyMessage objectDestroyMessage = new() { - if (!target.Connection.isReady) - return; + netId = identity.netId + }; - NetworkWriterPooled writer = NetworkWriterPool.Get(); + player.Connection.Send(objectDestroyMessage); + SendSpawnMessageMethodInfo?.Invoke(null, [identity, player.Connection]); + } - foreach (object value in values) - WriterExtensions[value.GetType()].Invoke(null, new[] { writer, value }); + /// + /// Scales an object for the specified player. + /// + /// Target to send. + /// The to scale. + /// The scale the object needs to be set to. + public static void ScaleNetworkIdentityObject(this Player player, NetworkIdentity identity, Vector3 scale) + { + identity.gameObject.transform.localScale = scale; + ObjectDestroyMessage objectDestroyMessage = new() + { + netId = identity.netId + }; - RpcMessage msg = new() - { - netId = behaviorOwner.netId, - componentIndex = (byte)GetComponentIndex(behaviorOwner, targetType), - functionHash = (ushort)RpcFullNames[$"{targetType.Name}.{rpcName}"].GetStableHashCode(), - payload = writer.ToArraySegment(), - }; + player.Connection.Send(objectDestroyMessage); + SendSpawnMessageMethodInfo?.Invoke(null, [identity, player.Connection]); + } - target.Connection.Send(msg); + /// + /// Moves object for all the players. + /// + /// The to move. + /// The position to change. + public static void MoveNetworkIdentityObject(this NetworkIdentity identity, Vector3 pos) + { + identity.gameObject.transform.position = pos; + ObjectDestroyMessage objectDestroyMessage = new() + { + netId = identity.netId + }; - NetworkWriterPool.Return(writer); + foreach (var ply in Player.ReadyList) + { + ply.Connection.Send(objectDestroyMessage); + SendSpawnMessageMethodInfo?.Invoke(null, [identity, ply.Connection]); } + } + + /// + /// Scales an object for all players. + /// + /// The to scale. + /// The scale the object needs to be set to. + public static void ScaleNetworkIdentityObject(this NetworkIdentity identity, Vector3 scale) + { + identity.gameObject.transform.localScale = scale; + ObjectDestroyMessage objectDestroyMessage = new() + { + netId = identity.netId + }; - /// - /// Send fake values to client's . - /// - /// Target to send. - /// of object that owns . - /// 's type. - /// Custom writing action. - /// - /// EffectOnlySCP207. - /// - /// MirrorExtensions.SendFakeSyncObject(player, player.NetworkIdentity, typeof(PlayerEffectsController), (writer) => { - /// writer.WriteULong(1ul); // DirtyObjectsBit - /// writer.WriteUInt(1); // DirtyIndexCount - /// writer.WriteByte((byte)SyncList<byte>.Operation.OP_SET); // Operations - /// writer.WriteUInt(17); // EditIndex - /// }); - /// - /// - public static void SendFakeSyncObject(Player target, NetworkIdentity behaviorOwner, Type targetType, Action customAction) + foreach (var ply in Player.ReadyList) { - if (!target.Connection.isReady) - return; - - NetworkWriterPooled writer = NetworkWriterPool.Get(); - NetworkWriterPooled writer2 = NetworkWriterPool.Get(); - MakeCustomSyncWriter(behaviorOwner, targetType, customAction, null, writer, writer2); - target.ReferenceHub.networkIdentity.connectionToClient.Send(new EntityStateMessage() { netId = behaviorOwner.netId, payload = writer.ToArraySegment() }); - NetworkWriterPool.Return(writer); - NetworkWriterPool.Return(writer2); + ply.Connection.Send(objectDestroyMessage); + SendSpawnMessageMethodInfo?.Invoke(null, [identity, ply.Connection]); } + } - /// - /// Edit 's parameter and sync. - /// - /// Target object. - /// Edit function. - public static void EditNetworkObject(NetworkIdentity identity, Action customAction) + /// + /// Send fake values to client's . + /// + /// Target SyncVar property type. + /// Target to send. + /// of object that owns . + /// 's type. + /// Property name starting with Network. + /// Value of send to target. + public static void SendFakeSyncVar(this Player target, NetworkIdentity behaviorOwner, Type targetType, + string propertyName, T value) + { + if (!target.Connection.isReady) + return; + + var writer = NetworkWriterPool.Get(); + var writer2 = NetworkWriterPool.Get(); + MakeCustomSyncWriter(behaviorOwner, targetType, null, CustomSyncVarGenerator, writer, writer2); + target.Connection.Send(new EntityStateMessage { - customAction.Invoke(identity); + netId = behaviorOwner.netId, + payload = writer.ToArraySegment() + }); - ObjectDestroyMessage objectDestroyMessage = new() - { - netId = identity.netId, - }; + NetworkWriterPool.Return(writer); + NetworkWriterPool.Return(writer2); - foreach (Player player in Player.ReadyList) - { - player.Connection.Send(objectDestroyMessage, 0); - SendSpawnMessageMethodInfo.Invoke(null, new object[] { identity, player.Connection }); - } + void CustomSyncVarGenerator(NetworkWriter targetWriter) + { + targetWriter.WriteULong(SyncVarDirtyBits[$"{targetType.Name}.{propertyName}"]); + WriterExtensions[typeof(T)]?.Invoke(null, [targetWriter, value]); } + } - // Get components index in identity.(private) - private static int GetComponentIndex(NetworkIdentity identity, Type type) + /// + /// Force resync to client's . + /// + /// of object that owns . + /// 's type. + /// Property name starting with Network. + public static void ResyncSyncVar(NetworkIdentity behaviorOwner, Type targetType, string propertyName) + { + SetDirtyBitsMethodInfo.Invoke(behaviorOwner.gameObject.GetComponent(targetType), + [SyncVarDirtyBits[$"{targetType.Name}.{propertyName}"]]); + } + + /// + /// Send fake values to client's . + /// + /// Target to send. + /// of object that owns . + /// 's type. + /// Property name starting with Rpc. + /// Values of send to target. + public static void SendFakeTargetRpc(Player target, NetworkIdentity behaviorOwner, Type targetType, string rpcName, + params object[] values) + { + if (!target.Connection.isReady) + return; + + var writer = NetworkWriterPool.Get(); + + foreach (var value in values) + WriterExtensions[value.GetType()].Invoke(null, [writer, value]); + + RpcMessage msg = new() { - return Array.FindIndex(identity.NetworkBehaviours, (x) => x.GetType() == type); - } + netId = behaviorOwner.netId, + componentIndex = (byte)GetComponentIndex(behaviorOwner, targetType), + functionHash = (ushort)RpcFullNames[$"{targetType.Name}.{rpcName}"].GetStableHashCode(), + payload = writer.ToArraySegment() + }; - // Make custom writer(private) - private static void MakeCustomSyncWriter(NetworkIdentity behaviorOwner, Type targetType, Action customSyncObject, Action customSyncVar, NetworkWriter owner, NetworkWriter observer) + target.Connection.Send(msg); + + NetworkWriterPool.Return(writer); + } + + /// + /// Send fake values to client's . + /// + /// Target to send. + /// of object that owns . + /// 's type. + /// Custom writing action. + /// + /// EffectOnlySCP207. + /// + /// MirrorExtensions.SendFakeSyncObject(player, player.NetworkIdentity, typeof(PlayerEffectsController), (writer) => { + /// writer.WriteULong(1ul); // DirtyObjectsBit + /// writer.WriteUInt(1); // DirtyIndexCount + /// writer.WriteByte((byte)SyncList<byte>.Operation.OP_SET); // Operations + /// writer.WriteUInt(17); // EditIndex + /// }); + /// + /// + public static void SendFakeSyncObject(Player target, NetworkIdentity behaviorOwner, Type targetType, + Action customAction) + { + if (!target.Connection.isReady) + return; + + var writer = NetworkWriterPool.Get(); + var writer2 = NetworkWriterPool.Get(); + MakeCustomSyncWriter(behaviorOwner, targetType, customAction, null, writer, writer2); + target.ReferenceHub.networkIdentity.connectionToClient.Send(new EntityStateMessage + { netId = behaviorOwner.netId, payload = writer.ToArraySegment() }); + NetworkWriterPool.Return(writer); + NetworkWriterPool.Return(writer2); + } + + /// + /// Edit 's parameter and sync. + /// + /// Target object. + /// Edit function. + public static void EditNetworkObject(NetworkIdentity identity, Action customAction) + { + customAction.Invoke(identity); + + ObjectDestroyMessage objectDestroyMessage = new() { - ulong value = 0; - NetworkBehaviour behaviour = null; + netId = identity.netId + }; + + foreach (var player in Player.ReadyList) + { + player.Connection.Send(objectDestroyMessage); + SendSpawnMessageMethodInfo.Invoke(null, [identity, player.Connection]); + } + } + + // Get components index in identity.(private) + private static int GetComponentIndex(NetworkIdentity identity, Type type) + { + return Array.FindIndex(identity.NetworkBehaviours, x => x.GetType() == type); + } + + // Make custom writer(private) + private static void MakeCustomSyncWriter(NetworkIdentity behaviorOwner, Type targetType, + Action customSyncObject, Action customSyncVar, NetworkWriter owner, + NetworkWriter observer) + { + ulong value = 0; + NetworkBehaviour behaviour = null; - // Get NetworkBehaviors index (behaviorDirty use index) - for (int i = 0; i < behaviorOwner.NetworkBehaviours.Length; i++) + // Get NetworkBehaviors index (behaviorDirty use index) + for (var i = 0; i < behaviorOwner.NetworkBehaviours.Length; i++) + if (behaviorOwner.NetworkBehaviours[i].GetType() == targetType) { - if (behaviorOwner.NetworkBehaviours[i].GetType() == targetType) - { - behaviour = behaviorOwner.NetworkBehaviours[i]; - value = 1UL << (i & 31); - break; - } + behaviour = behaviorOwner.NetworkBehaviours[i]; + value = 1UL << (i & 31); + break; } - // Write target NetworkBehavior's dirty - Compression.CompressVarUInt(owner, value); + // Write target NetworkBehavior's dirty + Compression.CompressVarUInt(owner, value); - // Write init position - int position = owner.Position; - owner.WriteByte(0); - int position2 = owner.Position; + // Write init position + var position = owner.Position; + owner.WriteByte(0); + var position2 = owner.Position; - // Write custom sync data - if (customSyncObject is not null) - customSyncObject(owner); - else - behaviour.SerializeObjectsDelta(owner); + // Write custom sync data + if (customSyncObject is not null) + customSyncObject(owner); + else + behaviour.SerializeObjectsDelta(owner); - // Write custom syncvar - customSyncVar?.Invoke(owner); + // Write custom syncvar + customSyncVar?.Invoke(owner); - // Write syncdata position data - int position3 = owner.Position; - owner.Position = position; - owner.WriteByte((byte)(position3 - position2 & 255)); - owner.Position = position3; + // Write syncdata position data + var position3 = owner.Position; + owner.Position = position; + owner.WriteByte((byte)((position3 - position2) & 255)); + owner.Position = position3; - // Copy owner to observer - if (behaviour.syncMode != SyncMode.Observers) - observer.WriteBytes(owner.ToArraySegment().Array, position, owner.Position - position); - } + // Copy owner to observer + if (behaviour.syncMode != SyncMode.Observers) + observer.WriteBytes(owner.ToArraySegment().Array, position, owner.Position - position); } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Extensions/PlayerExtension.cs b/UncomplicatedCustomRoles/Extensions/PlayerExtension.cs index f498f52..ac6806d 100644 --- a/UncomplicatedCustomRoles/Extensions/PlayerExtension.cs +++ b/UncomplicatedCustomRoles/Extensions/PlayerExtension.cs @@ -1,198 +1,209 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . */ +using System; +using System.Collections.Generic; +using System.Linq; using InventorySystem.Configs; using LabApi.Features.Wrappers; using MEC; using Mirror; using PlayerRoles; -using System; -using System.Collections.Generic; -using System.Linq; -using CustomPlayerEffects; using UncomplicatedCustomRoles.API.Features; using UncomplicatedCustomRoles.API.Interfaces; using UncomplicatedCustomRoles.Manager; -using UnityEngine; -namespace UncomplicatedCustomRoles.Extensions +namespace UncomplicatedCustomRoles.Extensions; + +public static class PlayerExtension { - public static class PlayerExtension + /// + /// Check if a is currently a . + /// + /// + /// if the player is a custom role. + public static bool HasCustomRole(this Player player) { - /// - /// Check if a is currently a . - /// - /// - /// if the player is a custom role. - public static bool HasCustomRole(this Player player) - { - return SummonedCustomRole.TryGet(player, out _); - } + return SummonedCustomRole.TryGet(player, out _); + } - internal static void ForceApplyEffect(this ReferenceHub hub, string effectName, byte intensity, float duration, bool addDuration = false) - { - if (hub is null || !hub.playerEffectsController.TryGetEffect(effectName, out StatusEffectBase effect)) - return; + internal static void ForceApplyEffect(this ReferenceHub hub, string effectName, byte intensity, float duration, + bool addDuration = false) + { + if (hub is null || !hub.playerEffectsController.TryGetEffect(effectName, out var effect)) + return; - effect.ForceIntensity(intensity); - effect.ServerChangeDuration(duration, addDuration); - } - - /// - /// Set a to a without a coroutine. - /// - /// - /// - public static void SetCustomRoleSync(this Player player, ICustomRole role) - { - SpawnManager.ClearCustomTypes(player); - SpawnManager.SummonCustomSubclass(player, role.Id, true); - } + effect.ForceIntensity(intensity); + effect.ServerChangeDuration(duration, addDuration); + } - /// - /// Set a (via it's Id) to a without a coroutine. - /// - /// - /// - public static void SetCustomRoleSync(this Player player, int role) - { - SpawnManager.ClearCustomTypes(player); - SpawnManager.SummonCustomSubclass(player, role, true); - } + /// + /// Set a to a without a coroutine. + /// + /// + /// + public static void SetCustomRoleSync(this Player player, ICustomRole role) + { + SpawnManager.ClearCustomTypes(player); + SpawnManager.SummonCustomSubclass(player, role.Id); + } - /// - /// Set a (via it's Id) to a . - /// - /// - /// - public static void SetCustomRole(this Player player, int role) - { - SpawnManager.ClearCustomTypes(player); - Timing.RunCoroutine(SpawnManager.AsyncPlayerSpawner(player, role)); - } + /// + /// Set a (via it's Id) to a without a coroutine. + /// + /// + /// + public static void SetCustomRoleSync(this Player player, int role) + { + SpawnManager.ClearCustomTypes(player); + SpawnManager.SummonCustomSubclass(player, role); + } - /// - /// Set a to a . - /// - /// - /// - public static void SetCustomRole(this Player player, ICustomRole role) - { - SpawnManager.ClearCustomTypes(player); - Timing.RunCoroutine(SpawnManager.AsyncPlayerSpawner(player, role.Id)); - } + /// + /// Set a (via it's Id) to a . + /// + /// + /// + public static void SetCustomRole(this Player player, int role) + { + SpawnManager.ClearCustomTypes(player); + Timing.RunCoroutine(SpawnManager.AsyncPlayerSpawner(player, role)); + } - /// - /// Set every attribute of a given to a without considering the .

- /// Use this only at your own risk and only if you know what you are doing! - ///
- /// - /// - [Obsolete("You should not use this function unless you want to handle the role spawn by yourself!", false)] - public static void SetCustomRoleAttributes(this Player player, ICustomRole role) - { - SpawnManager.ClearCustomTypes(player); - SpawnManager.SummonSubclassApplier(player, role); - } + /// + /// Set a to a . + /// + /// + /// + public static void SetCustomRole(this Player player, ICustomRole role) + { + SpawnManager.ClearCustomTypes(player); + Timing.RunCoroutine(SpawnManager.AsyncPlayerSpawner(player, role.Id)); + } - /// - /// Try to get the current of a if it has one. - /// - /// - /// true if the player is currently - public static bool TryGetSummonedInstance(this Player player, out SummonedCustomRole summonedInstance) - { - summonedInstance = GetSummonedInstance(player); - return summonedInstance != null; - } + /// + /// Set every attribute of a given to a without considering the + /// .

+ /// Use this only at your own risk and only if you know what you are doing! + ///
+ /// + /// + [Obsolete("You should not use this function unless you want to handle the role spawn by yourself!", false)] + public static void SetCustomRoleAttributes(this Player player, ICustomRole role) + { + SpawnManager.ClearCustomTypes(player); + SpawnManager.SummonSubclassApplier(player, role); + } - /// - /// Try to get the current of a if it has one. - /// - /// - /// true if the player is currently - public static bool TryGetSummonedInstance(this ReferenceHub player, out SummonedCustomRole summonedInstance) - { - summonedInstance = GetSummonedInstance(player); - return summonedInstance != null; - } + /// + /// Try to get the current of a if it has one. + /// + /// + /// true if the player is currently + public static bool TryGetSummonedInstance(this Player player, out SummonedCustomRole summonedInstance) + { + summonedInstance = player.GetSummonedInstance(); + return summonedInstance != null; + } - /// - /// Get the current of a if it has one. - /// - /// - /// The current if the player has one, otherwise - public static SummonedCustomRole GetSummonedInstance(this Player player) => SummonedCustomRole.Get(player); - - /// - /// Get the current of a if it has one. - /// - /// - /// - public static SummonedCustomRole GetSummonedInstance(this ReferenceHub player) => SummonedCustomRole.Get(player); - - /// - /// Try to remove a from a if it has one. - /// - /// - /// If true the role will be resetted => modified stats like health and other things will be lost - /// True if success - public static bool TryRemoveCustomRole(this Player player, bool doResetRole = false) - { - if (SummonedCustomRole.TryGet(player, out SummonedCustomRole result)) - { - RoleTypeId Role = result.Role.Role; - result.Destroy(); + /// + /// Try to get the current of a if it has one. + /// + /// + /// true if the player is currently + public static bool TryGetSummonedInstance(this ReferenceHub player, out SummonedCustomRole summonedInstance) + { + summonedInstance = player.GetSummonedInstance(); + return summonedInstance != null; + } - if (doResetRole) - { - Vector3 OriginalPosition = player.Position; + /// + /// Get the current of a if it has one. + /// + /// + /// The current if the player has one, otherwise + public static SummonedCustomRole GetSummonedInstance(this Player player) + { + return SummonedCustomRole.Get(player); + } - player.SetRole(Role, RoleChangeReason.Destroyed, RoleSpawnFlags.AssignInventory); + /// + /// Get the current of a if it has one. + /// + /// + /// + public static SummonedCustomRole GetSummonedInstance(this ReferenceHub player) + { + return SummonedCustomRole.Get(player); + } - player.Position = OriginalPosition; - } + /// + /// Try to remove a from a if it has one. + /// + /// + /// If true the role will be resetted => modified stats like health and other things will be lost + /// True if success + public static bool TryRemoveCustomRole(this Player player, bool doResetRole = false) + { + if (SummonedCustomRole.TryGet(player, out var result)) + { + var Role = result.Role.Role; + result.Destroy(); + + if (doResetRole) + { + var OriginalPosition = player.Position; - return true; + player.SetRole(Role, RoleChangeReason.Destroyed, RoleSpawnFlags.AssignInventory); + + player.Position = OriginalPosition; } - return false; + return true; } - /// - /// Refresh the CustomInfo of a that has a . - /// - /// - /// - [Obsolete("This method is now obsolete, use the CustomInfo class instead!", true)] - public static void RefreshInfoArea(this Player player, string customInfo) - { - _ = new CustomInfo(player, ProcessCustomInfo(customInfo)); - } + return false; + } - /// - /// Changes in the given string [br] with the UNICODE escape char "\n" - /// - /// - /// - private static string ProcessCustomInfo(string customInfo) => customInfo.Replace("[br]", "\n"); + /// + /// Refresh the CustomInfo of a that has a . + /// + /// + /// + [Obsolete("This method is now obsolete, use the CustomInfo class instead!", true)] + public static void RefreshInfoArea(this Player player, string customInfo) + { + _ = new CustomInfo(player, ProcessCustomInfo(customInfo)); + } - // REF https://gitlab.com/exmod-team/EXILED/-/blob/master/EXILED/Exiled.API/Features/Player.cs?ref_type=heads#L2558 - internal static void SetCategoryLimit(this Player player, ItemCategory category, sbyte limit) - { - int index = InventoryLimits.StandardCategoryLimits.Where(x => x.Value >= 0).OrderBy(x => x.Key).ToList().FindIndex(x => x.Key == category); + /// + /// Changes in the given string [br] with the UNICODE escape char "\n" + /// + /// + /// + private static string ProcessCustomInfo(string customInfo) + { + return customInfo.Replace("[br]", "\n"); + } - if (index is -1) - return; + // REF https://gitlab.com/exmod-team/EXILED/-/blob/master/EXILED/Exiled.API/Features/Player.cs?ref_type=heads#L2558 + internal static void SetCategoryLimit(this Player player, ItemCategory category, sbyte limit) + { + var index = InventoryLimits.StandardCategoryLimits.Where(x => x.Value >= 0).OrderBy(x => x.Key).ToList() + .FindIndex(x => x.Key == category); - MirrorExtensions.SendFakeSyncObject(player, ServerConfigSynchronizer.Singleton.netIdentity, typeof(ServerConfigSynchronizer), writer => + if (index is -1) + return; + + MirrorExtensions.SendFakeSyncObject(player, ServerConfigSynchronizer.Singleton.netIdentity, + typeof(ServerConfigSynchronizer), writer => { writer.WriteULong(1ul); writer.WriteUInt(1); @@ -200,17 +211,19 @@ internal static void SetCategoryLimit(this Player player, ItemCategory category, writer.WriteInt(index); writer.WriteSByte(limit); }); - } + } - // REF https://gitlab.com/exmod-team/EXILED/-/blob/master/EXILED/Exiled.API/Features/Player.cs?ref_type=heads#L2584 - internal static void ResetCategoryLimit(this Player player, ItemCategory category) - { - int index = InventoryLimits.StandardCategoryLimits.Where(x => x.Value >= 0).OrderBy(x => x.Key).ToList().FindIndex(x => x.Key == category); + // REF https://gitlab.com/exmod-team/EXILED/-/blob/master/EXILED/Exiled.API/Features/Player.cs?ref_type=heads#L2584 + internal static void ResetCategoryLimit(this Player player, ItemCategory category) + { + var index = InventoryLimits.StandardCategoryLimits.Where(x => x.Value >= 0).OrderBy(x => x.Key).ToList() + .FindIndex(x => x.Key == category); - if (index is -1) - return; + if (index is -1) + return; - MirrorExtensions.SendFakeSyncObject(player, ServerConfigSynchronizer.Singleton.netIdentity, typeof(ServerConfigSynchronizer), writer => + MirrorExtensions.SendFakeSyncObject(player, ServerConfigSynchronizer.Singleton.netIdentity, + typeof(ServerConfigSynchronizer), writer => { writer.WriteULong(1ul); writer.WriteUInt(1); @@ -218,46 +231,48 @@ internal static void ResetCategoryLimit(this Player player, ItemCategory categor writer.WriteInt(index); writer.WriteSByte(ServerConfigSynchronizer.Singleton.CategoryLimits[index]); }); - } + } - internal static void ResetInventory(this Player player, IEnumerable items) - { - if (items is null) - return; + internal static void ResetInventory(this Player player, IEnumerable items) + { + if (items is null) + return; - player.ClearInventory(); - foreach (ItemType item in items) - player.AddItem(item); - } + player.ClearInventory(); + foreach (var item in items) + player.AddItem(item); + } - // REF https://gitlab.com/exmod-team/EXILED/-/blob/master/EXILED/Exiled.API/Features/Player.cs?ref_type=heads#L2458 - internal static ushort GetAmmoLimit(this Player player, ItemType type, bool ignoreArmor = false) - { - if (ignoreArmor) - return ServerConfigSynchronizer.Singleton.AmmoLimitsSync.FirstOrDefault(x => x.AmmoType == type).Limit; + // REF https://gitlab.com/exmod-team/EXILED/-/blob/master/EXILED/Exiled.API/Features/Player.cs?ref_type=heads#L2458 + internal static ushort GetAmmoLimit(this Player player, ItemType type, bool ignoreArmor = false) + { + if (ignoreArmor) + return ServerConfigSynchronizer.Singleton.AmmoLimitsSync.FirstOrDefault(x => x.AmmoType == type).Limit; - return InventoryLimits.GetAmmoLimit(type, player.ReferenceHub); - } + return InventoryLimits.GetAmmoLimit(type, player.ReferenceHub); + } - // REF https://gitlab.com/exmod-team/EXILED/-/blob/master/EXILED/Exiled.API/Features/Player.cs?ref_type=heads#L2479 - internal static void SetAmmoLimit(this Player player, ItemType type, ushort limit) - { - int index = ServerConfigSynchronizer.Singleton.AmmoLimitsSync.FindIndex(x => x.AmmoType == type); - MirrorExtensions.SendFakeSyncObject(player, ServerConfigSynchronizer.Singleton.netIdentity, typeof(ServerConfigSynchronizer), writer => + // REF https://gitlab.com/exmod-team/EXILED/-/blob/master/EXILED/Exiled.API/Features/Player.cs?ref_type=heads#L2479 + internal static void SetAmmoLimit(this Player player, ItemType type, ushort limit) + { + var index = ServerConfigSynchronizer.Singleton.AmmoLimitsSync.FindIndex(x => x.AmmoType == type); + MirrorExtensions.SendFakeSyncObject(player, ServerConfigSynchronizer.Singleton.netIdentity, + typeof(ServerConfigSynchronizer), writer => { writer.WriteULong(2ul); writer.WriteUInt(1); writer.WriteByte((byte)SyncList.Operation.OP_SET); writer.WriteInt(index); - writer.WriteAmmoLimit(new() { Limit = limit, AmmoType = type, }); + writer.WriteAmmoLimit(new ServerConfigSynchronizer.AmmoLimit { Limit = limit, AmmoType = type }); }); - } + } - // REF https://gitlab.com/exmod-team/EXILED/-/blob/master/EXILED/Exiled.API/Features/Player.cs?ref_type=heads#L2499 - internal static void ResetAmmoLimit(this Player player, ItemType type) - { - int index = ServerConfigSynchronizer.Singleton.AmmoLimitsSync.FindIndex(x => x.AmmoType == type); - MirrorExtensions.SendFakeSyncObject(player, ServerConfigSynchronizer.Singleton.netIdentity, typeof(ServerConfigSynchronizer), writer => + // REF https://gitlab.com/exmod-team/EXILED/-/blob/master/EXILED/Exiled.API/Features/Player.cs?ref_type=heads#L2499 + internal static void ResetAmmoLimit(this Player player, ItemType type) + { + var index = ServerConfigSynchronizer.Singleton.AmmoLimitsSync.FindIndex(x => x.AmmoType == type); + MirrorExtensions.SendFakeSyncObject(player, ServerConfigSynchronizer.Singleton.netIdentity, + typeof(ServerConfigSynchronizer), writer => { writer.WriteULong(2ul); writer.WriteUInt(1); @@ -265,7 +280,5 @@ internal static void ResetAmmoLimit(this Player player, ItemType type) writer.WriteInt(index); writer.WriteAmmoLimit(ServerConfigSynchronizer.Singleton.AmmoLimitsSync[index]); }); - } - } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Extensions/RoleExtension.cs b/UncomplicatedCustomRoles/Extensions/RoleExtension.cs index a41238b..0e15115 100644 --- a/UncomplicatedCustomRoles/Extensions/RoleExtension.cs +++ b/UncomplicatedCustomRoles/Extensions/RoleExtension.cs @@ -1,8 +1,8 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . @@ -13,31 +13,51 @@ using PlayerRoles.FirstPersonControl; using UnityEngine; -namespace UncomplicatedCustomRoles.Extensions +namespace UncomplicatedCustomRoles.Extensions; + +public static class RoleExtension { - public static class RoleExtension + public static bool CompareLife(this Footprint footprint, Footprint other) { - public static bool CompareLife(this Footprint footprint, Footprint other) => footprint.LifeIdentifier == other.LifeIdentifier; - - public static bool CompareLife(this Footprint footprint, ReferenceHub other) => footprint.LifeIdentifier == other.roleManager.CurrentRole.UniqueLifeIdentifier; + return footprint.LifeIdentifier == other.LifeIdentifier; + } - public static Color GetColor(this RoleTypeId roleType) => roleType is RoleTypeId.None ? Color.white : roleType.GetRoleBase().RoleColor; + public static bool CompareLife(this Footprint footprint, ReferenceHub other) + { + return footprint.LifeIdentifier == other.roleManager.CurrentRole.UniqueLifeIdentifier; + } - public static string GetFullName(this RoleTypeId typeId) => typeId.GetRoleBase().RoleName; + public static Color GetColor(this RoleTypeId roleType) + { + return roleType is RoleTypeId.None ? Color.white : roleType.GetRoleBase().RoleColor; + } - public static PlayerRoleBase GetRoleBase(this RoleTypeId roleType) => roleType.TryGetRoleBase(out PlayerRoleBase roleBase) ? roleBase : null; + public static string GetFullName(this RoleTypeId typeId) + { + return typeId.GetRoleBase().RoleName; + } - public static bool TryGetRoleBase(this RoleTypeId roleType, out PlayerRoleBase roleBase) => PlayerRoleLoader.TryGetRoleTemplate(roleType, out roleBase); + public static PlayerRoleBase GetRoleBase(this RoleTypeId roleType) + { + return roleType.TryGetRoleBase(out var roleBase) ? roleBase : null; + } - public static bool TryGetRoleBase(this RoleTypeId roleType, out T roleBase) where T : PlayerRoleBase => PlayerRoleLoader.TryGetRoleTemplate(roleType, out roleBase); + public static bool TryGetRoleBase(this RoleTypeId roleType, out PlayerRoleBase roleBase) + { + return roleType.TryGetRoleTemplate(out roleBase); + } - public static Vector3 GetRandomSpawnLocation(this RoleTypeId roleType) - { - if (roleType.TryGetRoleBase(out FpcStandardRoleBase fpcRole) && fpcRole.SpawnpointHandler != null && fpcRole.SpawnpointHandler.TryGetSpawnpoint(out Vector3 position, out float horizontalRotation)) - return position; + public static bool TryGetRoleBase(this RoleTypeId roleType, out T roleBase) where T : PlayerRoleBase + { + return roleType.TryGetRoleTemplate(out roleBase); + } - return Vector3.zero; - } + public static Vector3 GetRandomSpawnLocation(this RoleTypeId roleType) + { + if (roleType.TryGetRoleBase(out FpcStandardRoleBase fpcRole) && fpcRole.SpawnpointHandler != null && + fpcRole.SpawnpointHandler.TryGetSpawnpoint(out var position, out var horizontalRotation)) + return position; + return Vector3.zero; } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Extensions/StringExtension.cs b/UncomplicatedCustomRoles/Extensions/StringExtension.cs index 0aa515c..c74d1aa 100644 --- a/UncomplicatedCustomRoles/Extensions/StringExtension.cs +++ b/UncomplicatedCustomRoles/Extensions/StringExtension.cs @@ -1,8 +1,8 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . @@ -15,82 +15,82 @@ using System.Text.Json; using UncomplicatedCustomRoles.Manager; -namespace UncomplicatedCustomRoles.Extensions +namespace UncomplicatedCustomRoles.Extensions; + +public static class StringExtension { - public static class StringExtension + public static readonly HashSet _intChars = + [ + '0', + '1', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9' + ]; + + public static string ToInt(this string str, string separator = "") { - public static readonly HashSet _intChars = new() - { - '0', - '1', - '2', - '3', - '4', - '5', - '6', - '7', - '8', - '9' - }; - - public static string ToInt(this string str, string separator = "") - { - List result = new(); + List result = []; - foreach (char ch in str) - if (_intChars.Contains(ch)) - result.Add(ch); + foreach (var ch in str) + if (_intChars.Contains(ch)) + result.Add(ch); - return string.Join(separator, result); - } + return string.Join(separator, result); + } - public static string BulkReplace(this string str, Dictionary replace, string matrix = null) - { - foreach (KeyValuePair kvp in replace.Where(kvp => kvp.Value is not null)) - str = str.Replace(matrix is null ? kvp.Key : matrix.Replace("", kvp.Key), kvp.Value?.ToString()); + public static string BulkReplace(this string str, Dictionary replace, string matrix = null) + { + foreach (var kvp in replace.Where(kvp => kvp.Value is not null)) + str = str.Replace(matrix is null ? kvp.Key : matrix.Replace("", kvp.Key), kvp.Value?.ToString()); - return str; - } + return str; + } - public static string GenerateWithBuffer(this string str, int bufferSize) - { - for (int a = str.Length; a < bufferSize; a++) - str += " "; + public static string GenerateWithBuffer(this string str, int bufferSize) + { + for (var a = str.Length; a < bufferSize; a++) + str += " "; - return str; - } + return str; + } - public static string RemoveBracketsOnEndOfName(this string name) - { - var bracketStart = name.IndexOf('('); + public static string RemoveBracketsOnEndOfName(this string name) + { + var bracketStart = name.IndexOf('('); + + if (bracketStart > 0) + name = name.Remove(bracketStart, name.Length - bracketStart); - if (bracketStart > 0) - name = name.Remove(bracketStart, name.Length - bracketStart); + return name; + } - return name; + public static HttpStatusCode GetStatusCode(this string str, out string message) + { + LogManager.Debug($"Parsing JSON for status code: {str}"); + var doc = JsonDocument.Parse(str); + var root = doc.RootElement; + + message = null; + if (root.TryGetProperty("message", out var messageElement)) + { + message = messageElement.GetString(); + LogManager.Debug($"Extracted message: {message}"); } - - public static HttpStatusCode GetStatusCode(this string str, out string message) + + if (root.TryGetProperty("status", out var status) && + Enum.TryParse(status.ToString(), out HttpStatusCode statusCode)) { - LogManager.Debug($"Parsing JSON for status code: {str}"); - JsonDocument doc = JsonDocument.Parse(str); - JsonElement root = doc.RootElement; - - message = null; - if (root.TryGetProperty("message", out JsonElement messageElement)) - { - message = messageElement.GetString(); - LogManager.Debug($"Extracted message: {message}"); - } - - if (root.TryGetProperty("status", out JsonElement status) && Enum.TryParse(status.ToString(), out HttpStatusCode statusCode)) - { - LogManager.Debug($"Extracted status code: {statusCode}"); - return statusCode; - } - - LogManager.Debug("Status code not found, returning HttpStatusCode.Unused"); - return HttpStatusCode.Unused; + LogManager.Debug($"Extracted status code: {statusCode}"); + return statusCode; } + + LogManager.Debug("Status code not found, returning HttpStatusCode.Unused"); + return HttpStatusCode.Unused; } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Extensions/Vector3Extension.cs b/UncomplicatedCustomRoles/Extensions/Vector3Extension.cs index 1f2ef43..8097bd0 100644 --- a/UncomplicatedCustomRoles/Extensions/Vector3Extension.cs +++ b/UncomplicatedCustomRoles/Extensions/Vector3Extension.cs @@ -1,8 +1,8 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . @@ -11,54 +11,53 @@ using UncomplicatedCustomRoles.API.Struct; using UnityEngine; -namespace UncomplicatedCustomRoles.Extensions +namespace UncomplicatedCustomRoles.Extensions; + +public static class Vector3Extension { - public static class Vector3Extension + /// + /// Adds a X value to the current + /// + /// + /// + /// The modificed + public static Vector3 AddX(this Vector3 vector, float value) { - /// - /// Adds a X value to the current - /// - /// - /// - /// The modificed - public static Vector3 AddX(this Vector3 vector, float value) - { - vector.x += value; - return vector; - } + vector.x += value; + return vector; + } - /// - /// Adds a Y value to the current - /// - /// - /// - /// The modificed - public static Vector3 AddY(this Vector3 vector, float value) - { - vector.y += value; - return vector; - } + /// + /// Adds a Y value to the current + /// + /// + /// + /// The modificed + public static Vector3 AddY(this Vector3 vector, float value) + { + vector.y += value; + return vector; + } - /// - /// Adds a Z value to the current - /// - /// - /// - /// The modificed - public static Vector3 AddZ(this Vector3 vector, float value) - { - vector.z += value; - return vector; - } + /// + /// Adds a Z value to the current + /// + /// + /// + /// The modificed + public static Vector3 AddZ(this Vector3 vector, float value) + { + vector.z += value; + return vector; + } - /// - /// Converts the current to a local - /// - /// - /// - public static Triplet ToTriplet(this Vector3 vector) - { - return new(vector.x, vector.y, vector.z); - } + /// + /// Converts the current to a local + /// + /// + /// + public static Triplet ToTriplet(this Vector3 vector) + { + return new Triplet(vector.x, vector.y, vector.z); } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Integrations/DynamicInvoke.cs b/UncomplicatedCustomRoles/Integrations/DynamicInvoke.cs index ffe6b0b..fa41433 100644 --- a/UncomplicatedCustomRoles/Integrations/DynamicInvoke.cs +++ b/UncomplicatedCustomRoles/Integrations/DynamicInvoke.cs @@ -1,8 +1,8 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . @@ -12,149 +12,154 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using LabApi.Loader; using UncomplicatedCustomRoles.Manager; -namespace UncomplicatedCustomRoles.Integrations +namespace UncomplicatedCustomRoles.Integrations; + +public static class DynamicInvoke { - public static class DynamicInvoke + private static readonly Dictionary _methods = new(); + + private static readonly Dictionary _types = new(); + + private static readonly Dictionary _assemblies = new(); + + /// + /// Get the of a method or property from a specified plugin.

+ /// '_get' and '_set' will load the getter and setter of a property respectively. + ///
+ /// + /// + /// + /// + public static MethodInfo GetMethod(string plugin, string address, bool isLabapi = false, int methodCounter = -1, + string[] requiredParamNames = null) { - private static readonly Dictionary _methods = new(); + if (_methods.TryGetValue(address, out var method)) + return method; - private static readonly Dictionary _types = new(); + if (!_assemblies.TryGetValue(plugin, out var assembly)) + { + assembly = isLabapi ? GetLabAPIAssembly(plugin) : GetExiledAssembly(plugin); + _assemblies.Add(plugin, assembly); + } - private static readonly Dictionary _assemblies = new(); + if (assembly is null) + return null; // Soft dependency not found - chill - /// - /// Get the of a method or property from a specified plugin.

- /// '_get' and '_set' will load the getter and setter of a property respectively. - ///
- /// - /// - /// - /// - public static MethodInfo GetMethod(string plugin, string address, bool isLabapi = false, int methodCounter = -1, string[] requiredParamNames = null) - { - if (_methods.TryGetValue(address, out MethodInfo method)) - return method; + var argument = address.Split('.')?.Last(); + var stringType = address.Replace($".{argument}", string.Empty); - if (!_assemblies.TryGetValue(plugin, out Assembly assembly)) - { - assembly = isLabapi ? GetLabAPIAssembly(plugin) : GetExiledAssembly(plugin); - _assemblies.Add(plugin, assembly); - } + if (!_types.TryGetValue(stringType, out var type)) + { + type = assembly.GetType(stringType); + _types.Add(stringType, type); + } - if (assembly is null) - return null; // Soft dependency not found - chill + if (type is null) + { + LogManager.Warn($"[DynamicInvoke] Failed to locate type {stringType} in assembly {assembly.FullName}!"); + return null; + } - string argument = address.Split('.')?.Last(); - string stringType = address.Replace($".{argument}", string.Empty); + if (argument.Contains('_')) // Handle _get and _set cases - Element IS a property + { + var stringProperty = argument.Split('_')[0]; // Cannot be null + var property = type.GetProperty(stringProperty); + MethodInfo resultMethod; - if (!_types.TryGetValue(stringType, out Type type)) + if (property is null) { - type = assembly.GetType(stringType); - _types.Add(stringType, type); + LogManager.Warn( + $"[DynamicInvoke] Failed to locate property {stringProperty} in type {stringType} in assembly {assembly.FullName}!"); + return null; } - if (type is null) + if (argument.EndsWith("_get")) // Handle getter + resultMethod = property.GetGetMethod(); + else + resultMethod = property.GetSetMethod(); + + if (resultMethod is null) { - LogManager.Warn($"[DynamicInvoke] Failed to locate type {stringType} in assembly {assembly.FullName}!"); + LogManager.Warn( + $"[DynamicInvoke] Failed to locate method _get() or _set() in property {stringProperty} in type {stringType} in assembly {assembly.FullName}!"); return null; } - if (argument.Contains('_')) // Handle _get and _set cases - Element IS a property - { - string stringProperty = argument.Split('_')[0]; // Cannot be null - PropertyInfo property = type.GetProperty(stringProperty); - MethodInfo resultMethod; - - if (property is null) - { - LogManager.Warn($"[DynamicInvoke] Failed to locate property {stringProperty} in type {stringType} in assembly {assembly.FullName}!"); - return null; - } - - if (argument.EndsWith("_get")) // Handle getter - resultMethod = property.GetGetMethod(); - else - resultMethod = property.GetSetMethod(); - - if (resultMethod is null) - { - LogManager.Warn($"[DynamicInvoke] Failed to locate method _get() or _set() in property {stringProperty} in type {stringType} in assembly {assembly.FullName}!"); - return null; - } - - _methods.Add(address, resultMethod); - return resultMethod; - } - else // Normal method + _methods.Add(address, resultMethod); + return resultMethod; + } + else // Normal method + { + var resultMethods = type.GetMethods().Where(m => m.Name == argument); + MethodInfo resultMethod; + + if (methodCounter != -1 || (requiredParamNames is not null && requiredParamNames.Length > 0)) { - IEnumerable resultMethods = type.GetMethods().Where(m => m.Name == argument); - MethodInfo resultMethod; - - if (methodCounter != -1 || (requiredParamNames is not null && requiredParamNames.Length > 0)) - { - IEnumerable filtered = resultMethods; + var filtered = resultMethods; - if (methodCounter != -1) - filtered = filtered.Where(m => m.GetParameters().Length == methodCounter); + if (methodCounter != -1) + filtered = filtered.Where(m => m.GetParameters().Length == methodCounter); - if (requiredParamNames is not null && requiredParamNames.Length > 0) + if (requiredParamNames is not null && requiredParamNames.Length > 0) + filtered = filtered.Where(m => { - filtered = filtered.Where(m => - { - var paramNames = m.GetParameters().Select(p => p.Name).ToArray(); - return requiredParamNames.All(rpn => paramNames.Contains(rpn, StringComparer.OrdinalIgnoreCase)); - }); - } - - resultMethod = filtered.FirstOrDefault(); - } else - { - resultMethod = resultMethods.FirstOrDefault(); - } - - if (resultMethod is null) - { - LogManager.Warn($"[DynamicInvoke] Failed to locate method {argument} in type {stringType} in assembly {assembly.FullName}!"); - return null; - } - - _methods.Add(address, resultMethod); - return resultMethod; - } - } + var paramNames = m.GetParameters().Select(p => p.Name).ToArray(); + return requiredParamNames.All(rpn => + paramNames.Contains(rpn, StringComparer.OrdinalIgnoreCase)); + }); - private static Assembly GetLabAPIAssembly(string pluginName) - { - try + resultMethod = filtered.FirstOrDefault(); + } + else { - KeyValuePair? plugin = LabApi.Loader.PluginLoader.Plugins.FirstOrDefault(p => p.Key.Name == pluginName); - - if (plugin is not null) - return plugin.Value.Value; - - return null; + resultMethod = resultMethods.FirstOrDefault(); } - catch (Exception e) + + if (resultMethod is null) { - LogManager.Error(e.ToString()); + LogManager.Warn( + $"[DynamicInvoke] Failed to locate method {argument} in type {stringType} in assembly {assembly.FullName}!"); return null; } + + _methods.Add(address, resultMethod); + return resultMethod; } - - private static Assembly GetExiledAssembly(string pluginName) + } + + private static Assembly GetLabAPIAssembly(string pluginName) + { + try { - try - { - Assembly assembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(p => p.FullName.Contains(pluginName)); - return assembly; - } - catch (Exception e) - { - LogManager.Error(e.ToString()); - return null; - } + KeyValuePair? plugin = + PluginLoader.Plugins.FirstOrDefault(p => p.Key.Name == pluginName); + + if (plugin is not null) + return plugin.Value.Value; + + return null; + } + catch (Exception e) + { + LogManager.Error(e.ToString()); + return null; + } + } + + private static Assembly GetExiledAssembly(string pluginName) + { + try + { + var assembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(p => p.FullName.Contains(pluginName)); + return assembly; + } + catch (Exception e) + { + LogManager.Error(e.ToString()); + return null; } } } \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Integrations/ECI.cs b/UncomplicatedCustomRoles/Integrations/ECI.cs index 7f2a6e5..2b27d0d 100644 --- a/UncomplicatedCustomRoles/Integrations/ECI.cs +++ b/UncomplicatedCustomRoles/Integrations/ECI.cs @@ -16,7 +16,8 @@ namespace UncomplicatedCustomRoles.Integrations; internal static class ECI { - internal static object PluginInstance { get; } = DynamicInvoke.GetMethod("Exiled.CustomItems", "Exiled.CustomItems.CustomItems.Instance_get")?.Invoke(null, null); + internal static object PluginInstance { get; } = DynamicInvoke + .GetMethod("Exiled.CustomItems", "Exiled.CustomItems.CustomItems.Instance_get")?.Invoke(null, null); public static void GiveCustomItem(uint id, Player player) { @@ -24,11 +25,14 @@ public static void GiveCustomItem(uint id, Player player) { if (PluginInstance is null) { - LogManager.Error($"Failed to run Exiled.CustomItems.GiveCustomItem({id}): Instance of the plugin not found!"); + LogManager.Error( + $"Failed to run Exiled.CustomItems.GiveCustomItem({id}): Instance of the plugin not found!"); return; } - var tryGiveMethod = DynamicInvoke.GetMethod("Exiled.CustomItems", "Exiled.CustomItems.API.Features.CustomItem.TryGive", - false, 3, new[] { "id" }); + + var tryGiveMethod = DynamicInvoke.GetMethod("Exiled.CustomItems", + "Exiled.CustomItems.API.Features.CustomItem.TryGive", + false, 3, ["id"]); if (tryGiveMethod is null) { @@ -38,7 +42,7 @@ public static void GiveCustomItem(uint id, Player player) } var exiledPlayerMethod = DynamicInvoke.GetMethod("Exiled.API", "Exiled.API.Features.Player.Get", false, 1, - new[] { "apiPlayer" }); + ["apiPlayer"]); if (exiledPlayerMethod is null) { LogManager.Error( @@ -46,15 +50,16 @@ public static void GiveCustomItem(uint id, Player player) return; } - var exiledPlayer = exiledPlayerMethod.Invoke(null, new object[] { player }); + var exiledPlayer = exiledPlayerMethod.Invoke(null, [player]); - var result = tryGiveMethod.Invoke(PluginInstance, new[] { exiledPlayer, id, true }); + var result = tryGiveMethod.Invoke(PluginInstance, [exiledPlayer, id, true]); if (result is true) LogManager.Silent($"Gave custom item id {id} to player {player?.Nickname} via CustomItems.TryGive."); else - LogManager.Warn($"Failed to give custom item id {id} to player {player?.Nickname}. Check if the CustomItem exists"); + LogManager.Warn( + $"Failed to give custom item id {id} to player {player?.Nickname}. Check if the CustomItem exists"); } catch (Exception e) { diff --git a/UncomplicatedCustomRoles/Integrations/LabApiExtensions.cs b/UncomplicatedCustomRoles/Integrations/LabApiExtensions.cs index a399f20..7f54460 100644 --- a/UncomplicatedCustomRoles/Integrations/LabApiExtensions.cs +++ b/UncomplicatedCustomRoles/Integrations/LabApiExtensions.cs @@ -13,39 +13,38 @@ using PlayerRoles; using UncomplicatedCustomRoles.Manager; -namespace UncomplicatedCustomRoles.Integrations +namespace UncomplicatedCustomRoles.Integrations; + +internal static class LabApiExtensions { - internal static class LabApiExtensions - { - private const string PluginName = "LabApiExtensions"; + private const string PluginName = "LabApiExtensions"; - public static bool IsAvailable => - DynamicInvoke.GetMethod(PluginName, "LabApiExtensions.Managers.FakeRoleManager.AddFakeRole", false, 2) != null; + public static bool IsAvailable => + DynamicInvoke.GetMethod(PluginName, "LabApiExtensions.Managers.FakeRoleManager.AddFakeRole", false, 2) != null; - public static void AddFakeRole(Player player, RoleTypeId roleType) + public static void AddFakeRole(Player player, RoleTypeId roleType) + { + try + { + DynamicInvoke.GetMethod(PluginName, "LabApiExtensions.Managers.FakeRoleManager.AddFakeRole", false, 2) + ?.Invoke(null, [player, roleType]); + } + catch (Exception e) { - try - { - DynamicInvoke.GetMethod(PluginName, "LabApiExtensions.Managers.FakeRoleManager.AddFakeRole", false, 2) - ?.Invoke(null, new object[] { player, roleType }); - } - catch (Exception e) - { - LogManager.Error($"[LabApiExtensions] Failed to AddFakeRole for {player?.Nickname}: {e}"); - } + LogManager.Error($"[LabApiExtensions] Failed to AddFakeRole for {player?.Nickname}: {e}"); } + } - public static void RemoveFakeRole(Player player) + public static void RemoveFakeRole(Player player) + { + try + { + DynamicInvoke.GetMethod(PluginName, "LabApiExtensions.Managers.FakeRoleManager.RemoveFakeRole", false, 1) + ?.Invoke(null, [player]); + } + catch (Exception e) { - try - { - DynamicInvoke.GetMethod(PluginName, "LabApiExtensions.Managers.FakeRoleManager.RemoveFakeRole", false, 1) - ?.Invoke(null, new object[] { player }); - } - catch (Exception e) - { - LogManager.Error($"[LabApiExtensions] Failed to RemoveFakeRole for {player?.Nickname}: {e}"); - } + LogManager.Error($"[LabApiExtensions] Failed to RemoveFakeRole for {player?.Nickname}: {e}"); } } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Integrations/RespawnTimer.cs b/UncomplicatedCustomRoles/Integrations/RespawnTimer.cs index 7f5df1e..3c28901 100644 --- a/UncomplicatedCustomRoles/Integrations/RespawnTimer.cs +++ b/UncomplicatedCustomRoles/Integrations/RespawnTimer.cs @@ -1,8 +1,8 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . @@ -11,56 +11,53 @@ using LabApi.Features.Wrappers; using PlayerRoles; using PlayerRoles.Spectating; -using UncomplicatedCustomRoles.API.Features; using UncomplicatedCustomRoles.API.Interfaces; using UncomplicatedCustomRoles.Extensions; using UncomplicatedCustomRoles.Manager; -namespace UncomplicatedCustomRoles.Integrations -{ +namespace UncomplicatedCustomRoles.Integrations; #pragma warning disable CS8974 // Conversione del gruppo di metodi in un tipo non delegato - internal static class RespawnTimer +internal static class RespawnTimer +{ + private const string RespawnTimerTextKey = "CUSTOM_ROLE"; + + public static void Enable() { - const string RespawnTimerTextKey = "CUSTOM_ROLE"; + DynamicInvoke.GetMethod("RespawnTimer", "RespawnTimer.API.Placeholder.Register")?.Invoke(null, [ + RespawnTimerTextKey, + GetPublicRoleName + ]); + + LogManager.Debug("Compatibility loader for RespawnTimer: success"); + } - public static void Enable() - { - DynamicInvoke.GetMethod("RespawnTimer", "RespawnTimer.API.Placeholder.Register")?.Invoke(null, new object[] - { - RespawnTimerTextKey, - GetPublicRoleName - }); + public static string GetPublicCustomRoleName(ICustomRole role, Player watcherPlayer) + { + if (!Plugin.Instance.Config.HiddenRolesId.TryGetValue(role.Id, out var information)) + return role.Name; - LogManager.Debug("Compatibility loader for RespawnTimer: success"); - } - public static string GetPublicCustomRoleName(ICustomRole role, Player watcherPlayer) - { - if (!Plugin.Instance.Config.HiddenRolesId.TryGetValue(role.Id, out HiddenRoleInformation information)) - return role.Name; + if ((information.OnlyVisibleOnOverwatch && watcherPlayer.Role == RoleTypeId.Overwatch) || + watcherPlayer.RemoteAdminAccess) + return Plugin.Instance.Config.RespawnTimerContent.Replace("%customrole%", role.Name); + return information.RoleNameWhenHidden; + } - if ((information.OnlyVisibleOnOverwatch && watcherPlayer.Role == RoleTypeId.Overwatch) || watcherPlayer.RemoteAdminAccess) - return Plugin.Instance.Config.RespawnTimerContent.Replace("%customrole%", role.Name); + public static string GetPublicRoleName(Player player) + { + if (player.RoleBase is not SpectatorRole spectator) + return Plugin.Instance.Config.RespawnTimerContentEmpty; - return information.RoleNameWhenHidden; - } + var spectated = Player.Get(spectator.SyncedSpectatedNetId); - public static string GetPublicRoleName(Player player) - { - if (player.RoleBase is not SpectatorRole spectator) - return Plugin.Instance.Config.RespawnTimerContentEmpty; + if (spectated is null) + return string.Empty; - Player spectated = Player.Get(spectator.SyncedSpectatedNetId); + if (spectated.TryGetSummonedInstance(out var summoned)) + return GetPublicCustomRoleName(summoned.Role, player); - if (spectated is null) - return string.Empty; - - if (spectated.TryGetSummonedInstance(out SummonedCustomRole summoned)) - return GetPublicCustomRoleName(summoned.Role, player); - - return Plugin.Instance.Config.RespawnTimerContentEmpty; - } + return Plugin.Instance.Config.RespawnTimerContentEmpty; } } \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Integrations/SLWardobe.cs b/UncomplicatedCustomRoles/Integrations/SLWardobe.cs index 52ce901..0e1af66 100644 --- a/UncomplicatedCustomRoles/Integrations/SLWardobe.cs +++ b/UncomplicatedCustomRoles/Integrations/SLWardobe.cs @@ -9,74 +9,75 @@ */ using System.Linq; -using System.Reflection; using LabApi.Features.Wrappers; using UncomplicatedCustomRoles.Manager; -namespace UncomplicatedCustomRoles.Integrations +namespace UncomplicatedCustomRoles.Integrations; + +internal static class SLWardobe { - internal static class SLWardobe - { - public static object PluginInstance { get; } = DynamicInvoke.GetMethod("SLWardrobe", "SLWardrobe.SLWardrobe.Instance_get")?.Invoke(null, null); + public static object PluginInstance { get; } = + DynamicInvoke.GetMethod("SLWardrobe", "SLWardrobe.SLWardrobe.Instance_get")?.Invoke(null, null); - public static void ApplySuit(Player player, string suitName) + public static void ApplySuit(Player player, string suitName) + { + if (PluginInstance is null) { - if (PluginInstance is null) - { - LogManager.Error("Failed to run SLWardrobe.ApplySuit(): Instance of the plugin not found!"); - return; - } - - MethodInfo method = DynamicInvoke.GetMethod("SLWardrobe", "SLWardrobe.SLWardrobe.ApplySuit"); + LogManager.Error("Failed to run SLWardrobe.ApplySuit(): Instance of the plugin not found!"); + return; + } - if (method is null) - { - LogManager.Error("Failed to run SLWardrobe.ApplySuit(): Method not found!"); - return; - } + var method = DynamicInvoke.GetMethod("SLWardrobe", "SLWardrobe.SLWardrobe.ApplySuit"); - MethodInfo exiledPlayerMethod = DynamicInvoke.GetMethod("Exiled.API", "Exiled.API.Features.Player.Get", false, 1, new[] { "apiPlayer" }); - - if (exiledPlayerMethod is null) - { - LogManager.Error("Failed to run SLWardrobe.ApplySuit(): Exiled Player.Get method not found!"); - return; - } - - var exiledPlayer = exiledPlayerMethod.Invoke(null, new object[] { player }); - LogManager.Silent($"ArgsCounter_ {method.GetParameters().Length} for 2 - expected: {string.Join(", ", method.GetParameters().Select(p => p.ParameterType.FullName))} - found: {exiledPlayer?.GetType().FullName}, {suitName.GetType().FullName}"); - method.Invoke(PluginInstance, new[] { exiledPlayer, suitName }); - + if (method is null) + { + LogManager.Error("Failed to run SLWardrobe.ApplySuit(): Method not found!"); + return; } - public static void RemoveSuit(Player player) + var exiledPlayerMethod = DynamicInvoke.GetMethod("Exiled.API", "Exiled.API.Features.Player.Get", false, 1, + ["apiPlayer"]); + + if (exiledPlayerMethod is null) { - if (PluginInstance is null) - { - LogManager.Error("Failed to run SLWardrobe.RemoveSuit(): Instance of the plugin not found!"); - return; - } + LogManager.Error("Failed to run SLWardrobe.ApplySuit(): Exiled Player.Get method not found!"); + return; + } - MethodInfo method = DynamicInvoke.GetMethod("SLWardrobe", "SLWardrobe.SuitBinder.RemoveSuit"); + var exiledPlayer = exiledPlayerMethod.Invoke(null, [player]); + LogManager.Silent( + $"ArgsCounter_ {method.GetParameters().Length} for 2 - expected: {string.Join(", ", method.GetParameters().Select(p => p.ParameterType.FullName))} - found: {exiledPlayer?.GetType().FullName}, {suitName.GetType().FullName}"); + method.Invoke(PluginInstance, [exiledPlayer, suitName]); + } - if (method is null) - { - LogManager.Error("Failed to run SLWardrobe.RemoveSuit(): Method not found!"); - return; - } + public static void RemoveSuit(Player player) + { + if (PluginInstance is null) + { + LogManager.Error("Failed to run SLWardrobe.RemoveSuit(): Instance of the plugin not found!"); + return; + } - MethodInfo exiledPlayerMethod = DynamicInvoke.GetMethod("Exiled.API", "Exiled.API.Features.Player.Get", false, 1, new[] { "apiPlayer" }); + var method = DynamicInvoke.GetMethod("SLWardrobe", "SLWardrobe.SuitBinder.RemoveSuit"); - if (exiledPlayerMethod is null) - { - LogManager.Error("Failed to run SLWardrobe.RemoveSuit(): Exiled Player.Get method not found!"); - return; - } + if (method is null) + { + LogManager.Error("Failed to run SLWardrobe.RemoveSuit(): Method not found!"); + return; + } + + var exiledPlayerMethod = DynamicInvoke.GetMethod("Exiled.API", "Exiled.API.Features.Player.Get", false, 1, + ["apiPlayer"]); - var exiledPlayer = exiledPlayerMethod.Invoke(null, new object[] { player }); - LogManager.Silent($"ArgsCounter_ {method.GetParameters().Length} for 1 - expected: {string.Join(", ", method.GetParameters().Select(p => p.ParameterType.FullName))} - found: {exiledPlayer?.GetType().FullName}"); - method.Invoke(PluginInstance, new[] { exiledPlayer }); + if (exiledPlayerMethod is null) + { + LogManager.Error("Failed to run SLWardrobe.RemoveSuit(): Exiled Player.Get method not found!"); + return; } - } - } + var exiledPlayer = exiledPlayerMethod.Invoke(null, [player]); + LogManager.Silent( + $"ArgsCounter_ {method.GetParameters().Length} for 1 - expected: {string.Join(", ", method.GetParameters().Select(p => p.ParameterType.FullName))} - found: {exiledPlayer?.GetType().FullName}"); + method.Invoke(PluginInstance, [exiledPlayer]); + } +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Integrations/ScriptedEvents.cs b/UncomplicatedCustomRoles/Integrations/ScriptedEvents.cs index 17168d3..8051c82 100644 --- a/UncomplicatedCustomRoles/Integrations/ScriptedEvents.cs +++ b/UncomplicatedCustomRoles/Integrations/ScriptedEvents.cs @@ -1,8 +1,8 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . @@ -13,175 +13,199 @@ using System.Linq; using LabApi.Features.Wrappers; using UncomplicatedCustomRoles.API.Features; -using UncomplicatedCustomRoles.API.Interfaces; using UncomplicatedCustomRoles.Extensions; using UncomplicatedCustomRoles.Manager; -namespace UncomplicatedCustomRoles.Integrations -{ - internal static class ScriptedEvents - { - private static object _mainPlugin; - - /// - /// Gets the main class of Scripted Events - /// - internal static object MainPlugin => _mainPlugin ??= DynamicInvoke.GetMethod("ScriptedEvents", "ScriptedEvents.MainPlugin.Singleton_get"); +namespace UncomplicatedCustomRoles.Integrations; - /// - /// Gets the current version of ScriptedEvents - /// - internal static Version Version - { - get - { - if (field is not null) - return field; - try - { - field = (Version)(DynamicInvoke.GetMethod("ScriptedEvents", "ScriptedEvents.MainPlugin.Version_get") - ?.Invoke(MainPlugin, []) ?? new Version(0, 0, 0)); - } - catch - { - field = new Version(0, 0, 0); - } - return field; - } - } - - /// - /// Gets whether the version is correct or not - /// - internal static bool IsRightVersion => Version.CompareTo(new Version(3, 1, 6)) > 0; +internal static class ScriptedEvents +{ + private static object _mainPlugin; - /// - /// Gets a list of every CustomAction registered by UCR - /// - internal static List CustomActions { get; } = new(); + private static bool _alreadyLoaded; - private static bool _alreadyLoaded = false; + /// + /// Gets the main class of Scripted Events + /// + internal static object MainPlugin => _mainPlugin ??= + DynamicInvoke.GetMethod("ScriptedEvents", "ScriptedEvents.MainPlugin.Singleton_get"); - /// - /// Register a new CustomAction - /// - /// - /// - public static void RegisterCustomAction(string name, Func, Tuple> action) + /// + /// Gets the current version of ScriptedEvents + /// + internal static Version Version + { + get { + if (field is not null) + return field; try { - DynamicInvoke.GetMethod("ScriptedEvents", "ScriptedEvents.API.Features.ApiHelper.RegisterCustomAction")?.Invoke(null, new object[] { name, action }); - CustomActions.Add(name); - LogManager.Debug($"Successfully registered the ScriptedEvents CustomAction for UCR with the name '{name}'"); + field = (Version)(DynamicInvoke.GetMethod("ScriptedEvents", "ScriptedEvents.MainPlugin.Version_get") + ?.Invoke(MainPlugin, []) ?? new Version(0, 0, 0)); } - catch (Exception e) + catch { - LogManager.Error($"{e.Source} - {e.GetType().FullName} error: {e.Message}"); + field = new Version(0, 0, 0); } + + return field; } + } - /// - /// Register every expected CustomAction native of UCR - /// - public static void RegisterCustomActions() + /// + /// Gets whether the version is correct or not + /// + internal static bool IsRightVersion => Version.CompareTo(new Version(3, 1, 6)) > 0; + + /// + /// Gets a list of every CustomAction registered by UCR + /// + internal static List CustomActions { get; } = []; + + /// + /// Register a new CustomAction + /// + /// + /// + public static void RegisterCustomAction(string name, + Func, Tuple> action) + { + try { - if (_alreadyLoaded) - return; + DynamicInvoke.GetMethod("ScriptedEvents", "ScriptedEvents.API.Features.ApiHelper.RegisterCustomAction") + ?.Invoke(null, + [name, action]); + CustomActions.Add(name); + LogManager.Debug($"Successfully registered the ScriptedEvents CustomAction for UCR with the name '{name}'"); + } + catch (Exception e) + { + LogManager.Error($"{e.Source} - {e.GetType().FullName} error: {e.Message}"); + } + } - if (!IsRightVersion) - { - if (Version == new Version(0, 0, 0)) - return; + /// + /// Register every expected CustomAction native of UCR + /// + public static void RegisterCustomActions() + { + if (_alreadyLoaded) + return; - LogManager.Warn("The ScriptedEvents integration of UCR can't be enabled as your version of ScriptedEvents is OUTDATED!\nRequired: >= 3.1.6 - Found: " + Version); + if (!IsRightVersion) + { + if (Version == new Version(0, 0, 0)) return; - } - // Set custom role - RegisterCustomAction("SET_UCR_ROLE", (Tuple args) => - { - if (args.Item1.Length < 2) - return new(false, "Error: the function SET_UCR_ROLE requires 2 args: SET_UCR_ROLE ", null); + LogManager.Warn( + "The ScriptedEvents integration of UCR can't be enabled as your version of ScriptedEvents is OUTDATED!\nRequired: >= 3.1.6 - Found: " + + Version); + return; + } - Player Player = GetPlayerFromArgs(args); + // Set custom role + RegisterCustomAction("SET_UCR_ROLE", args => + { + if (args.Item1.Length < 2) + return new Tuple(false, + "Error: the function SET_UCR_ROLE requires 2 args: SET_UCR_ROLE ", null); - if (Player is null) - return new(false, $"Error: the given Player ({args.Item1.ElementAt(0)}) does not exists!", null); + var Player = GetPlayerFromArgs(args); - if (!CustomRole.CustomRoles.ContainsKey(int.Parse(args.Item1[1]))) - return new(false, $"Error: the given CustomRole ({int.Parse(args.Item1[1])}) does not exists!", null); + if (Player is null) + return new Tuple(false, + $"Error: the given Player ({args.Item1.ElementAt(0)}) does not exists!", null); - ICustomRole Role = CustomRole.CustomRoles[int.Parse(args.Item1[1])]; + if (!int.TryParse(args.Item1[1], out var roleId)) + return new Tuple(false, + $"Error: the given CustomRole Id ({args.Item1[1]}) is not a number!", null); - Player.SetCustomRoleSync(Role); + if (!CustomRole.CustomRoles.TryGetValue(roleId, out var Role)) + return new Tuple(false, + $"Error: the given CustomRole ({roleId}) does not exists!", null); - return new(true, string.Empty, null); - }); + Player.SetCustomRoleSync(Role); - // Remove custom role - RegisterCustomAction("REMOVE_UCR_ROLE", (Tuple args) => - { - if (args.Item1.Length < 1) - return new(false, "Error: the function REMOVE_UCR_ROLE requires 1 args: REMOVE_UCR_ROLE ", null); + return new Tuple(true, string.Empty, null); + }); - Player Player = GetPlayerFromArgs(args); + // Remove custom role + RegisterCustomAction("REMOVE_UCR_ROLE", args => + { + if (args.Item1.Length < 1) + return new Tuple(false, + "Error: the function REMOVE_UCR_ROLE requires 1 args: REMOVE_UCR_ROLE ", null); - if (Player is null) - return new(false, $"Error: the given Player ({args.Item1.ElementAt(0)}) does not exists!", null); + var Player = GetPlayerFromArgs(args); - if (Player.HasCustomRole()) - Player.TryRemoveCustomRole(); + if (Player is null) + return new Tuple(false, + $"Error: the given Player ({args.Item1.ElementAt(0)}) does not exists!", null); - return new(true, string.Empty, null); - }); + if (Player.HasCustomRole()) + Player.TryRemoveCustomRole(); - RegisterCustomAction("GET_UCR_ROLE", (Tuple args) => - { - if (args.Item1.Length < 1) - return new(false, "Error: the function GET_UCR_ROLE requires 1 args: GET_UCR_ROLE ", null); + return new Tuple(true, string.Empty, null); + }); - Player Player = GetPlayerFromArgs(args); + RegisterCustomAction("GET_UCR_ROLE", args => + { + if (args.Item1.Length < 1) + return new Tuple(false, + "Error: the function GET_UCR_ROLE requires 1 args: GET_UCR_ROLE ", null); - if (Player is null) - return new(false, $"Error: the given Player ({args.Item1.ElementAt(0)}) does not exists!", null); + var Player = GetPlayerFromArgs(args); - if (Player.TryGetSummonedInstance(out SummonedCustomRole role)) - return new(true, string.Empty, new object[] { role.Role.Id.ToString() }); + if (Player is null) + return new Tuple(false, + $"Error: the given Player ({args.Item1.ElementAt(0)}) does not exists!", null); - return new(true, string.Empty, null); - }); + if (Player.TryGetSummonedInstance(out var role)) + return new Tuple(true, string.Empty, [role.Role.Id.ToString()]); - _alreadyLoaded = true; - } + return new Tuple(true, string.Empty, null); + }); - /// - /// Unregister every registered CustomAction registered in - /// - public static void UnregisterCustomActions() - { - foreach (string Name in CustomActions) - DynamicInvoke.GetMethod("ScriptedEvents", "ScriptedEvents.API.Features.ApiHelper.UnregisterCustomAction")?.Invoke(null, new object[] { Name }); + _alreadyLoaded = true; + } - CustomActions.Clear(); - } + /// + /// Unregister every registered CustomAction registered in + /// + public static void UnregisterCustomActions() + { + foreach (var Name in CustomActions) + DynamicInvoke.GetMethod("ScriptedEvents", "ScriptedEvents.API.Features.ApiHelper.UnregisterCustomAction") + ?.Invoke(null, + [Name]); + + CustomActions.Clear(); + } - /// - /// Try to get a Player from the given input - /// - /// - /// - /// - /// - internal static Player GetPlayer(string input, object script) => ((Player[])DynamicInvoke.GetMethod("ScriptedEvents", "ScriptedEvents.API.Features.ApiHelper.GetPlayers")?.Invoke(null, new[] { input, script, 1 })).FirstOrDefault(); - - /// - /// Try to get a Player from the given input, supposing the player is the first argument - /// - /// - /// - /// - /// - internal static Player GetPlayerFromArgs(Tuple args, int index = 0) => GetPlayer(args.Item1.ElementAt(index), args.Item2); + /// + /// Try to get a Player from the given input + /// + /// + /// + /// + /// + internal static Player GetPlayer(string input, object script) + { + return ((Player[])DynamicInvoke.GetMethod("ScriptedEvents", "ScriptedEvents.API.Features.ApiHelper.GetPlayers") + ?.Invoke(null, + [input, script, 1])).FirstOrDefault(); + } + + /// + /// Try to get a Player from the given input, supposing the player is the first argument + /// + /// + /// + /// + /// + internal static Player GetPlayerFromArgs(Tuple args, int index = 0) + { + return GetPlayer(args.Item1.ElementAt(index), args.Item2); } } \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Integrations/UCI.cs b/UncomplicatedCustomRoles/Integrations/UCI.cs index b9667d0..376471f 100644 --- a/UncomplicatedCustomRoles/Integrations/UCI.cs +++ b/UncomplicatedCustomRoles/Integrations/UCI.cs @@ -1,8 +1,8 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . @@ -15,53 +15,61 @@ using LabApi.Loader; using UncomplicatedCustomRoles.Manager; -namespace UncomplicatedCustomRoles.Integrations +namespace UncomplicatedCustomRoles.Integrations; + +internal static class UCI { - internal static class UCI - { - public static Assembly Assembly = PluginLoader.Plugins.FirstOrDefault(p => p.Key.Name is "UncomplicatedCustomItems").Value; + public static Assembly Assembly = + PluginLoader.Plugins.FirstOrDefault(p => p.Key.Name is "UncomplicatedCustomItems").Value; - public static Type SummonedCustomItem = Assembly?.GetType("UncomplicatedCustomItems.API.Features.SummonedCustomItem"); + public static Type SummonedCustomItem = + Assembly?.GetType("UncomplicatedCustomItems.API.Features.SummonedCustomItem"); - public static bool HasCustomItem(uint id, out object customItem) - { - customItem = null; + public static bool HasCustomItem(uint id, out object customItem) + { + customItem = null; - LogManager.Silent($"UCI found, trying check if the item {id} exists..."); + LogManager.Silent($"UCI found, trying check if the item {id} exists..."); - try + try + { + if ((bool?)DynamicInvoke + .GetMethod("UncomplicatedCustomItems", "UncomplicatedCustomItems.API.Utilities.IsCustomItem") + ?.Invoke(null, + [id]) ?? false) { - if ((bool?)DynamicInvoke.GetMethod("UncomplicatedCustomItems", "UncomplicatedCustomItems.API.Utilities.IsCustomItem")?.Invoke(null, new object[] { id }) ?? false) - { - customItem = DynamicInvoke.GetMethod("UncomplicatedCustomItems", "UncomplicatedCustomItems.API.Utilities.GetCustomItem")?.Invoke(null, new object[] { id }); + customItem = DynamicInvoke + .GetMethod("UncomplicatedCustomItems", "UncomplicatedCustomItems.API.Utilities.GetCustomItem") + ?.Invoke(null, + [id]); - return customItem is not null; - } - - return false; - } - catch (Exception e) - { - LogManager.Error(e.ToString()); - return false; + return customItem is not null; } - } - public static void GiveCustomItem(uint id, Player player) + return false; + } + catch (Exception e) { + LogManager.Error(e.ToString()); + return false; + } + } - LogManager.Silent($"UCI found, trying to give the item {id} to {player}"); + public static void GiveCustomItem(uint id, Player player) + { + LogManager.Silent($"UCI found, trying to give the item {id} to {player}"); - try - { - if (HasCustomItem(id, out object customItem) && customItem is not null) - SummonedCustomItem?.GetConstructor(new Type[] { Assembly.GetType("UncomplicatedCustomItems.API.Interfaces.ICustomItem"), typeof(Player) }).Invoke(new object[] { customItem, player }); - } - catch (Exception e) - { - LogManager.Error(e.ToString()); - } + try + { + if (HasCustomItem(id, out var customItem) && customItem is not null) + SummonedCustomItem?.GetConstructor([ + Assembly.GetType("UncomplicatedCustomItems.API.Interfaces.ICustomItem"), typeof(Player) + ]).Invoke([customItem, player]); + } + catch (Exception e) + { + LogManager.Error(e.ToString()); } } } \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Integrations/UCT.cs b/UncomplicatedCustomRoles/Integrations/UCT.cs index 97f8492..638bf0f 100644 --- a/UncomplicatedCustomRoles/Integrations/UCT.cs +++ b/UncomplicatedCustomRoles/Integrations/UCT.cs @@ -12,42 +12,41 @@ using LabApi.Features.Wrappers; using UncomplicatedCustomRoles.Manager; -namespace UncomplicatedCustomRoles.Integrations +namespace UncomplicatedCustomRoles.Integrations; + +internal static class UCT { - internal static class UCT + private const string PluginName = "UncomplicatedCustomTeams"; + + public static bool TryGetCustomTeamId(Player player, out uint teamId) { - private const string PluginName = "UncomplicatedCustomTeams"; - - public static bool TryGetCustomTeamId(Player player, out uint teamId) + teamId = 0; + try { - teamId = 0; - try - { - object summonedTeam = DynamicInvoke.GetMethod(PluginName, + var summonedTeam = DynamicInvoke.GetMethod(PluginName, "UncomplicatedCustomTeams.API.TeamExtensions.GetCustomTeam", true)? - .Invoke(null, new object[] { player }); + .Invoke(null, [player]); - if (summonedTeam is null) - return false; + if (summonedTeam is null) + return false; - object definition = DynamicInvoke.GetMethod(PluginName, + var definition = DynamicInvoke.GetMethod(PluginName, "UncomplicatedCustomTeams.API.Features.Runtime.SummonedTeam.Definition_get", true)? - .Invoke(summonedTeam, null); + .Invoke(summonedTeam, null); - if (definition is null) - return false; + if (definition is null) + return false; - teamId = Convert.ToUInt32(DynamicInvoke.GetMethod(PluginName, + teamId = Convert.ToUInt32(DynamicInvoke.GetMethod(PluginName, "UncomplicatedCustomTeams.API.Features.Definitions.Team.Id_get", true)? - .Invoke(definition, null)); + .Invoke(definition, null)); - return true; - } - catch (Exception e) - { - LogManager.Error(e.ToString()); - return false; - } + return true; + } + catch (Exception e) + { + LogManager.Error(e.ToString()); + return false; } } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Manager/FileConfigs.cs b/UncomplicatedCustomRoles/Manager/FileConfigs.cs index a16802e..bb1a9a2 100644 --- a/UncomplicatedCustomRoles/Manager/FileConfigs.cs +++ b/UncomplicatedCustomRoles/Manager/FileConfigs.cs @@ -1,95 +1,112 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . */ -using LabApi.Loader.Features.Paths; -using LabApi.Loader.Features.Yaml; using System; using System.IO; +using LabApi.Loader.Features.Paths; +using LabApi.Loader.Features.Yaml; using UncomplicatedCustomRoles.API.Features; using UncomplicatedCustomRoles.Compatibility; -namespace UncomplicatedCustomRoles.Manager +namespace UncomplicatedCustomRoles.Manager; + +internal static class FileConfigs { - internal static class FileConfigs + internal static string Dir = Path.Combine(PathManager.Configs.FullName, "UncomplicatedCustomRoles"); + + public static bool Is(string localDir = "") { - internal static string Dir = Path.Combine(PathManager.Configs.FullName, "UncomplicatedCustomRoles"); + return Directory.Exists(Path.Combine(Dir, localDir)); + } - public static bool Is(string localDir = "") => Directory.Exists(Path.Combine(Dir, localDir)); + public static string[] List(string localDir = "") + { + return Directory.GetFiles(Path.Combine(Dir, localDir)); + } - public static string[] List(string localDir = "") => Directory.GetFiles(Path.Combine(Dir, localDir)); + public static void LoadAll(string localDir = "") + { + LoadAction(localDir); - public static void LoadAll(string localDir = "") + foreach (var dir in Directory.GetDirectories(Path.Combine(Dir, localDir))) { - LoadAction(localDir); - - foreach (string dir in Directory.GetDirectories(Path.Combine(Dir, localDir))) + var name = dir.Replace(Dir, string.Empty); + if (name[0] is '/' or '\\') + name = name.Remove(0, 1); + + if (int.TryParse(name, out var num) && num < 990000) + continue; + + if (name is "") + continue; + + LoadAction(name); + } + } + + public static void LoadAction(string localDir = "") + { + foreach (var FileName in List(localDir)) + try { - string name = dir.Replace(Dir, string.Empty); - if (name[0] is '/' or '\\') - name = name.Remove(0, 1); + if (Directory.Exists(FileName)) + continue; - if (int.TryParse(name, out int num) && num < 990000) + if (Path.GetFileName(FileName).StartsWith(".")) continue; - if (name is "") + if (FileName.EndsWith(".dll")) + { + PluginImportManager.Load(FileName); continue; + } - LoadAction(name); + CompatibilityManager.ParseAndLoadCustomRole(FileName); } - } - - public static void LoadAction(string localDir = "") - { - foreach (string FileName in List(localDir)) + catch (Exception ex) { + string[] fileLines; try { - if (Directory.Exists(FileName)) - continue; - - if (FileName.StartsWith(".")) - return; - - if (FileName.EndsWith(".dll")) - { - PluginImportManager.Load(FileName); - return; - } - - CompatibilityManager.ParseAndLoadCustomRole(FileName); + fileLines = File.ReadAllLines(FileName); } - catch (Exception ex) + catch { - // Add the role to the not-loaded list - CustomRole.NotLoadedRoles.Add(new(FileName, File.ReadAllLines(FileName), ex)); - - if (!Plugin.Instance.Config.Debug) - LogManager.Error($"Failed to parse {FileName}:\n{CompatibilityManager.HandleErrorString(ex, true)}\nNotice: This YAML error has been caused by this configuration and it's not a bug of the whole plugin!", "SR0001"); - else - LogManager.Error($"Failed to parse {FileName}. YAML Exception: {ex.Message}.\nStack trace: {ex.StackTrace}\nThis is a YAML error that YOU CAUSED and therefore >>YOU<< NEED TO FIX IT!\nDON'T COME TO US WITH THIS ERROR!", "SR0001"); + fileLines = []; } + + CustomRole.NotLoadedRoles.Add(new ErrorCustomRole(FileName, fileLines, ex)); + + if (!Plugin.Instance.Config.Debug) + LogManager.Error( + $"Failed to parse {FileName}:\n{CompatibilityManager.HandleErrorString(ex, true)}\nNotice: This YAML error has been caused by this configuration and it's not a bug of the whole plugin!", + "SR0001"); + else + LogManager.Error( + $"Failed to parse {FileName}. YAML Exception: {ex.Message}.\nStack trace: {ex.StackTrace}\nThis is a YAML error that YOU CAUSED and therefore >>YOU<< NEED TO FIX IT!\nDON'T COME TO US WITH THIS ERROR!", + "SR0001"); } - } + } - public static void Welcome(string localDir = "") + public static void Welcome(string localDir = "") + { + if (!Is(localDir)) { - if (!Is(localDir)) - { - Directory.CreateDirectory(Path.Combine(Dir, localDir)); - File.WriteAllText(Path.Combine(Dir, localDir, "example-role.yml"), YamlConfigParser.Serializer.Serialize(new CustomRole() + Directory.CreateDirectory(Path.Combine(Dir, localDir)); + File.WriteAllText(Path.Combine(Dir, localDir, "example-role.yml"), YamlConfigParser.Serializer.Serialize( + new CustomRole { Id = CompatibilityManager.GetFirstFreeId() })); - LogManager.Info($"Plugin does not have a role folder, generated one in {Path.Combine(Dir, localDir)}"); - } + LogManager.Info($"Plugin does not have a role folder, generated one in {Path.Combine(Dir, localDir)}"); } } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Manager/ImportManager.cs b/UncomplicatedCustomRoles/Manager/ImportManager.cs index 8e6f08d..393bc6f 100644 --- a/UncomplicatedCustomRoles/Manager/ImportManager.cs +++ b/UncomplicatedCustomRoles/Manager/ImportManager.cs @@ -1,87 +1,94 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . */ -using LabApi.Loader; using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Threading.Tasks; +using LabApi.Loader; using UncomplicatedCustomRoles.API.Attributes; using UncomplicatedCustomRoles.API.Features; using UncomplicatedCustomRoles.API.Interfaces; using UncomplicatedCustomRoles.Extensions; -namespace UncomplicatedCustomRoles.Manager +namespace UncomplicatedCustomRoles.Manager; + +internal class ImportManager { - internal class ImportManager - { - public static readonly List ActivePlugins = new(); + public const float WaitingTime = 5f; + public static readonly List ActivePlugins = []; - public static readonly List AvailableAssemblies = new(); + public static readonly List AvailableAssemblies = []; - public const float WaitingTime = 5f; + private static bool _alreadyLoaded; - private static bool _alreadyLoaded = false; + public static void Init() + { + if (_alreadyLoaded) + return; - public static void Init() - { - if (_alreadyLoaded) - return; + // Call a delayed task + Task.Run(Actor); + } - // Call a delayed task - Task.Run(Actor); - } + public static void Reload() + { + _alreadyLoaded = false; + Actor(); + } - public static void Reload() - { - _alreadyLoaded = false; - Actor(); - } + public static void Unload() + { + ActivePlugins.Clear(); + AvailableAssemblies.Clear(); + AvailableAssemblies.Add(Plugin.Assembly); + } - public static void Unload() - { - ActivePlugins.Clear(); - AvailableAssemblies.Clear(); - AvailableAssemblies.Add(Plugin.Assembly); - } + private static void Actor() + { + LogManager.Debug("Checking for CustomRole registered in other plugins to import..."); - private static void Actor() - { - LogManager.Debug($"Checking for CustomRole registered in other plugins to import..."); + _alreadyLoaded = true; - _alreadyLoaded = true; + if (!AvailableAssemblies.Contains(Plugin.Assembly)) AvailableAssemblies.Add(Plugin.Assembly); - foreach (KeyValuePair plugin in PluginLoader.Plugins.Where(plugin => plugin.Key.Name != Plugin.Instance.Name)) - { + + foreach (var plugin in PluginLoader.Plugins.Where(plugin => plugin.Key.Name != Plugin.Instance.Name)) + { + if (!AvailableAssemblies.Contains(plugin.Value)) AvailableAssemblies.Add(plugin.Value); - LogManager.Silent($"[Import Manager] Passing plugin {plugin.Key.Name}"); - foreach (Type type in plugin.Value.GetTypes()) - try + LogManager.Silent($"[Import Manager] Passing plugin {plugin.Key.Name}"); + foreach (var type in plugin.Value.GetTypes()) + try + { + var attribs = type.GetCustomAttributes(typeof(PluginCustomRole), false); + if (attribs != null && attribs.Length > 0 && typeof(ICustomRole).IsAssignableFrom(type) && + !type.IsAbstract && !type.IsInterface) { - object[] attribs = type.GetCustomAttributes(typeof(PluginCustomRole), false); - if (attribs != null && attribs.Length > 0 && typeof(ICustomRole).IsAssignableFrom(type) && !type.IsAbstract && !type.IsInterface) - { - ActivePlugins.TryAdd(plugin.Key); + ActivePlugins.TryAdd(plugin.Key); - ICustomRole Role = Activator.CreateInstance(type) as ICustomRole; + var Role = Activator.CreateInstance(type) as ICustomRole; - CustomRole.Register(Role); - LogManager.Info($"CustomRole {Role} imported from external plugin {plugin.Key.Name} (v{plugin.Key.Version.ToString(3)})"); - } + CustomRole.Register(Role); + LogManager.Info( + $"CustomRole {Role} imported from external plugin {plugin.Key.Name} (v{plugin.Key.Version.ToString(3)})"); } - catch (Exception e) - { - LogManager.Error($"Error while registering CustomRole from class by Attribute:\nType: {type.FullName} [{plugin.Key.Name}]\nException: {e}"); - } - } + } + catch (Exception e) + { + LogManager.Error( + $"Error while registering CustomRole from class by Attribute:\nType: {type.FullName} [{plugin.Key.Name}]\nException: {e}"); + } } + + YamlFlagsHandler.InvalidateCache(); } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Manager/LogManager.cs b/UncomplicatedCustomRoles/Manager/LogManager.cs index 5fb5870..3d7593b 100644 --- a/UncomplicatedCustomRoles/Manager/LogManager.cs +++ b/UncomplicatedCustomRoles/Manager/LogManager.cs @@ -1,95 +1,100 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . */ -using Discord; using System; using System.Collections.Generic; using System.IO; using System.Net; -using System.Text; +using Discord; using LabApi.Features.Console; using LabApi.Loader.Features.Paths; using LabApi.Loader.Features.Yaml; using NorthwoodLib.Pools; using UncomplicatedCustomRoles.API.Features; -using UncomplicatedCustomRoles.API.Interfaces; -namespace UncomplicatedCustomRoles.Manager +namespace UncomplicatedCustomRoles.Manager; + +internal class LogManager { - internal class LogManager + // We should store the data here + public static readonly HashSet History = []; + private static bool DebugEnabled => Plugin.Instance.Config.Debug; + + public static void Debug(string message) + { + History.Add(new LogEntry(DateTimeOffset.Now.ToUnixTimeMilliseconds(), nameof(LogLevel.Debug), message)); + if (!DebugEnabled) + return; + Logger.Debug(message); + } + + public static void SmInfo(string message, string label = "Info") + { + History.Add(new LogEntry(DateTimeOffset.Now.ToUnixTimeMilliseconds(), label, message)); + Logger.Raw($"[{label}] [{Plugin.Instance.Name}] {message}", ConsoleColor.Gray); + } + + public static void Info(string message, ConsoleColor color = ConsoleColor.Cyan) + { + History.Add(new LogEntry(DateTimeOffset.Now.ToUnixTimeMilliseconds(), nameof(LogLevel.Info), message)); + Logger.Raw($"[INFO] [{Plugin.Instance.Name}] {message}", color); + } + + public static void Warn(string message, string error = "CS0000") + { + History.Add(new LogEntry(DateTimeOffset.Now.ToUnixTimeMilliseconds(), nameof(LogLevel.Warn), message, error)); + Logger.Warn(message); + } + + public static void Error(string message, string error = "CS0000") { - // We should store the data here - public static readonly HashSet History = new(); - private static bool DebugEnabled => Plugin.Instance.Config.Debug; - - public static void Debug(string message) - { - History.Add(new(DateTimeOffset.Now.ToUnixTimeMilliseconds(), nameof(LogLevel.Debug), message)); - if (!DebugEnabled) - return; - Logger.Debug(message); - } - - public static void SmInfo(string message, string label = "Info") - { - History.Add(new(DateTimeOffset.Now.ToUnixTimeMilliseconds(), label, message)); - Logger.Raw($"[{label}] [{Plugin.Instance.Name}] {message}", ConsoleColor.Gray); - } - - public static void Info(string message, ConsoleColor color = ConsoleColor.Cyan) - { - History.Add(new(DateTimeOffset.Now.ToUnixTimeMilliseconds(), nameof(LogLevel.Info), message)); - Logger.Raw($"[INFO] [{Plugin.Instance.Name}] {message}", color); - } - - public static void Warn(string message, string error = "CS0000") - { - History.Add(new(DateTimeOffset.Now.ToUnixTimeMilliseconds(), nameof(LogLevel.Warn), message, error)); - Logger.Warn(message); - } - - public static void Error(string message, string error = "CS0000") - { - History.Add(new(DateTimeOffset.Now.ToUnixTimeMilliseconds(), nameof(LogLevel.Warn), message, error)); - Logger.Error(message); - } - - public static void Silent(string message) => History.Add(new(DateTimeOffset.Now.ToUnixTimeMilliseconds(), "Silent", message)); - - public static void System(string message) => History.Add(new(DateTimeOffset.Now.ToUnixTimeMilliseconds(), "System", message)); - - internal static HttpStatusCode SendReport(out string content, bool online = true) - { - content = null; - - if (History.Count < 1) - return HttpStatusCode.Forbidden; - - StringBuilder builder = StringBuilderPool.Shared.Rent(); - - foreach (LogEntry Element in History) - builder.Append($"{Element}\n"); - - // Now let's add the separator - builder.Append("\n======== BEGIN CUSTOM ROLES ========\n"); - - foreach (ICustomRole Role in CustomRole.CustomRoles.Values) - builder.Append($"{YamlConfigParser.Serializer.Serialize(Role)}\n\n---\n\n"); - - HttpStatusCode response = HttpStatusCode.OK; - if (online) - response = Plugin.HttpManager.ShareLogs(StringBuilderPool.Shared.ToStringReturn(builder), out content); - else - File.WriteAllText(Path.Combine(PathManager.Configs.FullName, $"UCR-Report-{DateTimeOffset.Now.ToUnixTimeSeconds()}.txt"), StringBuilderPool.Shared.ToStringReturn(builder)); - - return response; - } + History.Add(new LogEntry(DateTimeOffset.Now.ToUnixTimeMilliseconds(), nameof(LogLevel.Warn), message, error)); + Logger.Error(message); + } + + public static void Silent(string message) + { + History.Add(new LogEntry(DateTimeOffset.Now.ToUnixTimeMilliseconds(), "Silent", message)); + } + + public static void System(string message) + { + History.Add(new LogEntry(DateTimeOffset.Now.ToUnixTimeMilliseconds(), "System", message)); + } + + internal static HttpStatusCode SendReport(out string content, bool online = true) + { + content = null; + + if (History.Count < 1) + return HttpStatusCode.Forbidden; + + var builder = StringBuilderPool.Shared.Rent(); + + foreach (var Element in History) + builder.Append($"{Element}\n"); + + // Now let's add the separator + builder.Append("\n======== BEGIN CUSTOM ROLES ========\n"); + + foreach (var Role in CustomRole.CustomRoles.Values) + builder.Append($"{YamlConfigParser.Serializer.Serialize(Role)}\n\n---\n\n"); + + var response = HttpStatusCode.OK; + if (online) + response = Plugin.HttpManager.ShareLogs(StringBuilderPool.Shared.ToStringReturn(builder), out content); + else + File.WriteAllText( + Path.Combine(PathManager.Configs.FullName, $"UCR-Report-{DateTimeOffset.Now.ToUnixTimeSeconds()}.txt"), + StringBuilderPool.Shared.ToStringReturn(builder)); + + return response; } } \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Manager/MapSpawnValidator.cs b/UncomplicatedCustomRoles/Manager/MapSpawnValidator.cs new file mode 100644 index 0000000..11f576c --- /dev/null +++ b/UncomplicatedCustomRoles/Manager/MapSpawnValidator.cs @@ -0,0 +1,87 @@ +/* + * This file is a part of the UncomplicatedCustomRoles project. + * + * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) + * + * This file is licensed under the GNU Affero General Public License v3.0. + * You should have received a copy of the AGPL license along with this file. + * If not, see . + */ + +using System; +using System.Collections.Generic; +using System.Linq; +using LabApi.Features.Wrappers; +using MapGeneration; +using UncomplicatedCustomRoles.API.Enums; +using UncomplicatedCustomRoles.API.Features; +using UncomplicatedCustomRoles.Extensions; +using SpawnPoint = UncomplicatedCustomRoles.API.Features.SpawnPoint; + +namespace UncomplicatedCustomRoles.Manager; + +internal static class MapSpawnValidator +{ + internal static void ValidateAll() + { + var rooms = Room.List; + if (rooms is null || rooms.Count == 0) + return; + + HashSet roomNames = new( + rooms.Where(r => r?.GameObject is not null).Select(r => r.GameObject.name.RemoveBracketsOnEndOfName()), + StringComparer.Ordinal); + + HashSet zonesWithRooms = new(rooms.Select(r => r.Zone)); + + var loggedValidRooms = false; + + foreach (var role in CustomRole.CustomRoles.Values) + { + RoleValidator.ValidatePostLoad(role); + + var spawn = role.SpawnSettings; + if (spawn is null) + continue; + + var label = $"{role.Name} ({role.Id})"; + + switch (spawn.Spawn) + { + case SpawnType.RoomsSpawn when spawn.SpawnRooms is not null: + foreach (var roomName in spawn.SpawnRooms.Where(name => !roomNames.Contains(name))) + { + LogManager.Warn( + $"[Role Validator] {label}: spawn room '{roomName}' does not exist on the current map; players there fall back to their original position."); + if (!loggedValidRooms) + { + LogManager.Warn( + $"[Role Validator] Rooms available on the current map: {string.Join(", ", roomNames.OrderBy(n => n))}"); + loggedValidRooms = true; + } + } + + break; + + case SpawnType.SpawnPointSpawn when spawn.SpawnPoints is not null: + foreach (var pointName in spawn.SpawnPoints.Where(name => !SpawnPoint.Exists(name))) + LogManager.Warn( + $"[Role Validator] {label}: spawn point '{pointName}' is not registered. Registered spawn points: {RegisteredSpawnPoints()}."); + break; + + case SpawnType.ZoneSpawn when spawn.SpawnZones is not null: + foreach (var zone in spawn.SpawnZones.Where(z => !zonesWithRooms.Contains(z))) + LogManager.Warn( + $"[Role Validator] {label}: zone '{zone}' has no rooms on the current map, players can't be placed there."); + break; + } + } + } + + private static string RegisteredSpawnPoints() + { + var names = SpawnPoint.List.Concat(SpawnPoint.UnsyncedList).Select(p => p.Name); + var enumerable = names.ToList(); + return enumerable.Any() ? string.Join(", ", enumerable) : "(none)"; + } +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Manager/NET/HttpManager.cs b/UncomplicatedCustomRoles/Manager/NET/HttpManager.cs index b68129a..badc927 100644 --- a/UncomplicatedCustomRoles/Manager/NET/HttpManager.cs +++ b/UncomplicatedCustomRoles/Manager/NET/HttpManager.cs @@ -1,221 +1,233 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . */ -using MEC; -using System.Text.Json; using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; +using System.Text.Json; using System.Threading.Tasks; using LabApi.Events.Arguments.PlayerEvents; using LabApi.Events.Handlers; using LabApi.Features.Wrappers; +using MEC; +using UncomplicatedCustomRoles.API.Features.Messages; using UncomplicatedCustomRoles.API.Struct; using UncomplicatedCustomRoles.Extensions; -using UncomplicatedCustomRoles.API.Features.Messages; -namespace UncomplicatedCustomRoles.Manager.NET -{ +namespace UncomplicatedCustomRoles.Manager.NET; #pragma warning disable IDE1006 - internal class HttpManager +internal class HttpManager +{ + /// + /// Create a new istance of the HttpManager + /// + /// + public HttpManager(string prefix) { - /// - /// Gets the of the presence coroutine. - /// - public CoroutineHandle PresenceCoroutine { get; internal set; } - - /// - /// Gets if the feature can be activated - missing library - /// - public bool IsAllowed { get; internal set; } = true; - - /// - /// Gets the prefix of the plugin for our APIs - /// - public string Prefix { get; } - - /// - /// Gets the public istance - /// - public HttpClient HttpClient { get; } - - /// - /// Gets the UCS APIs endpoint - /// - public string Endpoint { get; } = "https://api.ucserver.it/v3/plugin"; - - /// - /// Gets the CreditTag storage for the plugin, downloaded from our central server - /// - public Dictionary> Credits { get; internal set; } = new(); - - /// - /// Gets the role of the given player (as steamid@64) inside UCR - /// - public List IsJobRole { get; } = new(); - - /// - /// Gets the latest of the plugin, loaded by the UCS cloud - /// - public Version LatestVersion { get - { - if (_latestVersion is null) - LoadLatestVersion(); - return _latestVersion; - } - } - - private Version _latestVersion { get; set; } = null; - - private bool _alreadyManaged { get; set; } = false; + Prefix = prefix; + RegisterEvents(); + HttpClient = new HttpClient(); + Task.Run(LoadCreditTags); + } - /// - /// Create a new istance of the HttpManager - /// - /// - public HttpManager(string prefix) + /// + /// Gets the of the presence coroutine. + /// + public CoroutineHandle PresenceCoroutine { get; internal set; } + + /// + /// Gets if the feature can be activated - missing library + /// + public bool IsAllowed { get; internal set; } = true; + + /// + /// Gets the prefix of the plugin for our APIs + /// + public string Prefix { get; } + + /// + /// Gets the public istance + /// + public HttpClient HttpClient { get; } + + /// + /// Gets the UCS APIs endpoint + /// + public string Endpoint { get; } = "https://api.ucserver.it/v3/plugin"; + + /// + /// Gets the CreditTag storage for the plugin, downloaded from our central server + /// + public Dictionary> Credits { get; internal set; } = new(); + + /// + /// Gets the role of the given player (as steamid@64) inside UCR + /// + public List IsJobRole { get; } = []; + + /// + /// Gets the latest of the plugin, loaded by the UCS cloud + /// + public Version LatestVersion + { + get { - Prefix = prefix; - RegisterEvents(); - HttpClient = new(); - Task.Run(LoadCreditTags); + if (_latestVersion is null) + LoadLatestVersion(); + return _latestVersion; } + } - internal void RegisterEvents() - { - PlayerEvents.Joined += OnVerified; - } + private Version _latestVersion { get; set; } - internal void UnregisterEvents() - { - PlayerEvents.Joined -= OnVerified; - } + internal void RegisterEvents() + { + PlayerEvents.Joined += OnVerified; + } - public void OnVerified(PlayerJoinedEventArgs ev) => ApplyCreditTag(ev.Player); + internal void UnregisterEvents() + { + PlayerEvents.Joined -= OnVerified; + } - public string AddServerOwner(Player player, string discordId) - { - return HttpQuery.Post("https://api.ucserver.it/v3/owners", JsonSerializer.Serialize(new OwnerMessage(player, discordId)), "application/json"); - } + public void OnVerified(PlayerJoinedEventArgs ev) + { + ApplyCreditTag(ev.Player); + } - public void LoadLatestVersion() - { - string Version = HttpQuery.Get($"{Endpoint}/{Prefix}/versions/latest@text/plain"); + public string AddServerOwner(Player player, string discordId) + { + return HttpQuery.Post("https://api.ucserver.it/v3/owners", + JsonSerializer.Serialize(new OwnerMessage(player, discordId)), "application/json"); + } - if (!string.IsNullOrEmpty(Version) && Version.Contains(".")) - _latestVersion = new(Version); - else - _latestVersion = new(); - } + public void LoadLatestVersion() + { + var Version = HttpQuery.Get($"{Endpoint}/{Prefix}/versions/latest@text/plain"); - public void LoadCreditTags() + try { - Credits = new(); - try - { - Dictionary> Data = JsonSerializer.Deserialize>>(HttpQuery.Get($"https://api.ucserver.it/credits.json")); - - if (Data is null) - { - LogManager.Warn("Failed to connect to the UCS Central Server to get the credit tags informations!"); - return; - } - - foreach (KeyValuePair> kvp in Data.Where(kvp => kvp.Value is not null && kvp.Value.ContainsKey("role") && kvp.Value.ContainsKey("color") && kvp.Value.ContainsKey("override") && kvp.Value.ContainsKey("job") )) - { - string role = kvp.Value["role"].GetString(); - string color = kvp.Value["color"].GetString(); - bool overrideStr = kvp.Value["override"].ValueKind switch - { - JsonValueKind.String => bool.Parse(kvp.Value["override"].GetString() ?? string.Empty), - JsonValueKind.True => true, - _ => false - }; - bool isJob = kvp.Value["job"].ValueKind == JsonValueKind.True; - Credits.Add(kvp.Key, new(role, color, overrideStr)); - if (isJob) - IsJobRole.Add(kvp.Key); - } - } - catch (Exception e) - { - LogManager.Error("An error occurred while loading the credit tags from the UCS Central Server!"); - LogManager.Debug($"Failed to act HttpManager::LoadCreditTags() - {e.GetType().FullName}: {e.Message}\n{e.StackTrace}"); - } + if (!string.IsNullOrEmpty(Version) && Version.Contains(".")) + _latestVersion = new Version(Version.Trim()); + else + _latestVersion = new Version(); } - - public Triplet GetCreditTag(Player player) + catch { - if (Credits.TryGetValue(player.UserId, out var tag)) - return tag; - - return new(null, null, false); + LogManager.Debug($"Failed to parse the latest version received from the UCS cloud: '{Version}'"); + _latestVersion = new Version(); } + } - public void ApplyCreditTag(Player player) + public void LoadCreditTags() + { + Credits = new Dictionary>(); + try { - if (!Plugin.Instance.Config.EnableCreditTags) - return; + var Data = JsonSerializer.Deserialize>>( + HttpQuery.Get("https://api.ucserver.it/credits.json")); - if (_alreadyManaged) - return; - - Triplet Tag = GetCreditTag(player); - - if (!string.IsNullOrEmpty(player.ReferenceHub.serverRoles.Network_myText)) + if (Data is null) { - if (Credits.Any(k => k.Value.First == player.ReferenceHub.serverRoles.Network_myText && k.Value.Second == player.ReferenceHub.serverRoles.Network_myColor)) - _alreadyManaged = true; - - if (!Tag.Third) - return; // Do not override + LogManager.Warn("Failed to connect to the UCS Central Server to get the credit tags informations!"); + return; } - if (Tag.First is not null && Tag.Second is not null) + foreach (var kvp in Data.Where(kvp => + kvp.Value is not null && kvp.Value.ContainsKey("role") && kvp.Value.ContainsKey("color") && + kvp.Value.ContainsKey("override") && kvp.Value.ContainsKey("job"))) { - player.ReferenceHub.serverRoles.SetText(Tag.First); - player.ReferenceHub.serverRoles.SetColor(Tag.Second); + var role = kvp.Value["role"].GetString(); + var color = kvp.Value["color"].GetString(); + var overrideStr = kvp.Value["override"].ValueKind switch + { + JsonValueKind.String => bool.Parse(kvp.Value["override"].GetString() ?? string.Empty), + JsonValueKind.True => true, + _ => false + }; + var isJob = kvp.Value["job"].ValueKind == JsonValueKind.True; + Credits.Add(kvp.Key, new Triplet(role, color, overrideStr)); + if (isJob) + IsJobRole.Add(kvp.Key); } } - - public bool IsLatestVersion(out Version latest) + catch (Exception e) { - latest = LatestVersion; - if (latest.CompareTo(Plugin.Instance.Version) > 0) - return false; + LogManager.Error("An error occurred while loading the credit tags from the UCS Central Server!"); + LogManager.Debug( + $"Failed to act HttpManager::LoadCreditTags() - {e.GetType().FullName}: {e.Message}\n{e.StackTrace}"); + } + } - return true; + public Triplet GetCreditTag(Player player) + { + if (Credits.TryGetValue(player.UserId, out var tag)) + return tag; - } + return new Triplet(null, null, false); + } + + public void ApplyCreditTag(Player player) + { + if (!Plugin.Instance.Config.EnableCreditTags) + return; - public bool IsLatestVersion() + var Tag = GetCreditTag(player); + + if (!string.IsNullOrEmpty(player.ReferenceHub.serverRoles.Network_myText)) { - if (LatestVersion.CompareTo(Plugin.Instance.Version) > 0) - return false; + if (Credits.Any(k => + k.Value.First == player.ReferenceHub.serverRoles.Network_myText && + k.Value.Second == player.ReferenceHub.serverRoles.Network_myColor)) + return; - return true; + if (!Tag.Third) + return; // Do not override } - internal HttpStatusCode ShareLogs(string data, out string content) + if (Tag.First is not null && Tag.Second is not null) { - content = HttpQuery.Post($"{Endpoint}/{Prefix}/logs", JsonSerializer.Serialize(new ShareLogMessage(data)), "application/json"); - return content.GetStatusCode(out _); + player.ReferenceHub.serverRoles.SetText(Tag.First); + player.ReferenceHub.serverRoles.SetColor(Tag.Second); } + } + + public bool IsLatestVersion(out Version latest) + { + latest = LatestVersion; + if (latest.CompareTo(Plugin.Instance.Version) > 0) + return false; + + return true; + } + public bool IsLatestVersion() + { + if (LatestVersion.CompareTo(Plugin.Instance.Version) > 0) + return false; + + return true; + } + + internal HttpStatusCode ShareLogs(string data, out string content) + { + content = HttpQuery.Post($"{Endpoint}/{Prefix}/logs", JsonSerializer.Serialize(new ShareLogMessage(data)), + "application/json"); + return content.GetStatusCode(out _); + } #nullable enable - internal string VersionInfo() - { - return HttpQuery.Get($"{Endpoint}/{Prefix}/versions/{Plugin.Instance.Version.ToString(4)}"); - } + internal string VersionInfo() + { + return HttpQuery.Get($"{Endpoint}/{Prefix}/versions/{Plugin.Instance.Version.ToString(4)}"); } } \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Manager/NET/SpawnPointApiCommunicator.cs b/UncomplicatedCustomRoles/Manager/NET/SpawnPointApiCommunicator.cs index 785ddbf..f2d8a41 100644 --- a/UncomplicatedCustomRoles/Manager/NET/SpawnPointApiCommunicator.cs +++ b/UncomplicatedCustomRoles/Manager/NET/SpawnPointApiCommunicator.cs @@ -1,18 +1,18 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . */ -using System.Text.Json; using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text.Json; using System.Threading.Tasks; using LabApi.Features.Wrappers; using LabApi.Loader.Features.Paths; @@ -20,144 +20,178 @@ using UncomplicatedCustomRoles.API.Features; using UncomplicatedCustomRoles.API.Interfaces; -namespace UncomplicatedCustomRoles.Manager.NET +namespace UncomplicatedCustomRoles.Manager.NET; + +internal class SpawnPointApiCommunicator { - internal class SpawnPointApiCommunicator + /// + /// Gets the maximum number of SpawnPoints per server + /// + public const int MaxSpawnPoints = 100; // Don't worry, the check is also in the APIs backend :wink: + + /// + /// Gets the API endpoint + /// + public static string Endpoint => "https://api.ucserver.it/spawnpoints"; + + /// + /// Gets the file path for the local spawnpoints of this server + /// + public static string FilePath => Path.Combine(PathManager.Configs.FullName, $".{Server.Port}-spawnpoints.json"); + + /// + /// Gets whether the spawnpoints should be local or "global" + /// + public static bool Local => Plugin.Instance.Config.LocalSpawnPoints; + + /// + /// Init the Communicator + /// + public static void Init() { - /// - /// Gets the API endpoint - /// - public static string Endpoint => "https://api.ucserver.it/spawnpoints"; - - /// - /// Gets the file path for the local spawnpoints of this server - /// - public static string FilePath => Path.Combine(PathManager.Configs.FullName, $".{Server.Port}-spawnpoints.json"); - - /// - /// Gets whether the spawnpoints should be local or "global" - /// - public static bool Local => Plugin.Instance.Config.LocalSpawnPoints; - - /// - /// Gets the maximum number of SpawnPoints per server - /// - public const int MaxSpawnPoints = 100; // Don't worry, the check is also in the APIs backend :wink: - - /// - /// Init the Communicator - /// - public static void Init() - { - if (!Plugin.HttpManager.IsAllowed) - return; + if (!Plugin.HttpManager.IsAllowed) + return; - if (!File.Exists(FilePath)) - File.WriteAllText(FilePath, JsonSerializer.Serialize(new SpawnPoint[] { })); + if (!File.Exists(FilePath)) + File.WriteAllText(FilePath, JsonSerializer.Serialize(new SpawnPoint[] { })); - Task.Run(LoadFromCloud); - } - - /// - /// Retrive s loaded on UCS cloud - /// - public static void LoadFromCloud() - { - // We need first to reset the list - SpawnPoint.List.Clear(); + Task.Run(LoadFromCloud); + } - if (Local) - { - TryLoadSpawnPoints(File.ReadAllText(FilePath)); - return; - } + /// + /// Retrive s loaded on UCS cloud + /// + public static void LoadFromCloud() + { + // We need first to reset the list + SpawnPoint.List.Clear(); + if (Local) + { try { - TryLoadSpawnPoints(HttpQuery.Get($"{Endpoint}/list?port={Server.Port}")); + TryLoadSpawnPoints(File.ReadAllText(FilePath)); } catch (Exception e) { - LogManager.Warn($"Failed to load SpawnPoints from the UCS cloud: {e.Message}"); + LogManager.Warn($"Failed to load the local SpawnPoints from {FilePath}: {e.Message}"); LogManager.Debug($"SpawnPointApiCommunicator::LoadFromCloud() failed - {e}"); } + + return; } - /// - /// Push the s inside UCS cloud - useful if the list has been updated!

- /// Every server has a limit of 10 ports with 10 spawnpoints for each one - ///
- /// - public static void PushSpawnPoints() + try { - if (Local) - { - File.WriteAllText(FilePath, JsonSerializer.Serialize(SpawnPoint.List.Where(s => s.Sync), new JsonSerializerOptions { WriteIndented = true })); - return; - } - - try - { - string answer = HttpQuery.Post($"{Endpoint}/update?port={Server.Port}", JsonSerializer.Serialize(SpawnPoint.List), "application/json"); - if (answer is "FILE_TOO_BIG_OR_SMALL" or "LIMIT_EXCEEDED" || answer.StartsWith("QTA_TOO_MUCH_")) - LogManager.Warn($"UCS cloud has declined the request: you have reached the maximum number of SpawnPoints: the current limit is: {MaxSpawnPoints} SpawnPoints per Server port and 10 total Server port!\nPlease contact us through our Discord! -- Server says: {answer}"); - else if (answer is "UNKNOWN_LOGIC" or "") - LogManager.Warn($"Failed to update your SpawnPoints on the UCS cloud: it seems to be broken!\nContact us as fast as possible!\nServer says: {answer}"); - else - if (Plugin.Instance.Config.EnableBasicLogs) - LogManager.Info($"Your list of SpawnPoints on UCS cloud has been updated!\nServer says: {answer}"); - else - LogManager.Silent($"Your list of SpawnPoints on UCS cloud has been updated!\nServer says: {answer}"); - } - catch (Exception e) - { - LogManager.Warn($"Failed to push SpawnPoints to the UCS cloud: {e.Message}"); - LogManager.Debug($"SpawnPointApiCommunicator::PushSpawnPoints() failed - {e}"); - } + TryLoadSpawnPoints(HttpQuery.Get($"{Endpoint}/list?port={Server.Port}")); + } + catch (Exception e) + { + LogManager.Warn($"Failed to load SpawnPoints from the UCS cloud: {e.Message}"); + LogManager.Debug($"SpawnPointApiCommunicator::LoadFromCloud() failed - {e}"); } + } - /// - /// Async call the function - /// - /// - public static Task AsyncPushSpawnPoints() => Task.Run(PushSpawnPoints); - - /// - /// Send a migration request to our central servers - /// - /// - /// - public static string PushMigrationRequest(int newPort) => HttpQuery.Get($"{Endpoint}/migrate?port={Server.Port}&to={newPort}"); - - /// - /// Send a downloadUrl request to our central request and share the answer - /// - /// - public static string AskDownloadUrl() => HttpQuery.Get($"{Endpoint}/download?port={Server.Port}"); - - public static string AskIp() => HttpQuery.Get($"{Endpoint}/ip"); - - /// - /// Check every in order to find if any of them are with an invalid (non-existing) SpawnPoint - /// - private static void CustomRoleSpawnCompatibilityChecker() + /// + /// Push the s inside UCS cloud - useful if the list has been updated!

+ /// Every server has a limit of 10 ports with 10 spawnpoints for each one + ///
+ /// + public static void PushSpawnPoints() + { + if (Local) { - foreach (ICustomRole role in CustomRole.CustomRoles.Values.Where(role => role.SpawnSettings is not null && role.SpawnSettings.SpawnPoints is not null && role.SpawnSettings.Spawn is SpawnType.SpawnPointSpawn)) - foreach (string spawnPoint in role.SpawnSettings.SpawnPoints) - if (!SpawnPoint.Exists(spawnPoint)) - LogManager.Warn($"CustomRole {role.Name} {role.Id} has an invalid SpawnPoint '{role.SpawnSettings.SpawnPoints}' inside it's configuration: the selected SpawnPoint does not exists!"); + File.WriteAllText(FilePath, + JsonSerializer.Serialize(SpawnPoint.List.Where(s => s.Sync), + new JsonSerializerOptions { WriteIndented = true })); + return; } - private static void TryLoadSpawnPoints(string json) + try + { + var answer = HttpQuery.Post($"{Endpoint}/update?port={Server.Port}", + JsonSerializer.Serialize(SpawnPoint.List), "application/json"); + if (answer is "FILE_TOO_BIG_OR_SMALL" or "LIMIT_EXCEEDED" || answer.StartsWith("QTA_TOO_MUCH_")) + LogManager.Warn( + $"UCS cloud has declined the request: you have reached the maximum number of SpawnPoints: the current limit is: {MaxSpawnPoints} SpawnPoints per Server port and 10 total Server port!\nPlease contact us through our Discord! -- Server says: {answer}"); + else if (answer is "UNKNOWN_LOGIC" or "") + LogManager.Warn( + $"Failed to update your SpawnPoints on the UCS cloud: it seems to be broken!\nContact us as fast as possible!\nServer says: {answer}"); + else if (Plugin.Instance.Config.EnableBasicLogs) + LogManager.Info($"Your list of SpawnPoints on UCS cloud has been updated!\nServer says: {answer}"); + else + LogManager.Silent($"Your list of SpawnPoints on UCS cloud has been updated!\nServer says: {answer}"); + } + catch (Exception e) { - List List = JsonSerializer.Deserialize>(json); + LogManager.Warn($"Failed to push SpawnPoints to the UCS cloud: {e.Message}"); + LogManager.Debug($"SpawnPointApiCommunicator::PushSpawnPoints() failed - {e}"); + } + } - foreach (SpawnPoint SpawnPoint in List) - SpawnPoint.List.Add(SpawnPoint); + /// + /// Async call the function + /// + /// + public static Task AsyncPushSpawnPoints() + { + return Task.Run(PushSpawnPoints); + } + + /// + /// Send a migration request to our central servers + /// + /// + /// + public static string PushMigrationRequest(int newPort) + { + return HttpQuery.Get($"{Endpoint}/migrate?port={Server.Port}&to={newPort}"); + } - LogManager.Info($"Loaded {List.Count} SpawnPoints from our central servers!"); + /// + /// Send a downloadUrl request to our central request and share the answer + /// + /// + public static string AskDownloadUrl() + { + return HttpQuery.Get($"{Endpoint}/download?port={Server.Port}"); + } - CustomRoleSpawnCompatibilityChecker(); + public static string AskIp() + { + return HttpQuery.Get($"{Endpoint}/ip"); + } + + /// + /// Check every in order to find if any of them are with an invalid (non-existing) + /// SpawnPoint + /// + private static void CustomRoleSpawnCompatibilityChecker() + { + foreach (var role in CustomRole.CustomRoles.Values.Where(role => + role.SpawnSettings is not null && role.SpawnSettings.SpawnPoints is not null && + role.SpawnSettings.Spawn is SpawnType.SpawnPointSpawn)) + foreach (var spawnPoint in role.SpawnSettings.SpawnPoints) + if (!SpawnPoint.Exists(spawnPoint)) + LogManager.Warn( + $"CustomRole {role.Name} ({role.Id}) has an invalid SpawnPoint '{spawnPoint}' inside its configuration: the selected SpawnPoint does not exist!"); + } + + private static void TryLoadSpawnPoints(string json) + { + var List = JsonSerializer.Deserialize>(json); + + if (List is null) + { + LogManager.Warn("Failed to load the SpawnPoints: the received content is not a valid SpawnPoint list!"); + return; } + + foreach (var SpawnPoint in List) + SpawnPoint.List.Add(SpawnPoint); + + LogManager.Info($"Loaded {List.Count} SpawnPoints from our central servers!"); + + CustomRoleSpawnCompatibilityChecker(); } } \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Manager/NET/VersionInfo.cs b/UncomplicatedCustomRoles/Manager/NET/VersionInfo.cs index 2c2d274..7a3e790 100644 --- a/UncomplicatedCustomRoles/Manager/NET/VersionInfo.cs +++ b/UncomplicatedCustomRoles/Manager/NET/VersionInfo.cs @@ -1,8 +1,8 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . @@ -10,63 +10,51 @@ using System.Text.Json.Serialization; -namespace UncomplicatedCustomRoles.Manager.NET -{ +namespace UncomplicatedCustomRoles.Manager.NET; #nullable enable - internal class VersionInfo +internal class VersionInfo +{ + [JsonConstructor] + public VersionInfo(string name, string source, string? sourceLink, string? customName, int preRelease, + int forceDebug, string message, int recall, string? recallTarget, string? recallReason, bool? recallImportant, + string hash) { - [JsonPropertyName("name")] - public string Name { get; } + Name = name; + Source = source; + SourceLink = sourceLink; + CustomName = customName; + PreRelease = preRelease; + ForceDebug = forceDebug; + Message = message; + Recall = recall; + RecallTarget = recallTarget; + RecallReason = recallReason; + RecallImportant = recallImportant; + Hash = hash; + } - [JsonPropertyName("source")] - public string Source { get; } + [JsonPropertyName("name")] public string Name { get; } - [JsonPropertyName("source_link")] - public string? SourceLink { get; } + [JsonPropertyName("source")] public string Source { get; } - [JsonPropertyName("custom_name")] - public string? CustomName { get; } + [JsonPropertyName("source_link")] public string? SourceLink { get; } - [JsonPropertyName("pre_release")] - public int PreRelease { get; } + [JsonPropertyName("custom_name")] public string? CustomName { get; } - [JsonPropertyName("force_debug")] - public int ForceDebug { get; } + [JsonPropertyName("pre_release")] public int PreRelease { get; } - [JsonPropertyName("message")] - public string Message { get; } + [JsonPropertyName("force_debug")] public int ForceDebug { get; } - [JsonPropertyName("recall")] - public int Recall { get; } + [JsonPropertyName("message")] public string Message { get; } - [JsonPropertyName("recall_target")] - public string? RecallTarget { get; } + [JsonPropertyName("recall")] public int Recall { get; } - [JsonPropertyName("recall_reason")] - public string? RecallReason { get; } + [JsonPropertyName("recall_target")] public string? RecallTarget { get; } - [JsonPropertyName("recall_important")] - public bool? RecallImportant { get; } + [JsonPropertyName("recall_reason")] public string? RecallReason { get; } - [JsonPropertyName("hash")] - public string Hash { get; } + [JsonPropertyName("recall_important")] public bool? RecallImportant { get; } - [JsonConstructor] - public VersionInfo(string name, string source, string? sourceLink, string? customName, int preRelease, int forceDebug, string message, int recall, string? recallTarget, string? recallReason, bool? recallImportant, string hash) - { - Name = name; - Source = source; - SourceLink = sourceLink; - CustomName = customName; - PreRelease = preRelease; - ForceDebug = forceDebug; - Message = message; - Recall = recall; - RecallTarget = recallTarget; - RecallReason = recallReason; - RecallImportant = recallImportant; - Hash = hash; - } - } -} + [JsonPropertyName("hash")] public string Hash { get; } +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Manager/PlaceholderManager.cs b/UncomplicatedCustomRoles/Manager/PlaceholderManager.cs index 7cd7770..e2519f5 100644 --- a/UncomplicatedCustomRoles/Manager/PlaceholderManager.cs +++ b/UncomplicatedCustomRoles/Manager/PlaceholderManager.cs @@ -1,43 +1,50 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . */ +using System.Collections.Generic; using LabApi.Features.Wrappers; using Respawning.NamingRules; using UncomplicatedCustomRoles.API.Interfaces; using UncomplicatedCustomRoles.Extensions; using UnityEngine; -namespace UncomplicatedCustomRoles.Manager -{ +namespace UncomplicatedCustomRoles.Manager; #nullable enable - public class PlaceholderManager +public class PlaceholderManager +{ + public static string ApplyPlaceholders(string? origin, Player player, ICustomRole? role) { - public static string ApplyPlaceholders(string? origin, Player player, ICustomRole? role) => (origin ?? string.Empty).BulkReplace(new() + return (origin ?? string.Empty).BulkReplace(new Dictionary + { + { "nick", player.Nickname }, + { "displayname", player.DisplayName }, + { "rand", Random.Range(0, 10) }, + { "dnumber", Random.Range(1000, 10000) }, + { "unitid", player.UnitId }, { - { "nick", player.Nickname }, - { "displayname", player.DisplayName }, - { "rand", Random.Range(0, 10) }, - { "dnumber", Random.Range(1000, 10000) }, - { "unitid", player.UnitId }, - { "unitname", NamingRulesManager.TryGetNamingRule(player.Team, out UnitNamingRule namingRule) ? namingRule.LastGeneratedName : string.Empty }, - { "rolename", player.Role.GetFullName() }, - { "customrolename", role?.Name }, - { "customroleid", role?.Id }, - { "customrolebadge", role?.BadgeName }, - { "health", player.Health }, - { "max_health", player.MaxHealth }, - { "ahp", player.ArtificialHealth }, - { "max_ahp", player.MaxArtificialHealth }, - { "hume", player.HumeShield }, - { "max_hume", player.MaxHumeShield }, - }, "%%"); + "unitname", + NamingRulesManager.TryGetNamingRule(player.Team, out var namingRule) + ? namingRule.LastGeneratedName + : string.Empty + }, + { "rolename", player.Role.GetFullName() }, + { "customrolename", role?.Name }, + { "customroleid", role?.Id }, + { "customrolebadge", role?.BadgeName }, + { "health", player.Health }, + { "max_health", player.MaxHealth }, + { "ahp", player.ArtificialHealth }, + { "max_ahp", player.MaxArtificialHealth }, + { "hume", player.HumeShield }, + { "max_hume", player.MaxHumeShield } + }, "%%"); } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Manager/PluginImportManager.cs b/UncomplicatedCustomRoles/Manager/PluginImportManager.cs index 41aeee4..5c48c29 100644 --- a/UncomplicatedCustomRoles/Manager/PluginImportManager.cs +++ b/UncomplicatedCustomRoles/Manager/PluginImportManager.cs @@ -6,56 +6,58 @@ using UncomplicatedCustomRoles.API.Features; using UncomplicatedCustomRoles.API.Interfaces; -namespace UncomplicatedCustomRoles.Manager +namespace UncomplicatedCustomRoles.Manager; + +internal static class PluginImportManager { - internal static class PluginImportManager - { - private static Dictionary List { get; } = new(); + private static Dictionary List { get; } = new(); - public static void Load(string file) + public static void Load(string file) + { + try { - try - { - Assembly assembly = Assembly.Load(File.ReadAllBytes(file)); + var assembly = Assembly.Load(File.ReadAllBytes(file)); - List.Add(assembly, file); + List.Add(assembly, file); - ImportCustomRoles(assembly); - ImportCustomModules(assembly); - } catch (Exception e) - { - LogManager.Error(e.ToString()); - } + ImportCustomRoles(assembly); + ImportCustomModules(assembly); } - - public static void UnloadAll() + catch (Exception e) { - List.Clear(); + LogManager.Error(e.ToString()); } + } - private static void ImportCustomRoles(Assembly assembly) - { - foreach (Type type in assembly.GetTypes()) - try - { - object[] attribs = type.GetCustomAttributes(typeof(PluginCustomRole), false); - if (attribs != null && attribs.Length > 0 && typeof(ICustomRole).IsAssignableFrom(type) && !type.IsAbstract && !type.IsInterface) - { - ICustomRole Role = Activator.CreateInstance(type) as ICustomRole; - - CustomRole.Register(Role); - LogManager.Info($"CustomRole {Role} imported from external UCR Plugin {List[assembly]}"); - } - } - catch (Exception e) + public static void UnloadAll() + { + List.Clear(); + } + + private static void ImportCustomRoles(Assembly assembly) + { + foreach (var type in assembly.GetTypes()) + try + { + var attribs = type.GetCustomAttributes(typeof(PluginCustomRole), false); + if (attribs != null && attribs.Length > 0 && typeof(ICustomRole).IsAssignableFrom(type) && + !type.IsAbstract && !type.IsInterface) { - LogManager.Error($"Error while registering CustomRole from class by Attribute:\nType: {type.FullName} [{List[assembly]}]\nException: {e}"); + var Role = Activator.CreateInstance(type) as ICustomRole; + + CustomRole.Register(Role); + LogManager.Info($"CustomRole {Role} imported from external UCR Plugin {List[assembly]}"); } - } + } + catch (Exception e) + { + LogManager.Error( + $"Error while registering CustomRole from class by Attribute:\nType: {type.FullName} [{List[assembly]}]\nException: {e}"); + } + } - private static void ImportCustomModules(Assembly assembly) - { - ImportManager.AvailableAssemblies.Add(assembly); // Subscribe for the YamlFlagsHandler check-up - } + private static void ImportCustomModules(Assembly assembly) + { + ImportManager.AvailableAssemblies.Add(assembly); // Subscribe for the YamlFlagsHandler check-up } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Manager/RoleValidator.cs b/UncomplicatedCustomRoles/Manager/RoleValidator.cs new file mode 100644 index 0000000..bcc4abe --- /dev/null +++ b/UncomplicatedCustomRoles/Manager/RoleValidator.cs @@ -0,0 +1,557 @@ +/* + * This file is a part of the UncomplicatedCustomRoles project. + * + * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) + * + * This file is licensed under the GNU Affero General Public License v3.0. + * You should have received a copy of the AGPL license along with this file. + * If not, see . + */ + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using CustomPlayerEffects; +using InventorySystem.Configs; +using MapGeneration; +using PlayerRoles; +using UncomplicatedCustomRoles.API.Enums; +using UncomplicatedCustomRoles.API.Features; +using UncomplicatedCustomRoles.API.Features.CustomModules; +using UncomplicatedCustomRoles.API.Interfaces; +using UncomplicatedCustomRoles.Integrations; + +namespace UncomplicatedCustomRoles.Manager; + +internal static class RoleValidator +{ + private static string[] _effectNames; + + private static readonly string[] KnownPlaceholders = + [ + "nick", "displayname", "rand", "dnumber", "unitid", "unitname", "rolename", + "customrolename", "customroleid", "customrolebadge", + "health", "max_health", "ahp", "max_ahp", "hume", "max_hume" + ]; + + private static readonly Regex PlaceholderRegex = + new("%([A-Za-z_]+)%", RegexOptions.Compiled); + + private static string[] EffectNames => _effectNames ??= ResolveEffectNames(); + + private static string[] ResolveEffectNames() + { + try + { + return typeof(StatusEffectBase).Assembly.GetTypes() + .Where(t => !t.IsAbstract && typeof(StatusEffectBase).IsAssignableFrom(t)) + .Select(t => t.Name) + .OrderBy(n => n) + .ToArray(); + } + catch (Exception e) + { + LogManager.Warn( + $"[Role Validator] Could not enumerate the game's status effects, 'effects' values won't be validated: {e.Message}"); + return []; + } + } + + internal static void Validate(ICustomRole role, out List errors, out List warnings) + { + errors = []; + warnings = []; + + if (role is null) + { + errors.Add("the role is null."); + return; + } + + ValidateIdentity(role, errors, warnings); + ValidateRoles(role, errors, warnings); + ValidateHealthLike(role, errors, warnings); + ValidateEffects(role, warnings); + ValidateInventory(role, warnings); + ValidateMisc(role, warnings); + ValidateSpawnSettings(role, errors, warnings); + ValidateRoleAfterEscape(role, warnings); + } + + internal static bool IsValid(ICustomRole role, out string error) + { + Validate(role, out var errors, out _); + error = errors.Count == 0 ? string.Empty : string.Join("\n", errors.Select(e => " - " + e)); + return errors.Count == 0; + } + + private static void ValidateIdentity(ICustomRole role, List errors, List warnings) + { + if (role.Id < 0) + warnings.Add($"'id' is negative ({role.Id}); ids should be 0 or greater."); + + if (string.IsNullOrWhiteSpace(role.Name)) + warnings.Add("'name' is empty; it is used to identify the role in logs and commands."); + + if (!string.IsNullOrEmpty(role.CustomInfo) + && !NicknameSync.ValidateCustomInfo(role.CustomInfo, out var customInfoError)) + warnings.Add($"'custom_info' will be rejected by the game: {customInfoError}"); + + ValidatePlaceholders("nickname", role.Nickname, warnings); + ValidatePlaceholders("custom_info", role.CustomInfo, warnings); + + if (!string.IsNullOrEmpty(role.Nickname) && role.Nickname.Contains(",") && + role.Nickname.Split(',').Any(string.IsNullOrWhiteSpace)) + warnings.Add( + "'nickname' contains an empty variant between commas; a player could spawn with an empty name."); + + ValidateBadge(role, warnings); + } + + private static void ValidatePlaceholders(string field, string value, List warnings) + { + if (string.IsNullOrEmpty(value)) + return; + + foreach (Match match in PlaceholderRegex.Matches(value)) + { + var name = match.Groups[1].Value; + if (!KnownPlaceholders.Contains(name)) + warnings.Add( + $"'{field}' contains the unknown placeholder '%{name}%'; it will be shown literally. Valid placeholders: {string.Join(", ", KnownPlaceholders.Select(p => $"%{p}%"))}."); + } + } + + private static void ValidateBadge(ICustomRole role, List warnings) + { + var nameUsable = role.BadgeName is not null && role.BadgeName.Length > 1; + var colorUsable = role.BadgeColor is not null && role.BadgeColor.Length > 2; + var nameSet = !string.IsNullOrWhiteSpace(role.BadgeName); + var colorSet = !string.IsNullOrWhiteSpace(role.BadgeColor); + + if ((nameSet || colorSet) && (!nameUsable || !colorUsable)) + { + if (nameSet && !nameUsable) + warnings.Add( + $"'badge_name' ('{role.BadgeName}') is too short (at least 2 characters); the badge will not be applied."); + if (colorSet && !colorUsable) + warnings.Add($"'badge_color' ('{role.BadgeColor}') is too short; the badge will not be applied."); + if (nameUsable && !colorSet) + warnings.Add("'badge_name' is set but 'badge_color' is empty; the badge will not be applied."); + } + + if (nameUsable && colorUsable && role.BadgeColor is not "default" && + !SpawnManager.colorMap.ContainsKey(role.BadgeColor)) + warnings.Add( + $"'badge_color' '{role.BadgeColor}' is not a badge color the game knows, clients may show it as white. Known colors: default, {string.Join(", ", SpawnManager.colorMap.Keys)}."); + } + + private static void ValidateRoles(ICustomRole role, List errors, List warnings) + { + if (role.Role is RoleTypeId.None || role.Role.GetTeam() is Team.Dead) + errors.Add( + $"'role' must be a valid role, got '{role.Role}'. Examples: ClassD, Scientist, NtfSergeant, Scp0492."); + + if (role.RoleAppearance is RoleTypeId.None || role.RoleAppearance.GetTeam() is Team.Dead) + warnings.Add( + $"'role_appearance' '{role.RoleAppearance}' is not a valid alive role; the appearance change will be skipped."); + } + + private static void ValidateHealthLike(ICustomRole role, List errors, List warnings) + { + if (role.Health is not null) + { + if (role.Health.Maximum < 1) + errors.Add($"'health.maximum' must be at least 1, got {role.Health.Maximum}."); + if (role.Health.Amount < 1) + warnings.Add($"'health.amount' is {role.Health.Amount}; the player would spawn (nearly) dead."); + if (role.Health.Maximum >= 1 && role.Health.Amount > role.Health.Maximum) + warnings.Add( + $"'health.amount' ({role.Health.Amount}) is above 'health.maximum' ({role.Health.Maximum}); it will be capped."); + } + + if (role.Ahp is not null) + { + if (role.Ahp.Amount < 0) + warnings.Add($"'ahp.amount' is negative ({role.Ahp.Amount}); it will be treated as 0."); + if (role.Ahp.Limit < 0) + warnings.Add($"'ahp.limit' is negative ({role.Ahp.Limit})."); + if (role.Ahp.Efficacy is < 0f or > 1f) + warnings.Add( + $"'ahp.efficacy' should be between 0 and 1 (fraction of damage absorbed), got {role.Ahp.Efficacy}."); + if (role.Ahp.Decay < 0) + warnings.Add($"'ahp.decay' is negative ({role.Ahp.Decay}); the AHP would grow instead of decaying."); + if (role.Ahp.Sustain < 0) + warnings.Add($"'ahp.sustain' is negative ({role.Ahp.Sustain})."); + } + + if (role.HumeShield is not null) + { + if (role.HumeShield.Amount < 0) + warnings.Add($"'hume_shield.amount' is negative ({role.HumeShield.Amount})."); + if (role.HumeShield.Maximum < 0) + warnings.Add($"'hume_shield.maximum' is negative ({role.HumeShield.Maximum})."); + if (role.HumeShield.Amount > 0 && role.HumeShield.Maximum < role.HumeShield.Amount) + warnings.Add( + $"'hume_shield.maximum' ({role.HumeShield.Maximum}) is below 'hume_shield.amount' ({role.HumeShield.Amount})."); + if (role.HumeShield.RegenerationAmount < 0) + warnings.Add( + $"'hume_shield.regeneration_amount' is negative ({role.HumeShield.RegenerationAmount}); the shield would drain instead of regenerating."); + if (role.HumeShield.RegenerationDelay < 0) + warnings.Add( + $"'hume_shield.regeneration_delay' is negative ({role.HumeShield.RegenerationDelay}); use 0 for no delay."); + if (role.HumeShield.RegenerationSpeed < 0) + warnings.Add( + $"'hume_shield.regeneration_speed' is negative ({role.HumeShield.RegenerationSpeed}); use 0 to regenerate every frame."); + if (role.HumeShield.Maximum > 0 && role.HumeShield.RegenerationAmount == 0 && + role.HumeShield.Amount < role.HumeShield.Maximum) + warnings.Add( + "'hume_shield.regeneration_amount' is 0, so the shield will never regenerate up to its maximum."); + } + + if (role.Stamina is not null) + { + if (role.Stamina.RegenMultiplier < 0) + warnings.Add($"'stamina.regen_multiplier' is negative ({role.Stamina.RegenMultiplier})."); + if (role.Stamina.UsageMultiplier < 0) + warnings.Add($"'stamina.usage_multiplier' is negative ({role.Stamina.UsageMultiplier})."); + } + } + + private static void ValidateEffects(ICustomRole role, List warnings) + { + if (role.Effects is null || EffectNames.Length == 0) + return; + + for (var i = 0; i < role.Effects.Count; i++) + { + var effect = role.Effects[i]; + if (effect is null) + { + warnings.Add($"'effects' entry #{i + 1} is empty."); + continue; + } + + if (string.IsNullOrWhiteSpace(effect.EffectType) || + !EffectNames.Any(n => n.StartsWith(effect.EffectType, StringComparison.InvariantCultureIgnoreCase))) + warnings.Add( + $"'effects' entry #{i + 1} has an unknown effect_type '{effect.EffectType}'; it will be skipped. Valid effects: {string.Join(", ", EffectNames)}."); + + if (effect.Intensity == 0) + warnings.Add( + $"'effects' entry #{i + 1} ('{effect.EffectType}') has intensity 0, which disables the effect; use at least 1."); + } + } + + private static void ValidateInventory(ICustomRole role, List warnings) + { + if (role.Inventory is not null) + { + if (role.Inventory.Count > 8) + warnings.Add( + $"'inventory' lists {role.Inventory.Count} items but a player only has 8 slots; the extra items will not fit."); + + foreach (var item in role.Inventory.Where(IsAmmo)) + warnings.Add( + $"'inventory' contains the ammo '{item}'; put ammo under 'ammo:' instead so the amount is respected."); + + if (role.Inventory.Any(i => i is ItemType.None)) + warnings.Add("'inventory' contains 'None' entries; they give nothing and should be removed."); + } + + if (role.Ammo is not null) + foreach (var ammo in role.Ammo.Keys.Where(k => !IsAmmo(k))) + warnings.Add($"'ammo' contains '{ammo}', which is not an ammo type; only Ammo* values belong here."); + + ValidateInventoryLimits(role, warnings); + } + + private static void ValidateInventoryLimits(ICustomRole role, List warnings) + { + if (role.CustomInventoryLimits is null || role.CustomInventoryLimits.Count == 0) + return; + + try + { + HashSet configurable = new( + InventoryLimits.StandardCategoryLimits + .Where(kvp => kvp.Value >= 0) + .Select(kvp => kvp.Key)); + + foreach (var category in role.CustomInventoryLimits.Keys.Where(c => !configurable.Contains(c))) + warnings.Add( + $"'custom_inventory_limits' contains '{category}', whose limit cannot be overridden; the entry is ignored. Configurable categories: {string.Join(", ", configurable.OrderBy(c => c.ToString()))}."); + } + catch (Exception e) + { + LogManager.Debug($"[Role Validator] Could not read the game's standard category limits: {e.Message}"); + } + } + + private static void ValidateMisc(ICustomRole role, List warnings) + { + if (role.MaxScp330Candies < 0) + warnings.Add($"'max_scp330_candies' is negative ({role.MaxScp330Candies})."); + + if (role.DamageMultiplier < 0) + warnings.Add( + $"'damage_multiplier' is negative ({role.DamageMultiplier}); the role would heal targets instead of damaging them."); + + if (role.SpawnHintDuration < 0) + warnings.Add($"'spawn_hint_duration' is negative ({role.SpawnHintDuration})."); + + if (!string.IsNullOrEmpty(role.SpawnBroadcast) && role.SpawnBroadcastDuration == 0) + warnings.Add( + "'spawn_broadcast' is set but 'spawn_broadcast_duration' is 0; the broadcast would disappear instantly."); + + if (!string.IsNullOrEmpty(role.SpawnHint) && role.SpawnHintDuration == 0) + warnings.Add("'spawn_hint' is set but 'spawn_hint_duration' is 0; the hint would disappear instantly."); + + if (role.Scale.x == 0 && role.Scale.y == 0 && role.Scale.z == 0) + warnings.Add("'scale' is 0 on every axis; the player would be invisible. Use 1 for the normal size."); + else if (role.Scale.x < 0 || role.Scale.y < 0 || role.Scale.z < 0) + warnings.Add( + $"'scale' has a negative axis ({role.Scale.x}, {role.Scale.y}, {role.Scale.z}); the model would be mirrored/broken."); + } + + private static void ValidateSpawnSettings(ICustomRole role, List errors, List warnings) + { + if (role.SpawnSettings is null) + { + errors.Add("'spawn_settings' is missing."); + return; + } + + switch (role.SpawnSettings.Spawn) + { + case SpawnType.ZoneSpawn when role.SpawnSettings.SpawnZones is null || !role.SpawnSettings.SpawnZones.Any(): + errors.Add("'spawn_settings.spawn' is ZoneSpawn but 'spawn_zones' is empty."); + break; + case SpawnType.RoomsSpawn + when role.SpawnSettings.SpawnRooms is null || !role.SpawnSettings.SpawnRooms.Any(): + errors.Add("'spawn_settings.spawn' is RoomsSpawn but 'spawn_rooms' is empty."); + break; + case SpawnType.SpawnPointSpawn + when role.SpawnSettings.SpawnPoints is null || !role.SpawnSettings.SpawnPoints.Any(): + errors.Add("'spawn_settings.spawn' is SpawnPointSpawn but 'spawn_points' is empty."); + break; + case SpawnType.RoleSpawn when role.SpawnSettings.SpawnRoles is null || !role.SpawnSettings.SpawnRoles.Any(): + errors.Add("'spawn_settings.spawn' is RoleSpawn but 'spawn_roles' is empty."); + break; + } + + if (role.SpawnSettings.SpawnChance is < 0 or > 100) + warnings.Add( + $"'spawn_settings.spawn_chance' should be between 0 and 100, got {role.SpawnSettings.SpawnChance}."); + + if (role.SpawnSettings.MinPlayers < 1) + warnings.Add($"'spawn_settings.min_players' should be at least 1, got {role.SpawnSettings.MinPlayers}."); + + if (role.SpawnSettings.MaxPlayers < 1) + warnings.Add( + $"'spawn_settings.max_players' is {role.SpawnSettings.MaxPlayers}; the role will never spawn naturally."); + else if (role.SpawnSettings.MaxPlayers < role.SpawnSettings.MinPlayers) + warnings.Add( + $"'spawn_settings.max_players' ({role.SpawnSettings.MaxPlayers}) is below 'min_players' ({role.SpawnSettings.MinPlayers}); the role will never spawn."); + + if (role.SpawnSettings.CanReplaceRoles is not null) + { + foreach (var replace in role.SpawnSettings.CanReplaceRoles.Where(r => + !SpawnManager.SpawnEvaluatedRoles.Contains(r))) + warnings.Add( + $"'spawn_settings.can_replace_roles' contains '{replace}', which the spawn system never evaluates - it will never trigger a replacement. Usable roles: {string.Join(", ", SpawnManager.SpawnEvaluatedRoles.OrderBy(r => r.ToString()))}."); + + foreach (var duplicate in role.SpawnSettings.CanReplaceRoles.GroupBy(r => r).Where(g => g.Count() > 1)) + warnings.Add( + $"'spawn_settings.can_replace_roles' lists '{duplicate.Key}' {duplicate.Count()} times, which multiplies the spawn chance for that role - remove the duplicates unless that is intended."); + } + + if (role.SpawnSettings.RequiredPermission is IDictionary) + warnings.Add( + "'spawn_settings.required_permission' is a mapping; it must be a single permission string or a list of permission strings."); + + if (role.SpawnSettings.SpawnZones is not null) + foreach (var zone in role.SpawnSettings.SpawnZones.Where(z => z is FacilityZone.None)) + warnings.Add( + $"'spawn_settings.spawn_zones' contains '{zone}', which is not a real facility zone. Valid zones: LightContainment, HeavyContainment, Entrance, Surface."); + + if (role.SpawnSettings.SpawnRoles is not null) + foreach (var spawnRole in role.SpawnSettings.SpawnRoles.Where(r => + r is RoleTypeId.None || r.GetTeam() is Team.Dead)) + warnings.Add( + $"'spawn_settings.spawn_roles' contains '{spawnRole}', which is not a spawnable role to take a spawn position from."); + } + + private static void ValidateRoleAfterEscape(ICustomRole role, List warnings) + { + if (role.RoleAfterEscape is null) + return; + + foreach (var kvp in role.RoleAfterEscape) + { + if (kvp.Key is not "default") + { + var key = kvp.Key.Split(' '); + if (key.Length != 4 || key[0] is not "cuffed" || key[1] is not "by") + warnings.Add( + $"'role_after_escape' key '{kvp.Key}' is invalid; use 'default' or 'cuffed by '."); + else + switch (key[2]) + { + case "InternalTeam" or "IT" when !Enum.TryParse(key[3], out Team _): + warnings.Add( + $"'role_after_escape' key '{kvp.Key}': '{key[3]}' is not a valid team. Valid teams: {string.Join(", ", Enum.GetNames(typeof(Team)))}."); + break; + case "CustomTeam" or "CT" when !uint.TryParse(key[3], out _): + warnings.Add( + $"'role_after_escape' key '{kvp.Key}': '{key[3]}' is not a valid custom team id (a number)."); + break; + case "CustomRole" or "CR" when !int.TryParse(key[3], out _): + warnings.Add( + $"'role_after_escape' key '{kvp.Key}': '{key[3]}' is not a valid custom role id (a number)."); + break; + case not ("InternalTeam" or "IT" or "CustomTeam" or "CT" or "CustomRole" or "CR"): + warnings.Add( + $"'role_after_escape' key '{kvp.Key}': unknown source '{key[2]}'; use InternalTeam (IT), CustomTeam (CT) or CustomRole (CR)."); + break; + } + } + + if (kvp.Value is "Deny" or "deny" or "DENY" || string.IsNullOrEmpty(kvp.Value)) + continue; + + var value = kvp.Value.Split(' '); + if (value.Length != 2) + warnings.Add( + $"'role_after_escape' value '{kvp.Value}' is invalid; use 'Deny', 'InternalRole ' or 'CustomRole '."); + else + switch (value[0]) + { + case "InternalRole" or "IR" when !Enum.TryParse(value[1], out RoleTypeId _): + warnings.Add( + $"'role_after_escape' value '{kvp.Value}': '{value[1]}' is not a valid role. Examples: ClassD, ChaosConscript, NtfPrivate."); + break; + case "CustomRole" or "CR" when !int.TryParse(value[1], out _): + warnings.Add( + $"'role_after_escape' value '{kvp.Value}': '{value[1]}' is not a valid custom role id (a number)."); + break; + case not ("InternalRole" or "IR" or "CustomRole" or "CR"): + warnings.Add( + $"'role_after_escape' value '{kvp.Value}': unknown source '{value[0]}'; use InternalRole (IR) or CustomRole (CR)."); + break; + } + } + } + + internal static void ValidatePostLoad(ICustomRole role) + { + var label = $"{role.Name} ({role.Id})"; + + ValidateCustomFlags(role, label); + ValidateEscapeReferences(role, label); + ValidateCustomItems(role, label); + } + + private static void ValidateCustomItems(ICustomRole role, string label) + { + if (role.CustomItemsInventory is null || role.CustomItemsInventory.Count == 0) + return; + + if (UCI.Assembly is null || ECI.PluginInstance is not null) + return; + + foreach (var id in role.CustomItemsInventory) + try + { + if (!UCI.HasCustomItem(id, out _)) + LogManager.Warn( + $"[Role Validator] {label}: 'custom_items_inventory' references custom item {id}, which is not registered in UncomplicatedCustomItems; nothing will be given for it."); + } + catch (Exception e) + { + LogManager.Debug($"[Role Validator] {label}: could not check custom item {id}: {e.Message}"); + } + } + + private static void ValidateCustomFlags(ICustomRole role, string label) + { + if (role.CustomFlags is null || role.CustomFlags.Count == 0) + return; + + Dictionary> flags; + try + { + flags = YamlFlagsHandler.Decode(role.CustomFlags) ?? new Dictionary>(); + } + catch (Exception e) + { + LogManager.Warn($"[Role Validator] {label}: 'custom_flags' could not be parsed: {e.Message}"); + return; + } + + foreach (var flag in flags) + { + var type = YamlFlagsHandler.Modules.FirstOrDefault(t => + string.Equals(t.Name, flag.Key, StringComparison.OrdinalIgnoreCase)); + + if (type is null) + { + LogManager.Warn( + $"[Role Validator] {label}: unknown custom flag '{flag.Key}'; it will be ignored. Available flags: {string.Join(", ", YamlFlagsHandler.Modules.Select(t => t.Name).OrderBy(n => n))}."); + continue; + } + + try + { + if (Activator.CreateInstance(type) is not CustomModule module) + continue; + + module.Initialize(null, flag.Value); + + var missing = module.RequiredArgs?.Where(arg => !module.Args.ContainsKey(arg)).ToList(); + if (missing is { Count: > 0 }) + { + LogManager.Warn( + $"[Role Validator] {label}: custom flag '{type.Name}' is missing required setting(s): {string.Join(", ", missing)}; it will be skipped on spawn."); + continue; + } + + if (!module.Validate(out var error)) + LogManager.Warn( + $"[Role Validator] {label}: custom flag '{type.Name}' has an invalid setting: {error} It will be skipped on spawn."); + } + catch (Exception e) + { + LogManager.Debug($"[Role Validator] {label}: could not dry-run custom flag '{type.Name}': {e.Message}"); + } + } + } + + private static void ValidateEscapeReferences(ICustomRole role, string label) + { + if (role.RoleAfterEscape is null) + return; + + foreach (var kvp in role.RoleAfterEscape) + { + var key = kvp.Key.Split(' '); + if (key.Length == 4 && key[2] is "CustomRole" or "CR" && int.TryParse(key[3], out var cuffedById) && + !CustomRole.CustomRoles.ContainsKey(cuffedById)) + LogManager.Warn( + $"[Role Validator] {label}: 'role_after_escape' key '{kvp.Key}' references custom role {cuffedById}, which is not registered."); + + var value = kvp.Value?.Split(' ') ?? []; + if (value.Length == 2 && value[0] is "CustomRole" or "CR" && int.TryParse(value[1], out var targetId) && + !CustomRole.CustomRoles.ContainsKey(targetId)) + LogManager.Warn( + $"[Role Validator] {label}: 'role_after_escape' value '{kvp.Value}' references custom role {targetId}, which is not registered - escaping players would go nowhere."); + } + } + + private static bool IsAmmo(ItemType item) + { + return item.ToString().StartsWith("Ammo", StringComparison.Ordinal); + } +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Manager/SpawnManager.cs b/UncomplicatedCustomRoles/Manager/SpawnManager.cs index e669e39..aed9ba9 100644 --- a/UncomplicatedCustomRoles/Manager/SpawnManager.cs +++ b/UncomplicatedCustomRoles/Manager/SpawnManager.cs @@ -1,576 +1,669 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . */ +using System; +using System.Collections; using System.Collections.Generic; using System.Linq; -using UnityEngine; -using System; using Cassie; -using UncomplicatedCustomRoles.Extensions; -using MEC; -using UncomplicatedCustomRoles.API.Features; -using UncomplicatedCustomRoles.API.Struct; -using UncomplicatedCustomRoles.API.Enums; -using UncomplicatedCustomRoles.API.Interfaces; -using PlayerRoles; -using PlayerStatsSystem; -using Subtitles; -using UncomplicatedCustomRoles.API.Features.CustomModules; -using UncomplicatedCustomRoles.Integrations; -using LabApi.Features.Wrappers; -using MapGeneration; using Footprinting; using InventorySystem; using LabApi.Events.Arguments.ServerEvents; +using LabApi.Events.Handlers; using LabApi.Features.Permissions; +using LabApi.Features.Wrappers; +using MapGeneration; +using MEC; +using PlayerRoles; +using PlayerStatsSystem; +using Subtitles; +using UncomplicatedCustomRoles.API.Enums; +using UncomplicatedCustomRoles.API.Features; using UncomplicatedCustomRoles.API.Features.Controllers; +using UncomplicatedCustomRoles.API.Features.CustomModules; +using UncomplicatedCustomRoles.API.Interfaces; +using UncomplicatedCustomRoles.API.Struct; using UncomplicatedCustomRoles.Events; +using UncomplicatedCustomRoles.Extensions; +using UncomplicatedCustomRoles.Integrations; +using UncomplicatedCustomRoles.Patches; +using UnityEngine; +using Random = UnityEngine.Random; // Mormora, la gente mormora // falla tacere praticando l'allegria -namespace UncomplicatedCustomRoles.Manager +namespace UncomplicatedCustomRoles.Manager; + +internal class SpawnManager { - internal class SpawnManager + public static readonly IReadOnlyDictionary colorMap = new Dictionary { - public static readonly IReadOnlyDictionary colorMap = new Dictionary() - { - { "pink", "#FF96DE" }, - { "red", "#C50000" }, - { "brown", "#944710" }, - { "silver", "#A0A0A0" }, - { "light_green", "#32CD32" }, - { "crimson", "#DC143C" }, - { "cyan", "#00B7EB" }, - { "aqua", "#00FFFF" }, - { "deep_pink", "#FF1493" }, - { "tomato", "#FF6448" }, - { "yellow", "#FAFF86" }, - { "magenta", "#FF0090" }, - { "blue_green", "#4DFFB8" }, - { "orange", "#FF9966" }, - { "lime", "#BFFF00" }, - { "green", "#228B22" }, - { "emerald", "#50C878" }, - { "carmine", "#960018" }, - { "nickel", "#727472" }, - { "mint", "#98FB98" }, - { "army_green", "#4B5320" }, - { "pumpkin", "#EE7600" } - }; - - public static void ClearCustomTypes(Player player) - { - if (SummonedCustomRole.TryGet(player, out SummonedCustomRole role)) - role.Destroy(); - } - - public static IEnumerator AsyncPlayerSpawner(Player player, int id, bool doBypassRoleOverwrite = true) - { - yield return Timing.WaitForSeconds(0.1f); - SummonCustomSubclass(player, id, doBypassRoleOverwrite); - } + { "pink", "#FF96DE" }, + { "red", "#C50000" }, + { "brown", "#944710" }, + { "silver", "#A0A0A0" }, + { "light_green", "#32CD32" }, + { "crimson", "#DC143C" }, + { "cyan", "#00B7EB" }, + { "aqua", "#00FFFF" }, + { "deep_pink", "#FF1493" }, + { "tomato", "#FF6448" }, + { "yellow", "#FAFF86" }, + { "magenta", "#FF0090" }, + { "blue_green", "#4DFFB8" }, + { "orange", "#FF9966" }, + { "lime", "#BFFF00" }, + { "green", "#228B22" }, + { "emerald", "#50C878" }, + { "carmine", "#960018" }, + { "nickel", "#727472" }, + { "mint", "#98FB98" }, + { "army_green", "#4B5320" }, + { "pumpkin", "#EE7600" } + }; + + internal static readonly HashSet SpawnEvaluatedRoles = + [ + RoleTypeId.ClassD, + RoleTypeId.Scientist, + RoleTypeId.NtfPrivate, + RoleTypeId.NtfSergeant, + RoleTypeId.NtfCaptain, + RoleTypeId.NtfSpecialist, + RoleTypeId.ChaosConscript, + RoleTypeId.ChaosMarauder, + RoleTypeId.ChaosRepressor, + RoleTypeId.ChaosRifleman, + RoleTypeId.Tutorial, + RoleTypeId.Scp049, + RoleTypeId.Scp0492, + RoleTypeId.Scp079, + RoleTypeId.Scp173, + RoleTypeId.Scp939, + RoleTypeId.Scp096, + RoleTypeId.Scp106, + RoleTypeId.Scp3114, + RoleTypeId.FacilityGuard + ]; + + public static void ClearCustomTypes(Player player) + { + if (SummonedCustomRole.TryGet(player, out var role)) + role.Destroy(); + } + + public static IEnumerator AsyncPlayerSpawner(Player player, int id, bool doBypassRoleOverwrite = true) + { + yield return Timing.WaitForSeconds(0.1f); + SummonCustomSubclass(player, id, doBypassRoleOverwrite); + } - public static void SummonCustomSubclass(Player player, int id, bool doBypassRoleOverwrite = true) + public static void SummonCustomSubclass(Player player, int id, bool doBypassRoleOverwrite = true) + { + try { - try + if (!CustomRole.CustomRoles.TryGetValue(id, out var Role) || Role is null) { + LogManager.Warn( + $"Sorry but the role with the Id {id} is not registered inside UncomplicatedCustomRoles!", + "CR0092"); + return; + } - if (!CustomRole.CustomRoles.TryGetValue(id, out ICustomRole Role) || Role is null) - { - LogManager.Warn($"Sorry but the role with the Id {id} is not registered inside UncomplicatedCustomRoles!", "CR0092"); - return; - } - - if (Role.SpawnSettings is null) - { - LogManager.Warn($"Tried to spawn a custom role without spawn_settings, aborting the SummonCustomSubclass(...) action!\nRole: {Role.Name} ({Role.Id})", "CR0093"); - return; - } + if (Role.SpawnSettings is null) + { + LogManager.Warn( + $"Tried to spawn a custom role without spawn_settings, aborting the SummonCustomSubclass(...) action!\nRole: {Role.Name} ({Role.Id})", + "CR0093"); + return; + } - if (!doBypassRoleOverwrite && !Role.SpawnSettings.CanReplaceRoles.Contains(player.Role)) - { - LogManager.Debug($"Can't spawn the player {player.Nickname} as UCR custom role {Role.Name} because it's role is not in the overwrittable list of custom role!\nStrange because this should be managed correctly by the plugin!"); - return; - } + if (!doBypassRoleOverwrite && !Role.SpawnSettings.CanReplaceRoles.Contains(player.Role)) + { + LogManager.Debug( + $"Can't spawn the player {player.Nickname} as UCR custom role {Role.Name} because it's role is not in the overwrittable list of custom role!\nStrange because this should be managed correctly by the plugin!"); + return; + } - // This will allow us to avoid the loop of another OnSpawning - Spawn.Spawning.Add(player.PlayerId); + // This will allow us to avoid the loop of another OnSpawning + Spawn.Spawning.Add(player.PlayerId); - Vector3 BasicPosition = player.Position; + var BasicPosition = player.Position; - RoleSpawnFlags SpawnFlag = RoleSpawnFlags.None; + var SpawnFlag = RoleSpawnFlags.None; - if (Role.SpawnSettings.Spawn == SpawnType.KeepRoleSpawn) - SpawnFlag = RoleSpawnFlags.UseSpawnpoint; + if (Role.SpawnSettings.Spawn == SpawnType.KeepRoleSpawn) + SpawnFlag = RoleSpawnFlags.UseSpawnpoint; - Patches.UcrSpawnContext.Enter(); - try - { - player.SetRole(Role.Role, RoleChangeReason.Respawn, SpawnFlag); - } - finally - { - Patches.UcrSpawnContext.Exit(); - } + UcrSpawnContext.Enter(); + try + { + player.SetRole(Role.Role, RoleChangeReason.Respawn, SpawnFlag); + } + finally + { + UcrSpawnContext.Exit(); + } - if (Role.SpawnSettings.Spawn == SpawnType.KeepCurrentPositionSpawn) - player.Position = BasicPosition; + if (Role.SpawnSettings.Spawn == SpawnType.KeepCurrentPositionSpawn) + player.Position = BasicPosition; - if (SpawnFlag == RoleSpawnFlags.None) + if (SpawnFlag == RoleSpawnFlags.None) + switch (Role.SpawnSettings.Spawn) { - switch (Role.SpawnSettings.Spawn) - { - case SpawnType.ZoneSpawn: - player.Position = Room.List.Where(room => room.Zone == Role.SpawnSettings.SpawnZones.RandomItem() && room.GameObject.GetComponentInChildren() is null && room.Name is not RoomName.EzEvacShelter).RandomValue().Position.AddY(1.5f); + case SpawnType.ZoneSpawn: + if (Role.SpawnSettings.SpawnZones is null || Role.SpawnSettings.SpawnZones.Count is 0) + { + LogManager.Warn( + $"Failed to spawn player {player.Nickname} ({player.PlayerId}) as CustomRole {Role.Name} ({Role.Id}): spawn is ZoneSpawn but spawn_zones is empty, keeping the previous position..."); + player.Position = BasicPosition; break; - case SpawnType.CompleteRandomSpawn: - player.Position = Room.List.Where(room => room.GameObject.GetComponentInChildren() is null).RandomValue().Position.AddY(1.5f); + } + + var zone = Role.SpawnSettings.SpawnZones.RandomItem(); + var zoneRoom = Room.List.Where(room => + room.Zone == zone && room.GameObject.GetComponentInChildren() is null && + room.Name is not RoomName.EzEvacShelter).RandomValue(); + + if (zoneRoom is null) + { + LogManager.Warn( + $"Failed to spawn player {player.Nickname} ({player.PlayerId}) as CustomRole {Role.Name} ({Role.Id}): no valid room found in zone {zone}, keeping the previous position..."); + player.Position = BasicPosition; break; - case SpawnType.RoomsSpawn: - string roomType = Role.SpawnSettings.SpawnRooms.RandomItem(); - - Room room = Room.List.Where(r => r is not null && r.GameObject.name.RemoveBracketsOnEndOfName() == roomType).RandomValue(); + } - if (room is null) - { - LogManager.Error("Failed to load room with Room Name " + roomType + "!\nMake sure it exists!"); - player.Position = BasicPosition; - break; - } + player.Position = zoneRoom.Position.AddY(1.5f); + break; + case SpawnType.CompleteRandomSpawn: + var randomRoom = Room.List + .Where(room => room.GameObject.GetComponentInChildren() is null).RandomValue(); - player.Position = room.Position.AddY(1.5f); + if (randomRoom is null) + { + player.Position = BasicPosition; + break; + } + player.Position = randomRoom.Position.AddY(1.5f); + break; + case SpawnType.RoomsSpawn: + if (Role.SpawnSettings.SpawnRooms is null || Role.SpawnSettings.SpawnRooms.Count is 0) + { + LogManager.Warn( + $"Failed to spawn player {player.Nickname} ({player.PlayerId}) as CustomRole {Role.Name} ({Role.Id}): spawn is RoomsSpawn but spawn_rooms is empty, keeping the previous position..."); + player.Position = BasicPosition; break; - case SpawnType.SpawnPointSpawn: - if (Role.SpawnSettings.SpawnPoints is not null && Role.SpawnSettings.SpawnPoints.GetType() == typeof(List) && SpawnPoint.TryGet(Role.SpawnSettings.SpawnPoints.RandomItem(), out SpawnPoint spawn)) - spawn.Spawn(player); - else - { - LogManager.Warn($"Failed to spawn player {player.Nickname} ({player.PlayerId}) as CustomRole {Role.Name} ({Role.Id}): selected SpawnPoint '{Role.SpawnSettings.SpawnPoints}' does not exists, set the spawn position to the previous one..."); - player.Position = BasicPosition; - } + } + + var roomType = Role.SpawnSettings.SpawnRooms.RandomItem(); + + var room = Room.List.Where(r => + r is not null && r.GameObject.name.RemoveBracketsOnEndOfName() == roomType).RandomValue(); + + if (room is null) + { + LogManager.Error("Failed to load room with Room Name " + roomType + + "!\nMake sure it exists!"); + player.Position = BasicPosition; break; - case SpawnType.ClassDCell: - player.Position = RoleTypeId.ClassD.GetRandomSpawnLocation(); - break; - case SpawnType.RoleSpawn: - player.Position = Role.SpawnSettings.SpawnRoles.RandomItem().GetRandomSpawnLocation(); + } + + player.Position = room.Position.AddY(1.5f); + + break; + case SpawnType.SpawnPointSpawn: + if (Role.SpawnSettings.SpawnPoints is not null && Role.SpawnSettings.SpawnPoints.Count > 0 && + SpawnPoint.TryGet(Role.SpawnSettings.SpawnPoints.RandomItem(), out var spawn)) + { + spawn.Spawn(player); + } + else + { + LogManager.Warn( + $"Failed to spawn player {player.Nickname} ({player.PlayerId}) as CustomRole {Role.Name} ({Role.Id}): none of the configured SpawnPoints ({(Role.SpawnSettings.SpawnPoints is null || Role.SpawnSettings.SpawnPoints.Count is 0 ? "none set" : string.Join(", ", Role.SpawnSettings.SpawnPoints))}) exists, keeping the previous position..."); + player.Position = BasicPosition; + } + + break; + case SpawnType.ClassDCell: + player.Position = RoleTypeId.ClassD.GetRandomSpawnLocation(); + break; + case SpawnType.RoleSpawn: + if (Role.SpawnSettings.SpawnRoles is null || Role.SpawnSettings.SpawnRoles.Count is 0) + { + LogManager.Warn( + $"Failed to spawn player {player.Nickname} ({player.PlayerId}) as CustomRole {Role.Name} ({Role.Id}): spawn is RoleSpawn but spawn_roles is empty, keeping the previous position..."); + player.Position = BasicPosition; break; - } - ; + } + + var roleSpawn = Role.SpawnSettings.SpawnRoles.RandomItem().GetRandomSpawnLocation(); + player.Position = roleSpawn != Vector3.zero ? roleSpawn : BasicPosition; + break; } - SummonSubclassApplier(player, Role); - } - catch (Exception ex) - { - LogManager.Error(ex.ToString(), "SP0002"); - } + SummonSubclassApplier(player, Role); + } + catch (Exception ex) + { + LogManager.Error(ex.ToString(), "SP0002"); } + } - public static void SummonSubclassApplier(Player Player, ICustomRole Role) + public static void SummonSubclassApplier(Player Player, ICustomRole Role) + { + try { - try - { - if (Role.CustomInventoryLimits is Dictionary inventoryLimits && inventoryLimits.Count > 0) - foreach (KeyValuePair category in inventoryLimits) - Player.SetCategoryLimit(category.Key, category.Value); - - Player.ResetInventory(Role.Inventory); - - LogManager.Silent($"Can we give any CustomItem? {Role.CustomItemsInventory.Count}"); - - if (Role.CustomItemsInventory.Any()) - foreach (uint itemId in Role.CustomItemsInventory) - if (!Player.IsInventoryFull) - try + if (Role.CustomInventoryLimits is Dictionary inventoryLimits && + inventoryLimits.Count > 0) + foreach (var category in inventoryLimits) + Player.SetCategoryLimit(category.Key, category.Value); + + Player.ResetInventory(Role.Inventory); + + LogManager.Silent($"Can we give any CustomItem? {Role.CustomItemsInventory.Count}"); + + if (Role.CustomItemsInventory.Any()) + foreach (var itemId in Role.CustomItemsInventory) + if (!Player.IsInventoryFull) + try + { + if (UCI.HasCustomItem(itemId, out _)) { - if (UCI.HasCustomItem(itemId, out _)) - { - LogManager.Debug($"Going to give UCI CustomItem {itemId} to {Player.PlayerId}"); - UCI.GiveCustomItem(itemId, Player); - } - else - { - LogManager.Debug($"Going to give EXILED CustomItem {itemId} to {Player.PlayerId}"); - ECI.GiveCustomItem(itemId, Player); - } + LogManager.Debug($"Going to give UCI CustomItem {itemId} to {Player.PlayerId}"); + UCI.GiveCustomItem(itemId, Player); } - catch (Exception ex) + else { - LogManager.Error($"Failed to give the custom item {itemId} to player {Player.PlayerId} ({Player.Nickname})! Exception: {ex}"); + LogManager.Debug($"Going to give EXILED CustomItem {itemId} to {Player.PlayerId}"); + ECI.GiveCustomItem(itemId, Player); } + } + catch (Exception ex) + { + LogManager.Error( + $"Failed to give the custom item {itemId} to player {Player.PlayerId} ({Player.Nickname})! Exception: {ex}"); + } + + Player.ClearAmmo(); + + if (Role.Ammo is not null && Role.Ammo.GetType() == typeof(Dictionary) && Role.Ammo.Any()) + foreach (var Ammo in Role.Ammo) + { + if (Ammo.Value > Player.GetAmmoLimit(Ammo.Key)) + Player.SetAmmoLimit(Ammo.Key, Ammo.Value); + Player.AddAmmo(Ammo.Key, Ammo.Value); + } + + // Reset the inventory if we need to add the old one + if (PlayerEventHandler.RespawnInventoryQueue.TryGetValue(Player.PlayerId, out var oldInventory)) + { + Player.ClearInventory(); Player.ClearAmmo(); - if (Role.Ammo is not null && Role.Ammo.GetType() == typeof(Dictionary) && Role.Ammo.Any()) - foreach (KeyValuePair Ammo in Role.Ammo) + foreach (var item in oldInventory.Item1) + if (!oldInventory.Item3) { - if (Ammo.Value > Player.GetAmmoLimit(Ammo.Key)) - Player.SetAmmoLimit(Ammo.Key, Ammo.Value); - - Player.AddAmmo(Ammo.Key, Ammo.Value); + Player.AddItem(item); + } + else + { + var pickup = Pickup.Create(item, Player.Position); + if (pickup is null) + continue; + pickup.Spawn(); } - // Reset the inventory if we need to add the old one - if (PlayerEventHandler.RespawnInventoryQueue.TryGetValue(Player.PlayerId, out Tuple, Dictionary, bool> oldInventory)) - { - Player.ClearInventory(); - Player.ClearAmmo(); - - foreach (ItemType item in oldInventory.Item1) - if (!oldInventory.Item3) - Player.AddItem(item); - else - { - var pickup = Pickup.Create(item, Player.Position); - if (pickup is null) - continue; - pickup.Spawn(); - } + foreach (var item in oldInventory.Item2) + if (!oldInventory.Item3) + { + Player.Inventory.ServerAddAmmo(item.Key, item.Value); + } + else + { + var pickup = Pickup.Create(item.Key, Player.Position); + if (pickup is null) + continue; + pickup.Spawn(); + } - foreach (KeyValuePair item in oldInventory.Item2) - if (!oldInventory.Item3) - Player.Inventory.ServerAddAmmo(item.Key, item.Value); - else - { - var pickup = Pickup.Create(item.Key, Player.Position); - if (pickup is null) - continue; - pickup.Spawn(); - } + PlayerEventHandler.RespawnInventoryQueue.TryRemove(Player.PlayerId, out _); + } - PlayerEventHandler.RespawnInventoryQueue.TryRemove(Player.PlayerId, out _); - } - - PlayerInfoArea InfoArea = Player.ReferenceHub.nicknameSync.Network_playerInfoToShow; + var InfoArea = Player.ReferenceHub.nicknameSync.Network_playerInfoToShow; - // Apply every required stats - Role.Health?.Apply(Player); - Role.Ahp?.Apply(Player); - Role.HumeShield?.Apply(Player); - Role.Stamina?.Apply(Player); + // Apply every required stats + Role.Health?.Apply(Player); + Role.Ahp?.Apply(Player); + Role.HumeShield?.Apply(Player); + Role.Stamina?.Apply(Player); - if (Role.Scale != Vector3.zero && Role.Scale != Vector3.one) - Player.Scale = Role.Scale; + if (Role.Scale != Vector3.zero && Role.Scale != Vector3.one) + Player.Scale = Role.Scale; - List PermanentEffects = new(); - if (Role.Effects != null && Role.Effects.Any()) + List PermanentEffects = []; + if (Role.Effects != null && Role.Effects.Any()) + foreach (IEffect effect in Role.Effects) { - foreach (IEffect effect in Role.Effects) + if (effect.Duration < 0) { - if (effect.Duration < 0) - { - effect.Duration = int.MaxValue; - PermanentEffects.Add(effect); - - Player.ReferenceHub.ForceApplyEffect(effect.EffectType, effect.Intensity, float.MaxValue); - continue; - } - LogManager.Debug($"Enabling effect {effect.EffectType} to {Player.Nickname} for {effect.Duration} (i:{effect.Intensity})"); - Player.ReferenceHub.ForceApplyEffect(effect.EffectType, effect.Intensity, effect.Duration); + effect.Duration = int.MaxValue; + PermanentEffects.Add(effect); + + Player.ReferenceHub.ForceApplyEffect(effect.EffectType, effect.Intensity, float.MaxValue); + continue; } - } - LogManager.Silent($"Found {PermanentEffects.Count} permament effects"); - if (Role.SpawnBroadcast != string.Empty) - { - Player.ClearBroadcasts(); - Player.SendBroadcast(Role.SpawnBroadcast, Role.SpawnBroadcastDuration); + LogManager.Debug( + $"Enabling effect {effect.EffectType} to {Player.Nickname} for {effect.Duration} (i:{effect.Intensity})"); + Player.ReferenceHub.ForceApplyEffect(effect.EffectType, effect.Intensity, effect.Duration); } - if (Role.SpawnHint != string.Empty) - Player.SendHint(Role.SpawnHint, Role.SpawnHintDuration); + LogManager.Silent($"Found {PermanentEffects.Count} permament effects"); - Triplet? Badge = null; - if (Role.BadgeName is not null && Role.BadgeName.Length > 1 && Role.BadgeColor is not null && Role.BadgeColor.Length > 2) - { - Badge = new(Player.ReferenceHub.serverRoles.Network_myText ?? "", Player.ReferenceHub.serverRoles.Network_myColor ?? "", Player.ReferenceHub.serverRoles.HasBadgeHidden); - LogManager.Debug($"Badge detected, putting {Role.BadgeName}@{Role.BadgeColor} to player {Player.PlayerId}"); + if (Role.SpawnBroadcast != string.Empty) + { + Player.ClearBroadcasts(); + Player.SendBroadcast(Role.SpawnBroadcast, Role.SpawnBroadcastDuration); + } - Player.ReferenceHub.serverRoles.SetText(Role.BadgeName.Replace("@hidden", "")); - Player.ReferenceHub.serverRoles.SetColor(Role.BadgeColor); + if (Role.SpawnHint != string.Empty) + Player.SendHint(Role.SpawnHint, Role.SpawnHintDuration); - if (Role.BadgeName.Contains("@hidden")) - if (Player.ReferenceHub.serverRoles.TryHideTag()) - LogManager.Debug("Tag successfully hidden!"); - } + Triplet? Badge = null; + if (Role.BadgeName is not null && Role.BadgeName.Length > 1 && Role.BadgeColor is not null && + Role.BadgeColor.Length > 2) + { + Badge = new Triplet(Player.ReferenceHub.serverRoles.Network_myText ?? "", + Player.ReferenceHub.serverRoles.Network_myColor ?? "", + Player.ReferenceHub.serverRoles.HasBadgeHidden); + LogManager.Debug( + $"Badge detected, putting {Role.BadgeName}@{Role.BadgeColor} to player {Player.PlayerId}"); + + Player.ReferenceHub.serverRoles.SetText(Role.BadgeName.Replace("@hidden", "")); + Player.ReferenceHub.serverRoles.SetColor(Role.BadgeColor); + + if (Role.BadgeName.Contains("@hidden")) + if (Player.ReferenceHub.serverRoles.TryHideTag()) + LogManager.Debug("Tag successfully hidden!"); + } - // Changing nickname if needed - bool ChangedNick = false; - if (Plugin.Instance.Config.AllowNicknameEdit && !string.IsNullOrEmpty(Role.Nickname)) - { - string Nick = PlaceholderManager.ApplyPlaceholders(Role.Nickname, Player, Role); - if (Role.Nickname.Contains(",")) - Player.DisplayName = Nick.Split(',').RandomItem(); - else - Player.DisplayName = Nick; + // Changing nickname if needed + var ChangedNick = false; + if (Plugin.Instance.Config.AllowNicknameEdit && !string.IsNullOrEmpty(Role.Nickname)) + { + var Nick = PlaceholderManager.ApplyPlaceholders(Role.Nickname, Player, Role); + if (Role.Nickname.Contains(",")) + Player.DisplayName = Nick.Split(',').RandomItem(); + else + Player.DisplayName = Nick; - if (Plugin.Instance.Config.OverrideRpNames) - Timing.CallDelayed(3f, () => // Override RPNames shit (sowwy andrew) - { - if (Role.Nickname.Contains(",")) - Player.DisplayName = Nick.Split(',').RandomItem(); - else - Player.DisplayName = Nick; - }); + if (Plugin.Instance.Config.OverrideRpNames) + Timing.CallDelayed(3f, () => // Override RPNames shit (sowwy andrew) + { + if (Role.Nickname.Contains(",")) + Player.DisplayName = Nick.Split(',').RandomItem(); + else + Player.DisplayName = Nick; + }); - ChangedNick = true; - } + ChangedNick = true; + } - // Roll out custom info - CustomInfo customInfo = new(Player, Role); + // Roll out custom info + CustomInfo customInfo = new(Player, Role); - LogManager.Debug($"{Player} successfully spawned as {Role.Name} ({Role.Id})!"); + LogManager.Debug($"{Player} successfully spawned as {Role.Name} ({Role.Id})!"); - SummonedCustomRole roleInstance = new(Player, Role, Badge, PermanentEffects, InfoArea, customInfo, ChangedNick); + SummonedCustomRole roleInstance = + new(Player, Role, Badge, PermanentEffects, InfoArea, customInfo, ChangedNick); - customInfo.UpdateInfo(Player); - - EscapeController escapeController = Player.GameObject.AddComponent(); - escapeController.Init(roleInstance); - - if (Spawn.Spawning.Contains(Player.PlayerId)) - Spawn.Spawning.Remove(Player.PlayerId); + customInfo.UpdateInfo(Player); - if (API.Features.Escape.Bucket.Contains(Player.PlayerId)) - API.Features.Escape.Bucket.Remove(Player.PlayerId); + var escapeController = Player.GameObject.AddComponent(); + escapeController.Init(roleInstance); - LogManager.Debug($"{Player} successfully spawned as {Role.Name} ({Role.Id})! [2VDS]"); - } - catch (Exception ex) - { - LogManager.Error(ex.ToString(), "SP0001"); - } + if (Spawn.Spawning.Contains(Player.PlayerId)) + Spawn.Spawning.Remove(Player.PlayerId); + + if (API.Features.Escape.Bucket.Contains(Player.PlayerId)) + API.Features.Escape.Bucket.Remove(Player.PlayerId); + + LogManager.Debug($"{Player} successfully spawned as {Role.Name} ({Role.Id})! [2VDS]"); + } + catch (Exception ex) + { + LogManager.Error(ex.ToString(), "SP0001"); } + } - public static KeyValuePair? ParseEscapeRole(Dictionary roleAfterEscape, Player player) + public static KeyValuePair? ParseEscapeRole(Dictionary roleAfterEscape, Player player) + { + Dictionary?> AsCuffedByInternalTeam = new(); + Dictionary?> AsCuffedByCustomTeam = new(); + // cuffed by InternalTeam FoundationForces + // 0 1 2 3 = 4 + Dictionary?> AsCuffedByCustomRole = new(); + KeyValuePair? Default = new(false, RoleTypeId.Spectator); + + foreach (var kvp in roleAfterEscape) { - Dictionary?> AsCuffedByInternalTeam = new(); - Dictionary?> AsCuffedByCustomTeam = new(); - // cuffed by InternalTeam FoundationForces - // 0 1 2 3 = 4 - Dictionary?> AsCuffedByCustomRole = new(); - KeyValuePair? Default = new(false, RoleTypeId.Spectator); - - foreach (KeyValuePair kvp in roleAfterEscape) + var Data = ParseEscapeString(kvp.Value); + if (kvp.Key is "default") { - KeyValuePair? Data = ParseEscapeString(kvp.Value); - if (kvp.Key is "default") - Default = Data; - else - { - List Elements = kvp.Key.Split(' ').ToList(); - - if (Elements.Count != 4) - { - LogManager.Warn($"Failed to parse an EscapeRole[key]: syntax should be cuffed by , found {Elements.Count} args!\nSource: {kvp.Key}"); - return new(false, RoleTypeId.Spectator); - } + Default = Data; + } + else + { + var Elements = kvp.Key.Split(' ').ToList(); - if (Elements[0] is not "cuffed") - { - LogManager.Warn($"Failed to parse an EscapeRole[key]: syntax should be cuffed by , found {Elements.Count} args!\nSource: {kvp.Key}"); - return new(false, RoleTypeId.Spectator); - } + if (Elements.Count != 4) + { + LogManager.Warn( + $"Failed to parse an EscapeRole[key]: syntax should be cuffed by , found {Elements.Count} args!\nSource: {kvp.Key}"); + return new KeyValuePair(false, RoleTypeId.Spectator); + } - if (Elements[1] is not "by") - { - LogManager.Warn($"Failed to parse an EscapeRole[key]: syntax should be cuffed by , found {Elements.Count} args!\nSource: {kvp.Key}"); - return new(false, RoleTypeId.Spectator); - } + if (Elements[0] is not "cuffed") + { + LogManager.Warn( + $"Failed to parse an EscapeRole[key]: syntax should be cuffed by , found {Elements.Count} args!\nSource: {kvp.Key}"); + return new KeyValuePair(false, RoleTypeId.Spectator); + } - if ((Elements[2] is "InternalTeam" || Elements[2] is "IT") && Enum.TryParse(Elements[3], out Team team)) - AsCuffedByInternalTeam.TryAdd(team, Data); - else if ((Elements[2] is "CustomTeam" || Elements[2] is "CT") && uint.TryParse(Elements[3], out uint customTeam)) - AsCuffedByCustomTeam.TryAdd(customTeam, Data); - else if ((Elements[2] is "CustomRole" || Elements[2] is "CR") && int.TryParse(Elements[3], out int id) && CustomRole.CustomRoles.ContainsKey(id)) - AsCuffedByCustomRole.TryAdd(id, Data); - else - LogManager.Warn($"Function SpawnManager::ParseEscapeRole[2](<...>) failed!\nPossible causes can be:\n- The source is not valid. Allowed: InternalTeam / IT / CustomRole / CR. Found: {Elements[2]}\n- The target is not a CustomRole / InternalRole. Found: {Elements[3]}"); + if (Elements[1] is not "by") + { + LogManager.Warn( + $"Failed to parse an EscapeRole[key]: syntax should be cuffed by , found {Elements.Count} args!\nSource: {kvp.Key}"); + return new KeyValuePair(false, RoleTypeId.Spectator); } + + if ((Elements[2] is "InternalTeam" || Elements[2] is "IT") && Enum.TryParse(Elements[3], out Team team)) + AsCuffedByInternalTeam.TryAdd(team, Data); + else if ((Elements[2] is "CustomTeam" || Elements[2] is "CT") && + uint.TryParse(Elements[3], out var customTeam)) + AsCuffedByCustomTeam.TryAdd(customTeam, Data); + else if ((Elements[2] is "CustomRole" || Elements[2] is "CR") && + int.TryParse(Elements[3], out var id) && CustomRole.CustomRoles.ContainsKey(id)) + AsCuffedByCustomRole.TryAdd(id, Data); + else + LogManager.Warn( + $"Function SpawnManager::ParseEscapeRole[2](<...>) failed!\nPossible causes can be:\n- The source is not valid. Allowed: InternalTeam / IT / CustomRole / CR. Found: {Elements[2]}\n- The target is not a CustomRole / InternalRole. Found: {Elements[3]}"); } + } - // Now let's assign - if (!player.IsDisarmed) - return Default; - if (player.IsDisarmed && player.DisarmedBy is not null) - if (player.DisarmedBy.TryGetSummonedInstance(out SummonedCustomRole role) && AsCuffedByCustomRole.TryGetValue(role.Role.Id, out var crEscapeRole)) - return crEscapeRole; - else if (UCT.TryGetCustomTeamId(player.DisarmedBy, out uint uctTeamId) && AsCuffedByCustomTeam.TryGetValue(uctTeamId, out var uctEscapeRole)) - return uctEscapeRole; - else if (AsCuffedByInternalTeam.TryGetValue(player.DisarmedBy.Team, out var internalEscapeRole)) - return internalEscapeRole; - - LogManager.Silent($"Returing default type for escaping evaluation of player {player.PlayerId} who's cuffed by {player.DisarmedBy?.Team}"); + // Now let's assign + if (!player.IsDisarmed) return Default; - } + if (player.IsDisarmed && player.DisarmedBy is not null) + if (player.DisarmedBy.TryGetSummonedInstance(out var role) && + AsCuffedByCustomRole.TryGetValue(role.Role.Id, out var crEscapeRole)) + return crEscapeRole; + else if (UCT.TryGetCustomTeamId(player.DisarmedBy, out var uctTeamId) && + AsCuffedByCustomTeam.TryGetValue(uctTeamId, out var uctEscapeRole)) + return uctEscapeRole; + else if (AsCuffedByInternalTeam.TryGetValue(player.DisarmedBy.Team, out var internalEscapeRole)) + return internalEscapeRole; + + LogManager.Silent( + $"Returing default type for escaping evaluation of player {player.PlayerId} who's cuffed by {player.DisarmedBy?.Team}"); + return Default; + } + + public static KeyValuePair? ParseEscapeString(string escape) + { + if (escape is "Deny" or "deny" or "DENY") + return null; - public static KeyValuePair? ParseEscapeString(string escape) + var Elements = escape.Split(' ').ToList(); + if (Elements.Count != 2) { - if (escape is "Deny" or "deny" or "DENY") - return null; - - List Elements = escape.Split(' ').ToList(); - if (Elements.Count != 2) - { - LogManager.Warn($"Failed to parse an EscapeString[value]: syntax should be (2 args), found {Elements.Count} args!\nSource: {escape}"); - return new(false, RoleTypeId.Spectator); - } + LogManager.Warn( + $"Failed to parse an EscapeString[value]: syntax should be (2 args), found {Elements.Count} args!\nSource: {escape}"); + return new KeyValuePair(false, RoleTypeId.Spectator); + } - if ((Elements[0] is "CustomRole" || Elements[0] is "CR") && int.TryParse(Elements[1], out int customRoleId)) - return new(true, customRoleId); - if ((Elements[0] is "InternalRole" || Elements[0] is "IR") && Enum.TryParse(Elements[1], out RoleTypeId role)) - return new(false, role); - LogManager.Warn($"Function SpawnManager::ParseEscapeString(string escape) failed!\nPossible causes can be:\n- The source is not valid. Allowed: InternalRole / IR / CustomRole / CR. Found: {Elements[0]}\n- The target is not a CustomRole / InternalRole. Found: {Elements[1]}"); + if ((Elements[0] is "CustomRole" || Elements[0] is "CR") && int.TryParse(Elements[1], out var customRoleId)) + return new KeyValuePair(true, customRoleId); + if ((Elements[0] is "InternalRole" || Elements[0] is "IR") && Enum.TryParse(Elements[1], out RoleTypeId role)) + return new KeyValuePair(false, role); + LogManager.Warn( + $"Function SpawnManager::ParseEscapeString(string escape) failed!\nPossible causes can be:\n- The source is not valid. Allowed: InternalRole / IR / CustomRole / CR. Found: {Elements[0]}\n- The target is not a CustomRole / InternalRole. Found: {Elements[1]}"); - return new(false, RoleTypeId.Spectator); - } + return new KeyValuePair(false, RoleTypeId.Spectator); + } #nullable enable - public static ICustomRole? DoEvaluateSpawnForPlayer(Player player, RoleTypeId? role = null) - { - role ??= player.Role; + public static ICustomRole? DoEvaluateSpawnForPlayer(Player player, RoleTypeId? role = null) + { + role ??= player.Role; - if (role is null) - return null; + if (role is null) + return null; - RoleTypeId NewRole = (RoleTypeId)role; + var NewRole = (RoleTypeId)role; - Dictionary> RolePercentage = new() + if (player.HasCustomRole()) + { + LogManager.Debug("Was evalutating role select for an already custom role player, stopping"); + return null; + } + + Dictionary> RolePercentage = new(); + foreach (var evaluated in SpawnEvaluatedRoles) + RolePercentage[evaluated] = []; + + foreach (var Role in CustomRole.CustomRoles.Values.Where(cr => cr.SpawnSettings is not null)) + if (!Role.IgnoreSpawnSystem && Player.ReadyList.Count() >= Role.SpawnSettings?.MinPlayers && + SummonedCustomRole.Count(Role) < Role.SpawnSettings.MaxPlayers) { - { RoleTypeId.ClassD, new() }, - { RoleTypeId.Scientist, new() }, - { RoleTypeId.NtfPrivate, new() }, - { RoleTypeId.NtfSergeant, new() }, - { RoleTypeId.NtfCaptain, new() }, - { RoleTypeId.NtfSpecialist, new() }, - { RoleTypeId.ChaosConscript, new() }, - { RoleTypeId.ChaosMarauder, new() }, - { RoleTypeId.ChaosRepressor, new() }, - { RoleTypeId.ChaosRifleman, new() }, - { RoleTypeId.Tutorial, new() }, - { RoleTypeId.Scp049, new() }, - { RoleTypeId.Scp0492, new() }, - { RoleTypeId.Scp079, new() }, - { RoleTypeId.Scp173, new() }, - { RoleTypeId.Scp939, new() }, - { RoleTypeId.Scp096, new() }, - { RoleTypeId.Scp106, new() }, - { RoleTypeId.Scp3114, new() }, - { RoleTypeId.FacilityGuard, new() } - }; - - foreach (ICustomRole Role in CustomRole.CustomRoles.Values.Where(cr => cr.SpawnSettings is not null)) - if (!Role.IgnoreSpawnSystem && Player.ReadyList.Count() >= Role.SpawnSettings?.MinPlayers && SummonedCustomRole.Count(Role) < Role.SpawnSettings.MaxPlayers) + if (Role.SpawnSettings.RequiredPermission is not null) { - if (Role.SpawnSettings.RequiredPermission is not null) + static bool CheckPermission(Player player, string permission) { - static bool CheckPermission(Player player, string permission) - { - if (Enum.TryParse(permission, out PlayerPermissions playerPermissions)) - return player.HasPermission(playerPermissions); + if (Enum.TryParse(permission, out PlayerPermissions playerPermissions)) + return player.HasPermission(playerPermissions); - return player.HasAnyPermission(permission); - } + return player.HasAnyPermission(permission); + } - static IEnumerable ExtractPermissions(object obj) + static IEnumerable ExtractPermissions(object obj) + { + switch (obj) { - switch (obj) + case string s when !string.IsNullOrWhiteSpace(s): + return [s]; + case IEnumerable enumerable: { - case string s when !string.IsNullOrWhiteSpace(s): - return new[] { s }; - case System.Collections.IEnumerable enumerable: + var list = new List(); + foreach (var item in enumerable) { - var list = new List(); - foreach (var item in enumerable) - { - if (item is null) continue; - var s = item.ToString(); - if (!string.IsNullOrWhiteSpace(s)) list.Add(s); - } - return list; + if (item is null) continue; + var s = item.ToString(); + if (!string.IsNullOrWhiteSpace(s)) list.Add(s); } - default: - return Array.Empty(); + + return list; } + default: + return []; } + } - var permsList = ExtractPermissions(Role.SpawnSettings.RequiredPermission).ToList(); - if (permsList.Any()) + var permsList = ExtractPermissions(Role.SpawnSettings.RequiredPermission).ToList(); + if (permsList.Any()) + { + var hasAll = permsList.All(p => CheckPermission(player, p)); + if (!hasAll) { - bool hasAll = permsList.All(p => CheckPermission(player, p)); - if (!hasAll) - { - LogManager.Debug($"Player {player.PlayerId} doesn't have the required permission(s) to spawn as role {Role.Name} ({Role.Id}), skipping... Player Permissions: {string.Join(", ", player.GetPermissions())}, Required permission(s): {string.Join(", ", permsList)}"); - continue; - } + LogManager.Debug( + $"Player {player.PlayerId} doesn't have the required permission(s) to spawn as role {Role.Name} ({Role.Id}), skipping... Player Permissions: {string.Join(", ", player.GetPermissions())}, Required permission(s): {string.Join(", ", permsList)}"); + continue; } } - - foreach (RoleTypeId RoleType in Role.SpawnSettings.CanReplaceRoles) - for (int a = 0; a < Role.SpawnSettings.SpawnChance; a++) - RolePercentage[RoleType].Add(Role); } - if (player.HasCustomRole()) - { - LogManager.Debug("Was evalutating role select for an already custom role player, stopping"); - return null; - } - - if (RolePercentage.ContainsKey(NewRole)) - if (UnityEngine.Random.Range(0, 100) < RolePercentage[NewRole].Count) - return CustomRole.CustomRoles[RolePercentage[NewRole].RandomItem().Id]; - - return null; - } - - public static void AnnounceScpTermination(ReferenceHub scp, DamageHandlerBase hit) - { - string announcement1 = hit.CassieDeathAnnouncement.Announcement; - SubtitlePart[] subtitleParts1 = hit.CassieDeathAnnouncement.SubtitleParts; - if (string.IsNullOrEmpty(announcement1)) - return; - foreach (CassieAnnouncement cassieAnnouncement in CassieAnnouncementDispatcher.AllAnnouncementsPreview) - { - if (cassieAnnouncement is CassieScpTerminationAnnouncement terminationAnnouncement && terminationAnnouncement._announcementTts == announcement1 && SubtitlePart.CheckEqualValues(terminationAnnouncement._subtitles, subtitleParts1)) + foreach (var RoleType in Role.SpawnSettings.CanReplaceRoles) { - terminationAnnouncement._victims.Add(new Footprint(scp)); - terminationAnnouncement._remainingWait = 1f; - return; + if (!RolePercentage.TryGetValue(RoleType, out var bucket)) + continue; + + for (var a = 0; a < Role.SpawnSettings.SpawnChance; a++) + bucket.Add(Role); } } - CassieQueuingScpTerminationEventArgs ev = new CassieQueuingScpTerminationEventArgs(scp, announcement1, subtitleParts1, hit); - LabApi.Events.Handlers.ServerEvents.OnCassieQueuingScpTermination(ev); - if (!ev.IsAllowed) - return; - string announcement2 = ev.Announcement; - SubtitlePart[] subtitleParts2 = ev.SubtitleParts; - new CassieScpTerminationAnnouncement(new Footprint(scp), announcement2, subtitleParts2).AddToQueue(); - LabApi.Events.Handlers.ServerEvents.OnCassieQueuedScpTermination(new CassieQueuedScpTerminationEventArgs(scp, announcement2, subtitleParts2, hit)); - } - internal static IEnumerable LoadAppearanceAffectedPlayers(Player target) - { - List result = new(); - foreach (Player player in Player.ReadyList.Where(p => p.PlayerId != target.PlayerId)) - if (player.TryGetSummonedInstance(out SummonedCustomRole role) && !role.HasModule()) - result.Add(player); - else if (!player.TryGetSummonedInstance(out _)) - result.Add(player); - - return result; - } - } -} + if (RolePercentage.ContainsKey(NewRole)) + if (Random.Range(0, 100) < RolePercentage[NewRole].Count) + return CustomRole.CustomRoles[RolePercentage[NewRole].RandomItem().Id]; + return null; + } + public static void AnnounceScpTermination(ReferenceHub scp, DamageHandlerBase hit) + { + var announcement1 = hit.CassieDeathAnnouncement.Announcement; + var subtitleParts1 = hit.CassieDeathAnnouncement.SubtitleParts; + if (string.IsNullOrEmpty(announcement1)) + return; + foreach (var cassieAnnouncement in CassieAnnouncementDispatcher.AllAnnouncementsPreview) + if (cassieAnnouncement is CassieScpTerminationAnnouncement terminationAnnouncement && + terminationAnnouncement._announcementTts == announcement1 && + SubtitlePart.CheckEqualValues(terminationAnnouncement._subtitles, subtitleParts1)) + { + terminationAnnouncement._victims.Add(new Footprint(scp)); + terminationAnnouncement._remainingWait = 1f; + return; + } + + var ev = new CassieQueuingScpTerminationEventArgs(scp, announcement1, subtitleParts1, hit); + ServerEvents.OnCassieQueuingScpTermination(ev); + if (!ev.IsAllowed) + return; + var announcement2 = ev.Announcement; + var subtitleParts2 = ev.SubtitleParts; + new CassieScpTerminationAnnouncement(new Footprint(scp), announcement2, subtitleParts2).AddToQueue(); + ServerEvents.OnCassieQueuedScpTermination( + new CassieQueuedScpTerminationEventArgs(scp, announcement2, subtitleParts2, hit)); + } + + internal static IEnumerable LoadAppearanceAffectedPlayers(Player target) + { + List result = []; + foreach (var player in Player.ReadyList.Where(p => p.PlayerId != target.PlayerId)) + if (!player.TryGetSummonedInstance(out var role) || !role.HasModule()) + result.Add(player); + + return result; + } +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Manager/VersionManager.cs b/UncomplicatedCustomRoles/Manager/VersionManager.cs index a6d5e4d..0a1a72e 100644 --- a/UncomplicatedCustomRoles/Manager/VersionManager.cs +++ b/UncomplicatedCustomRoles/Manager/VersionManager.cs @@ -1,96 +1,104 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . */ - -using MEC; -using System.Text.Json; + using System; using System.IO; using System.Security.Cryptography; +using System.Text.Json; +using MEC; using UncomplicatedCustomRoles.Extensions; using UncomplicatedCustomRoles.Manager.NET; -namespace UncomplicatedCustomRoles.Manager +namespace UncomplicatedCustomRoles.Manager; + +internal static class VersionManager { - internal static class VersionManager - { - public static VersionInfo VersionInfo { get; set; } + public static VersionInfo VersionInfo { get; set; } - public static bool CorrectHash { get; private set; } = false; + public static bool CorrectHash { get; private set; } #nullable enable - public static void Init() + public static void Init() + { + try { - try + var data = Plugin.HttpManager.VersionInfo(); + data.GetStatusCode(out var msg); + VersionInfo = JsonSerializer.Deserialize(data); + if (VersionInfo is null) { - string data = Plugin.HttpManager.VersionInfo(); - data.GetStatusCode(out string msg); - VersionInfo = JsonSerializer.Deserialize(data); - if (VersionInfo is null) - { - LogManager.Silent($"Failed to convert API endpoint answer to VersionInfo.\nContent: {msg ?? "Message is null"}"); - return; - } + LogManager.Silent( + $"Failed to convert API endpoint answer to VersionInfo.\nContent: {msg ?? "Message is null"}"); + return; + } - if (VersionInfo.PreRelease != 0) - { - LogManager.Info($"\nNOTICE!\nYou are currently using the version v{Plugin.Instance.Version}, who's a PRE-RELEASE or an EXPERIMENTAL RELESE of UncomplicatedCustomRoles!\nLatest stable release: {Plugin.HttpManager.LatestVersion}\nNOTE: This is NOT a stable version, so there can be bugs and malfunctions, for this reason we do not recommend use in production."); - if (VersionInfo.ForceDebug != 0 && !(Plugin.Instance.Config?.Debug ?? true)) - { - LogManager.Info("Debug logs have been activated!"); - Plugin.Instance.Config.Debug = true; - } - } - else + if (VersionInfo.PreRelease != 0) + { + LogManager.Info( + $"\nNOTICE!\nYou are currently using the version v{Plugin.Instance.Version}, who's a PRE-RELEASE or an EXPERIMENTAL RELESE of UncomplicatedCustomRoles!\nLatest stable release: {Plugin.HttpManager.LatestVersion}\nNOTE: This is NOT a stable version, so there can be bugs and malfunctions, for this reason we do not recommend use in production."); + if (VersionInfo.ForceDebug != 0 && !(Plugin.Instance.Config?.Debug ?? true)) { - LogManager.Info($"You are using UncomplicatedCustomRoles v{VersionInfo.Name}{(VersionInfo.CustomName is not null ? $" '{VersionInfo.CustomName}'" : string.Empty)}!"); + LogManager.Info("Debug logs have been activated!"); + Plugin.Instance.Config.Debug = true; } + } + else + { + LogManager.Info( + $"You are using UncomplicatedCustomRoles v{VersionInfo.Name}{(VersionInfo.CustomName is not null ? $" '{VersionInfo.CustomName}'" : string.Empty)}!"); + } - string hash = HashFile(Plugin.Instance.FilePath); - if (hash != VersionInfo.Hash) - HashNotMatchMessageSender(hash); - - else - CorrectHash = true; + var hash = HashFile(Plugin.Instance.FilePath); + if (hash != VersionInfo.Hash) + HashNotMatchMessageSender(hash); - if (VersionInfo.Message is not null) - LogManager.Info(VersionInfo.Message); + else + CorrectHash = true; - if (VersionInfo.Recall != 0 && VersionInfo.RecallTarget is not null && VersionInfo.RecallImportant is not null && VersionInfo.RecallReason is not null) - { - RecallMessageSender(); - if ((bool)VersionInfo.RecallImportant) - Timing.CallContinuously(500000, RecallMessageSender); - } - } - catch (Exception e) + if (VersionInfo.Message is not null) + LogManager.Info(VersionInfo.Message); + + if (VersionInfo.Recall != 0 && VersionInfo.RecallTarget is not null && + VersionInfo.RecallImportant is not null && VersionInfo.RecallReason is not null) { - LogManager.Error("An error occurred while trying to fetch the version info from our central servers."); - LogManager.Debug(e.ToString()); + RecallMessageSender(); + if ((bool)VersionInfo.RecallImportant) + Timing.CallContinuously(500000, RecallMessageSender); } } + catch (Exception e) + { + LogManager.Error("An error occurred while trying to fetch the version info from our central servers."); + LogManager.Debug(e.ToString()); + } + } - public static void HashNotMatchMessageSender(string hash) => LogManager.Error($"\nIMPORTANT ERROR!\nFAILED TO VERIFY THE PLUGIN FILE!\nThe hash of the current executable file DOES NOT MATCH the hash of that version in our database!\nOfficial hash: {VersionInfo.Hash}\nCurrent hash: {hash}", "CS0102"); - - public static void RecallMessageSender() => LogManager.Warn($"\n>>> IMPORTANT NOTICE <<<\nThe current version of the plugin ({VersionInfo.Name}) HAS BEEN RECALLED FOR THE FOLLOWING REASON:\n| {VersionInfo.RecallReason?.Replace(Environment.NewLine, $"{Environment.NewLine}| ")}\nFor that reason we are asking you to PLEASE update to the next stable version, who's the {VersionInfo.RecallTarget}!\nThis version CONTAINS IMPORTANT BUGS and for that reason SWITCHING TO THE NEWER ONE IS ESSENTIAL!"); + public static void HashNotMatchMessageSender(string hash) + { + LogManager.Error( + $"\nIMPORTANT ERROR!\nFAILED TO VERIFY THE PLUGIN FILE!\nThe hash of the current executable file DOES NOT MATCH the hash of that version in our database!\nOfficial hash: {VersionInfo.Hash}\nCurrent hash: {hash}", + "CS0102"); + } - public static string HashFile(string path) - { - FileStream file = new(path, FileMode.Open) - { - Position = 0 - }; - byte[] bytes = SHA256Managed.Create().ComputeHash(file); + public static void RecallMessageSender() + { + LogManager.Warn( + $"\n>>> IMPORTANT NOTICE <<<\nThe current version of the plugin ({VersionInfo.Name}) HAS BEEN RECALLED FOR THE FOLLOWING REASON:\n| {VersionInfo.RecallReason?.Replace(Environment.NewLine, $"{Environment.NewLine}| ")}\nFor that reason we are asking you to PLEASE update to the next stable version, who's the {VersionInfo.RecallTarget}!\nThis version CONTAINS IMPORTANT BUGS and for that reason SWITCHING TO THE NEWER ONE IS ESSENTIAL!"); + } - file.Close(); + public static string HashFile(string path) + { + using FileStream file = new(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + using var sha = SHA256.Create(); + var bytes = sha.ComputeHash(file); - return BitConverter.ToString(bytes).Replace("-", string.Empty); - } + return BitConverter.ToString(bytes).Replace("-", string.Empty); } } \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Manager/YamlFlagsHandler.cs b/UncomplicatedCustomRoles/Manager/YamlFlagsHandler.cs index 1896ae9..a17b44a 100644 --- a/UncomplicatedCustomRoles/Manager/YamlFlagsHandler.cs +++ b/UncomplicatedCustomRoles/Manager/YamlFlagsHandler.cs @@ -1,8 +1,8 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . @@ -11,58 +11,61 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Reflection; using UncomplicatedCustomRoles.API.Features.CustomModules; using UncomplicatedCustomRoles.Extensions; -namespace UncomplicatedCustomRoles.Manager -{ +namespace UncomplicatedCustomRoles.Manager; #nullable enable - class YamlFlagsHandler +internal class YamlFlagsHandler +{ + private static Type[]? _modules; + + public static Type[] Modules { - public static Type[] Modules + get { - get - { - _modules ??= GetModules(); - return _modules; - } + _modules ??= GetModules(); + return _modules; } + } - private static Type[]? _modules = null; + internal static void InvalidateCache() + { + _modules = null; + } - public static Dictionary?>? Decode(List flags) - { - if (flags is null) - return null; + public static Dictionary?>? Decode(List flags) + { + if (flags is null) + return null; - Dictionary?> result = new(); + Dictionary?> result = new(); - foreach (object flag in flags) + foreach (var flag in flags) + if (flag is Dictionary str) + { + foreach (var res in str) + if (res.Value is Dictionary dict) + result[res.Key.ToString()] = dict.ConvertKeyToString(); + } + else { - if (flag is Dictionary str) - { - foreach (KeyValuePair res in str) - if (res.Value is Dictionary dict) - result[res.Key.ToString()] = dict.ConvertKeyToString(); - } - else - result[flag.ToString()] = null; + result[flag.ToString()] = null; } - return result; - } + return result; + } - public static Type[] GetModules() - { - List types = new(); + public static Type[] GetModules() + { + List types = []; - foreach (Assembly assembly in ImportManager.AvailableAssemblies) - foreach (Type type in assembly.GetTypes().Where(t => t.IsClass && !t.IsAbstract && t.IsSubclassOf(typeof(CustomModule)))) - types.Add(type); + foreach (var assembly in ImportManager.AvailableAssemblies) + foreach (var type in assembly.GetTypes() + .Where(t => t.IsClass && !t.IsAbstract && t.IsSubclassOf(typeof(CustomModule)))) + types.Add(type); - return types.ToArray(); - } + return types.ToArray(); } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Patches/Announcer.cs b/UncomplicatedCustomRoles/Patches/Announcer.cs index d2ab858..b69bfa9 100644 --- a/UncomplicatedCustomRoles/Patches/Announcer.cs +++ b/UncomplicatedCustomRoles/Patches/Announcer.cs @@ -1,15 +1,14 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . */ using System.Collections.Generic; -using System.Text; using Cassie; using HarmonyLib; using NorthwoodLib.Pools; @@ -20,59 +19,65 @@ using UncomplicatedCustomRoles.API.Features.CustomModules; using UncomplicatedCustomRoles.Manager; -namespace UncomplicatedCustomRoles.Patches +namespace UncomplicatedCustomRoles.Patches; + +internal static class Announcer { - internal static class Announcer - { - internal static readonly Dictionary SavedCustomAnnouncements = new(); - } - - [HarmonyPatch(typeof(CassieScpTerminationAnnouncement), nameof(CassieScpTerminationAnnouncement.AnnounceScpTermination))] - internal class AnnounceScpTerminationPatch + internal static readonly Dictionary SavedCustomAnnouncements = new(); +} + +[HarmonyPatch(typeof(CassieScpTerminationAnnouncement), + nameof(CassieScpTerminationAnnouncement.AnnounceScpTermination))] +internal class AnnounceScpTerminationPatch +{ + private static bool Prefix(ReferenceHub scp, DamageHandlerBase hit) { - private static bool Prefix(ReferenceHub scp, DamageHandlerBase hit) + if (scp.GetTeam() is Team.SCPs && SummonedCustomRole.TryGet(scp, out var role)) { - if (scp.GetTeam() is Team.SCPs && SummonedCustomRole.TryGet(scp, out SummonedCustomRole role)) - { - if (role.HasModule()) - return false; - - if (!role.TryGetModule(out CustomScpAnnouncer announcer)) return true; - SpawnManager.AnnounceScpTermination(scp, hit); - Announcer.SavedCustomAnnouncements[scp.PlayerId] = announcer.RoleName; + if (role.HasModule()) return false; - } - return true; + if (!role.TryGetModule(out CustomScpAnnouncer announcer)) return true; + SpawnManager.AnnounceScpTermination(scp, hit); + Announcer.SavedCustomAnnouncements[scp.PlayerId] = announcer.RoleName; + return false; } + + return true; } - - [HarmonyPatch(typeof(CassieScpTerminationAnnouncement), nameof(CassieScpTerminationAnnouncement.OnStartedPlaying))] - internal class OnStartedPlayingPatch +} + +[HarmonyPatch(typeof(CassieScpTerminationAnnouncement), nameof(CassieScpTerminationAnnouncement.OnStartedPlaying))] +internal class OnStartedPlayingPatch +{ + private static bool Prefix(CassieScpTerminationAnnouncement __instance) { - private static bool Prefix(CassieScpTerminationAnnouncement __instance) + var stringBuilder = StringBuilderPool.Shared.Rent(); + List subtitlePartList = []; + for (var index = 0; index < __instance.Victims.Count; ++index) { - StringBuilder stringBuilder = StringBuilderPool.Shared.Rent(); - List subtitlePartList = new List(); - for (int index = 0; index < __instance.Victims.Count; ++index) + string withoutSpace; + string withSpace; + if (Announcer.SavedCustomAnnouncements.TryGetValue(__instance.Victims[index].PlayerId, out var value)) { - string withoutSpace; - string withSpace; - if (Announcer.SavedCustomAnnouncements.TryGetValue(__instance.Victims[index].PlayerId, out string value)) - { - CassieScpTerminationAnnouncement.ConvertSCP(value, out withoutSpace, out withSpace); - Announcer.SavedCustomAnnouncements.Remove(__instance.Victims[index].PlayerId); - } else - CassieScpTerminationAnnouncement.ConvertSCP(__instance.Victims[index].Role, out withoutSpace, out withSpace); - - stringBuilder.Append(index == 0 ? "SCP " : ". SCP "); - stringBuilder.Append(withSpace); - subtitlePartList.Add(new SubtitlePart(SubtitleType.SCP, withoutSpace)); + CassieScpTerminationAnnouncement.ConvertSCP(value, out withoutSpace, out withSpace); + Announcer.SavedCustomAnnouncements.Remove(__instance.Victims[index].PlayerId); } - stringBuilder.Append(__instance._announcementTts); - subtitlePartList.AddRange(__instance._subtitles); - __instance.Payload = new CassieTtsPayload(StringBuilderPool.Shared.ToStringReturn(stringBuilder), subtitlePartList.ToArray()); - return false; + else + { + CassieScpTerminationAnnouncement.ConvertSCP(__instance.Victims[index].Role, out withoutSpace, + out withSpace); + } + + stringBuilder.Append(index == 0 ? "SCP " : ". SCP "); + stringBuilder.Append(withSpace); + subtitlePartList.Add(new SubtitlePart(SubtitleType.SCP, withoutSpace)); } + + stringBuilder.Append(__instance._announcementTts); + subtitlePartList.AddRange(__instance._subtitles); + __instance.Payload = new CassieTtsPayload(StringBuilderPool.Shared.ToStringReturn(stringBuilder), + subtitlePartList.ToArray()); + return false; } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Patches/ChangeCustomPlayerInfoPatch.cs b/UncomplicatedCustomRoles/Patches/ChangeCustomPlayerInfoPatch.cs index 64e3c77..49920d6 100644 --- a/UncomplicatedCustomRoles/Patches/ChangeCustomPlayerInfoPatch.cs +++ b/UncomplicatedCustomRoles/Patches/ChangeCustomPlayerInfoPatch.cs @@ -1,12 +1,9 @@ using System; -using System.Collections.Generic; -using System.Text; using CommandSystem; using CommandSystem.Commands.RemoteAdmin; using HarmonyLib; using LabApi.Features.Wrappers; using NorthwoodLib.Pools; -using UncomplicatedCustomRoles.API.Features; using UncomplicatedCustomRoles.Extensions; using Utils; @@ -15,51 +12,61 @@ namespace UncomplicatedCustomRoles.Patches; [HarmonyPatch(typeof(ChangeCustomPlayerInfoCommand), nameof(ChangeCustomPlayerInfoCommand.Execute))] internal class ChangeCustomPlayerInfoPatch { - private static bool Prefix(ChangeCustomPlayerInfoCommand __instance, ArraySegment arguments, ICommandSender sender, out string response, ref bool __result) + private static bool Prefix(ChangeCustomPlayerInfoCommand __instance, ArraySegment arguments, + ICommandSender sender, out string response, ref bool __result) { if (!sender.CheckPermission(PlayerPermissions.PlayersManagement, out response)) { __result = false; return false; } + if (arguments.Count < 1) { - response = $"To execute this command provide at least 1 argument!\nUsage: {arguments.Array[0]} {__instance.DisplayCommandUsage()}"; + response = + $"To execute this command provide at least 1 argument!\nUsage: {arguments.Array[0]} {__instance.DisplayCommandUsage()}"; __result = false; return false; } + string[] newargs; - List referenceHubList = RAUtils.ProcessPlayerIdOrNamesList(arguments, 0, out newargs); + var referenceHubList = RAUtils.ProcessPlayerIdOrNamesList(arguments, 0, out newargs); if (referenceHubList == null) { response = "Cannot find player! Try using the player ID!"; __result = false; return false; } - string str = newargs == null ? (string) null : string.Join(" ", newargs); - StringBuilder stringBuilder = StringBuilderPool.Shared.Rent(); - foreach (ReferenceHub me in referenceHubList) + + var str = newargs == null ? null : string.Join(" ", newargs); + var stringBuilder = StringBuilderPool.Shared.Rent(); + foreach (var me in referenceHubList) { var player = Player.Get(me); if (str == null) { - ServerLogs.AddLog(ServerLogs.Modules.Administrative, $"{sender.LogName} cleared custom info of player {me.PlayerId} ({me.nicknameSync.MyNick}).", ServerLogs.ServerLogType.RemoteAdminActivity_GameChanging); - stringBuilder.AppendFormat("Reset {0}'s custom info.\n", (object) me.LoggedNameFromRefHub()); - if (player.TryGetSummonedInstance(out SummonedCustomRole summonedInstance)) + ServerLogs.AddLog(ServerLogs.Modules.Administrative, + $"{sender.LogName} cleared custom info of player {me.PlayerId} ({me.nicknameSync.MyNick}).", + ServerLogs.ServerLogType.RemoteAdminActivity_GameChanging); + stringBuilder.AppendFormat("Reset {0}'s custom info.\n", me.LoggedNameFromRefHub()); + if (player.TryGetSummonedInstance(out var summonedInstance)) summonedInstance.CustomInfo.Info = string.Empty; else me.nicknameSync.CustomPlayerInfo = null; } else { - ServerLogs.AddLog(ServerLogs.Modules.Administrative, $"{sender.LogName} set custom info of player {me.PlayerId} ({me.nicknameSync.MyNick}) to \"{str}\".", ServerLogs.ServerLogType.RemoteAdminActivity_GameChanging); - stringBuilder.AppendFormat("Set {0}'s custom info to: {1}\n", (object) me.LoggedNameFromRefHub(), (object) str); - if (player.TryGetSummonedInstance(out SummonedCustomRole summonedInstance)) + ServerLogs.AddLog(ServerLogs.Modules.Administrative, + $"{sender.LogName} set custom info of player {me.PlayerId} ({me.nicknameSync.MyNick}) to \"{str}\".", + ServerLogs.ServerLogType.RemoteAdminActivity_GameChanging); + stringBuilder.AppendFormat("Set {0}'s custom info to: {1}\n", me.LoggedNameFromRefHub(), str); + if (player.TryGetSummonedInstance(out var summonedInstance)) summonedInstance.CustomInfo.Info = str; else me.nicknameSync.CustomPlayerInfo = str; } } + response = stringBuilder.ToString().Trim(); StringBuilderPool.Shared.Return(stringBuilder); __result = true; diff --git a/UncomplicatedCustomRoles/Patches/MakingNoise.cs b/UncomplicatedCustomRoles/Patches/MakingNoise.cs index c95c304..57509a8 100644 --- a/UncomplicatedCustomRoles/Patches/MakingNoise.cs +++ b/UncomplicatedCustomRoles/Patches/MakingNoise.cs @@ -1,8 +1,8 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . @@ -10,21 +10,20 @@ using HarmonyLib; using PlayerRoles.FirstPersonControl.Thirdperson; -using UncomplicatedCustomRoles.API.Features; using UncomplicatedCustomRoles.API.Features.CustomModules; using UncomplicatedCustomRoles.Extensions; -namespace UncomplicatedCustomRoles.Patches +namespace UncomplicatedCustomRoles.Patches; + +[HarmonyPatch(typeof(AnimatedCharacterModel), nameof(AnimatedCharacterModel.PlayFootstep))] +internal class MakingNoise { - [HarmonyPatch(typeof(AnimatedCharacterModel), nameof(AnimatedCharacterModel.PlayFootstep))] - internal class MakingNoise + private static bool Prefix(AnimatedCharacterModel __instance) { - static bool Prefix(AnimatedCharacterModel __instance) - { - if (__instance.OwnerHub.TryGetSummonedInstance(out SummonedCustomRole summonedInstance) && summonedInstance.HasModule()) - return false; + if (__instance.OwnerHub.TryGetSummonedInstance(out var summonedInstance) && + summonedInstance.HasModule()) + return false; - return true; - } + return true; } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Patches/PlayerEventPrefix.cs b/UncomplicatedCustomRoles/Patches/PlayerEventPrefix.cs index e875b3a..e31cc1f 100644 --- a/UncomplicatedCustomRoles/Patches/PlayerEventPrefix.cs +++ b/UncomplicatedCustomRoles/Patches/PlayerEventPrefix.cs @@ -1,75 +1,76 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . */ -using HarmonyLib; -using LabApi.Events.Arguments.Interfaces; -using LabApi.Events.Handlers; using System; using System.Collections.Generic; using System.Linq; using System.Reflection; +using HarmonyLib; +using LabApi.Events.Arguments.Interfaces; +using LabApi.Events.Handlers; using UncomplicatedCustomRoles.API.Features; -using UncomplicatedCustomRoles.API.Features.CustomModules; using UncomplicatedCustomRoles.Extensions; using UncomplicatedCustomRoles.Manager; -namespace UncomplicatedCustomRoles.Patches +namespace UncomplicatedCustomRoles.Patches; + +internal class PlayerEventPrefix { - internal class PlayerEventPrefix - { - private static IEnumerable _patchedMethods = new List(); + private static IEnumerable _patchedMethods = new List(); - private static readonly Dictionary EventNameCache = new(); + private static readonly Dictionary EventNameCache = new(); - private static void Prefix(IPlayerEvent ev) + private static void Prefix(IPlayerEvent ev) + { + try { - try - { - CustomRoleEventHandler.InvokeAll(ev); + CustomRoleEventHandler.InvokeAll(ev); - if (SummonedCustomRole.EventTriggeredModuleTotal > 0 - && ev.Player is not null && ev.Player.TryGetSummonedInstance(out SummonedCustomRole customRole)) + if (SummonedCustomRole.EventTriggeredModuleTotal > 0 + && ev.Player is not null && ev.Player.TryGetSummonedInstance(out var customRole)) + { + var eventType = ev.GetType(); + if (!EventNameCache.TryGetValue(eventType, out var name)) { - Type eventType = ev.GetType(); - if (!EventNameCache.TryGetValue(eventType, out string name)) - { - name = eventType.Name.Replace("EventArgs", string.Empty).Replace("Player", string.Empty); - EventNameCache[eventType] = name; - } - - foreach (CustomModule module in customRole.CustomModules) - if (module.TriggerOnEvents.Contains(name)) - if (!module.OnEvent(name, ev) && ev is ICancellableEvent deniableEvent) - deniableEvent.IsAllowed = false; + name = eventType.Name.Replace("EventArgs", string.Empty).Replace("Player", string.Empty); + EventNameCache[eventType] = name; } - } - catch (Exception ex) - { - LogManager.Error(ex.ToString()); + + foreach (var module in customRole.CustomModules) + if (module.TriggerOnEvents.Contains(name)) + if (!module.OnEvent(name, ev) && ev is ICancellableEvent deniableEvent) + deniableEvent.IsAllowed = false; } } - - internal static void Patch(Harmony harmony) + catch (Exception ex) { - HarmonyMethod prefixMethod = new(typeof(PlayerEventPrefix).GetMethod("Prefix", BindingFlags.Static | BindingFlags.NonPublic)); + LogManager.Error(ex.ToString()); + } + } + + internal static void Patch(Harmony harmony) + { + HarmonyMethod prefixMethod = + new(typeof(PlayerEventPrefix).GetMethod("Prefix", BindingFlags.Static | BindingFlags.NonPublic)); - _patchedMethods = typeof(PlayerEvents).GetMethods().Where(m => m.Name.StartsWith("On") && m.GetParameters().Length > 0 && typeof(IPlayerEvent).IsAssignableFrom(m.GetParameters()[0].ParameterType)); + _patchedMethods = typeof(PlayerEvents).GetMethods().Where(m => + m.Name.StartsWith("On") && m.GetParameters().Length > 0 && + typeof(IPlayerEvent).IsAssignableFrom(m.GetParameters()[0].ParameterType)); - foreach (MethodInfo method in _patchedMethods) - harmony.Patch(method, prefix: prefixMethod); - } + foreach (var method in _patchedMethods) + harmony.Patch(method, prefixMethod); + } - internal static void Unpatch(Harmony harmony) - { - foreach (MethodInfo method in _patchedMethods) - harmony.Unpatch(method, HarmonyPatchType.All); - } + internal static void Unpatch(Harmony harmony) + { + foreach (var method in _patchedMethods) + harmony.Unpatch(method, HarmonyPatchType.All); } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Patches/PlayerInfoSyncPatch.cs b/UncomplicatedCustomRoles/Patches/PlayerInfoSyncPatch.cs index 561ba75..3465414 100644 --- a/UncomplicatedCustomRoles/Patches/PlayerInfoSyncPatch.cs +++ b/UncomplicatedCustomRoles/Patches/PlayerInfoSyncPatch.cs @@ -12,43 +12,43 @@ using UncomplicatedCustomRoles.API.Features; using UncomplicatedCustomRoles.Extensions; -namespace UncomplicatedCustomRoles.Patches +namespace UncomplicatedCustomRoles.Patches; + +[HarmonyPatch(typeof(NicknameSync), nameof(NicknameSync.Network_customPlayerInfoString), MethodType.Setter)] +internal class CustomPlayerInfoSyncPatch { - [HarmonyPatch(typeof(NicknameSync), nameof(NicknameSync.Network_customPlayerInfoString), MethodType.Setter)] - internal class CustomPlayerInfoSyncPatch + private static bool Prefix(NicknameSync __instance, string value) { - private static bool Prefix(NicknameSync __instance, string value) - { - if (CustomInfo.SuppressExternalSync) - return true; - - if (__instance._hub is not null && __instance._hub.TryGetSummonedInstance(out SummonedCustomRole role) && role.CustomInfo is not null) - { - if (role.CustomInfo.Info != value) - role.CustomInfo.Info = value; + if (CustomInfo.SuppressExternalSync) + return true; - return false; - } + if (__instance._hub is not null && __instance._hub.TryGetSummonedInstance(out var role) && + role.CustomInfo is not null) + { + if (role.CustomInfo.Info != value) + role.CustomInfo.Info = value; - return true; + return false; } + + return true; } +} - [HarmonyPatch(typeof(NicknameSync), nameof(NicknameSync.Network_playerInfoToShow), MethodType.Setter)] - internal class PlayerInfoAreaSyncPatch +[HarmonyPatch(typeof(NicknameSync), nameof(NicknameSync.Network_playerInfoToShow), MethodType.Setter)] +internal class PlayerInfoAreaSyncPatch +{ + private static void Prefix(NicknameSync __instance, ref PlayerInfoArea value) { - private static void Prefix(NicknameSync __instance, ref PlayerInfoArea value) + if (CustomInfo.SuppressExternalSync) + return; + + if (__instance._hub is not null && __instance._hub.TryGetSummonedInstance(out var _)) { - if (CustomInfo.SuppressExternalSync) - return; - - if (__instance._hub is not null && __instance._hub.TryGetSummonedInstance(out SummonedCustomRole _)) - { - value |= PlayerInfoArea.CustomInfo; - value &= ~PlayerInfoArea.Role; - value &= ~PlayerInfoArea.Nickname; - value &= ~PlayerInfoArea.UnitName; - } + value |= PlayerInfoArea.CustomInfo; + value &= ~PlayerInfoArea.Role; + value &= ~PlayerInfoArea.Nickname; + value &= ~PlayerInfoArea.UnitName; } } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Patches/Scp3114StranglePrefix.cs b/UncomplicatedCustomRoles/Patches/Scp3114StranglePrefix.cs index 6dcbacd..979b983 100644 --- a/UncomplicatedCustomRoles/Patches/Scp3114StranglePrefix.cs +++ b/UncomplicatedCustomRoles/Patches/Scp3114StranglePrefix.cs @@ -1,8 +1,8 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . @@ -12,32 +12,35 @@ using PlayerRoles.PlayableScps.Scp3114; using UncomplicatedCustomRoles.API.Features; -namespace UncomplicatedCustomRoles.Patches +namespace UncomplicatedCustomRoles.Patches; + +[HarmonyPatch(typeof(Scp3114Strangle), nameof(Scp3114Strangle.ValidateTarget))] +internal class Scp3114StranglePrefix { - [HarmonyPatch(typeof(Scp3114Strangle), nameof(Scp3114Strangle.ValidateTarget))] - internal class Scp3114StranglePrefix + private static bool Prefix(ReferenceHub player, ref bool __result, Scp3114Strangle __instance) { - private static bool Prefix(ReferenceHub player, ref bool __result, Scp3114Strangle __instance) - { - if (player is null) - return true; + if (player is null) + return true; - if (player.roleManager.CurrentRole is null) - return true; + if (player.roleManager.CurrentRole is null) + return true; - if (SummonedCustomRole.TryGet(player, out SummonedCustomRole playerRole) && playerRole.Role.IsFriendOf is not null && playerRole.Role.IsFriendOf.Contains(__instance.Owner.roleManager.CurrentRole.Team)) - { - // Attacked player can't be strangled by SCP-3114 as it's his friend :) - __result = false; - return false; // Skip - } else if (SummonedCustomRole.TryGet(__instance.Owner, out SummonedCustomRole scpRole) && scpRole.Role.IsFriendOf is not null && scpRole.Role.IsFriendOf.Contains(player.roleManager.CurrentRole.Team)) - { - // Attacked player can't be strangled by SCP-3114 as it's his friend :) - __result = false; - return false; // Skip - } + if (SummonedCustomRole.TryGet(player, out var playerRole) && playerRole.Role.IsFriendOf is not null && + playerRole.Role.IsFriendOf.Contains(__instance.Owner.roleManager.CurrentRole.Team)) + { + // Attacked player can't be strangled by SCP-3114 as it's his friend :) + __result = false; + return false; // Skip + } - return true; + if (SummonedCustomRole.TryGet(__instance.Owner, out var scpRole) && scpRole.Role.IsFriendOf is not null && + scpRole.Role.IsFriendOf.Contains(player.roleManager.CurrentRole.Team)) + { + // Attacked player can't be strangled by SCP-3114 as it's his friend :) + __result = false; + return false; // Skip } + + return true; } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Patches/ServerNamePatch.cs b/UncomplicatedCustomRoles/Patches/ServerNamePatch.cs index 19289bd..0a50bba 100644 --- a/UncomplicatedCustomRoles/Patches/ServerNamePatch.cs +++ b/UncomplicatedCustomRoles/Patches/ServerNamePatch.cs @@ -1,8 +1,8 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . @@ -10,11 +10,14 @@ using HarmonyLib; -namespace UncomplicatedCustomRoles.Patches +namespace UncomplicatedCustomRoles.Patches; + +[HarmonyPatch(typeof(ServerConsole), nameof(ServerConsole.ReloadServerName))] +internal class ServerNamePatch { - [HarmonyPatch(typeof(ServerConsole), nameof(ServerConsole.ReloadServerName))] - internal class ServerNamePatch + private static void Postfix() { - private static void Postfix() => ServerConsole.ServerName += $"UCR {Plugin.Instance.Version.ToString(3)}"; + ServerConsole.ServerName += + $"UCR {Plugin.Instance.Version.ToString(3)}"; } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Patches/SetRolePatch.cs b/UncomplicatedCustomRoles/Patches/SetRolePatch.cs index ac1403b..7618439 100644 --- a/UncomplicatedCustomRoles/Patches/SetRolePatch.cs +++ b/UncomplicatedCustomRoles/Patches/SetRolePatch.cs @@ -1,8 +1,8 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . @@ -15,43 +15,46 @@ using Respawning.NamingRules; using UncomplicatedCustomRoles.API.Features; -namespace UncomplicatedCustomRoles.Patches -{ - internal static class UcrSpawnContext - { - [ThreadStatic] private static int _depth; +namespace UncomplicatedCustomRoles.Patches; - internal static bool Active => _depth > 0; +internal static class UcrSpawnContext +{ + [ThreadStatic] private static int _depth; - internal static void Enter() => _depth++; + internal static bool Active => _depth > 0; - internal static void Exit() - { - if (_depth > 0) - _depth--; - } + internal static void Enter() + { + _depth++; } - - [HarmonyPatch(typeof(PlayerRoleManager), nameof(PlayerRoleManager.InitializeNewRole))] - internal class SetRolePatch + + internal static void Exit() { - static void Prefix(PlayerRoleManager __instance, RoleTypeId targetId, RoleChangeReason reason, RoleSpawnFlags spawnFlags = RoleSpawnFlags.All, NetworkReader data = null) - { - if (SummonedCustomRole.TryGet(__instance.Hub, out SummonedCustomRole role)) - role.Destroy(); - } - - static void Postfix(PlayerRoleManager __instance, RoleChangeReason reason) - { - if (!UcrSpawnContext.Active || reason is not RoleChangeReason.Respawn) - return; - - if (__instance.CurrentRole is HumanRole humanRole - && NamingRulesManager.TryGetNamingRule(humanRole.Team, out _) - && NamingRulesManager.GeneratedNames.TryGetValue(humanRole.Team, out var names) - && names.Count > 0 - && humanRole.UnitNameId >= names.Count) - humanRole.UnitNameId = (byte)(names.Count - 1); - } + if (_depth > 0) + _depth--; } } + +[HarmonyPatch(typeof(PlayerRoleManager), nameof(PlayerRoleManager.InitializeNewRole))] +internal class SetRolePatch +{ + private static void Prefix(PlayerRoleManager __instance, RoleTypeId targetId, RoleChangeReason reason, + RoleSpawnFlags spawnFlags = RoleSpawnFlags.All, NetworkReader data = null) + { + if (SummonedCustomRole.TryGet(__instance.Hub, out var role)) + role.Destroy(); + } + + private static void Postfix(PlayerRoleManager __instance, RoleChangeReason reason) + { + if (!UcrSpawnContext.Active || reason is not RoleChangeReason.Respawn) + return; + + if (__instance.CurrentRole is HumanRole humanRole + && NamingRulesManager.TryGetNamingRule(humanRole.Team, out _) + && NamingRulesManager.GeneratedNames.TryGetValue(humanRole.Team, out var names) + && names.Count > 0 + && humanRole.UnitNameId >= names.Count) + humanRole.UnitNameId = (byte)(names.Count - 1); + } +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Patches/StaminaUsagePatch.cs b/UncomplicatedCustomRoles/Patches/StaminaUsagePatch.cs index eb9bae8..7e7f506 100644 --- a/UncomplicatedCustomRoles/Patches/StaminaUsagePatch.cs +++ b/UncomplicatedCustomRoles/Patches/StaminaUsagePatch.cs @@ -12,27 +12,26 @@ using InventorySystem; using UncomplicatedCustomRoles.Extensions; -namespace UncomplicatedCustomRoles.Patches +namespace UncomplicatedCustomRoles.Patches; + +[HarmonyPatch(typeof(Inventory), nameof(Inventory.StaminaUsageMultiplier), MethodType.Getter)] +public class StaminaUsagePatch { - [HarmonyPatch(typeof(Inventory), nameof(Inventory.StaminaUsageMultiplier), MethodType.Getter)] - public class StaminaUsagePatch + public static void Postfix(Inventory __instance, ref float __result) { - public static void Postfix(Inventory __instance, ref float __result) - { - if (!__instance._hub.TryGetSummonedInstance(out var role)) - return; - __result *= role.Role.Stamina.Infinite ? 0 : role.Role.Stamina.UsageMultiplier; - } + if (!__instance._hub.TryGetSummonedInstance(out var role)) + return; + __result *= role.Role.Stamina.Infinite ? 0 : role.Role.Stamina.UsageMultiplier; } +} - [HarmonyPatch(typeof(Inventory), nameof(Inventory.StaminaRegenMultiplier), MethodType.Getter)] - public class StaminaRegenPatch +[HarmonyPatch(typeof(Inventory), nameof(Inventory.StaminaRegenMultiplier), MethodType.Getter)] +public class StaminaRegenPatch +{ + public static void Postfix(Inventory __instance, ref float __result) { - public static void Postfix(Inventory __instance, ref float __result) - { - if (!__instance._hub.TryGetSummonedInstance(out var role)) - return; - __result *= role.Role.Stamina.RegenMultiplier; - } + if (!__instance._hub.TryGetSummonedInstance(out var role)) + return; + __result *= role.Role.Stamina.RegenMultiplier; } } \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Patches/TeamPatch.cs b/UncomplicatedCustomRoles/Patches/TeamPatch.cs index bc32fe6..72af53b 100644 --- a/UncomplicatedCustomRoles/Patches/TeamPatch.cs +++ b/UncomplicatedCustomRoles/Patches/TeamPatch.cs @@ -8,346 +8,388 @@ * If not, see . */ -using Achievements.Handlers; -using Footprinting; -using HarmonyLib; -using Interactables.Interobjects.DoorUtils; -using Mirror; -using InventorySystem.Items.ThrowableProjectiles; -using PlayerRoles; -using PlayerRoles.PlayableScps.Scp079.Rewards; -using PlayerRoles.PlayableScps.Scp939.Mimicry; using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Reflection.Emit; +using Achievements.Handlers; +using Footprinting; +using HarmonyLib; +using Interactables.Interobjects.DoorUtils; using InventorySystem.Disarming; using InventorySystem.Items; +using InventorySystem.Items.ThrowableProjectiles; using InventorySystem.Searching; -using MapGeneration.Distributors; +using Mirror; +using PlayerRoles; using PlayerRoles.PlayableScps.HumanTracker; using PlayerRoles.PlayableScps.Scp079; +using PlayerRoles.PlayableScps.Scp079.Rewards; +using PlayerRoles.PlayableScps.Scp939.Mimicry; using PlayerStatsSystem; using UncomplicatedCustomRoles.API.Features; using UncomplicatedCustomRoles.Manager; using static HarmonyLib.AccessTools; -namespace UncomplicatedCustomRoles.Patches +namespace UncomplicatedCustomRoles.Patches; + +[HarmonyPatchCategory(TeamPatchManager.Category)] +[HarmonyPatch(typeof(PlayerRoleManager), nameof(PlayerRoleManager.CurrentRole), MethodType.Getter)] +internal class PlayerRoleManagerPatch { - [HarmonyPatchCategory(TeamPatchManager.Category)] - [HarmonyPatch(typeof(PlayerRoleManager), nameof(PlayerRoleManager.CurrentRole), MethodType.Getter)] - internal class PlayerRoleManagerPatch + private static bool Prefix(PlayerRoleManager __instance, ref PlayerRoleBase __result) { - static bool Prefix(PlayerRoleManager __instance, ref PlayerRoleBase __result) - { + var hub = __instance.Hub; + if (hub is null || !DisguiseTeam.RoleBaseList.TryGetValue(hub.PlayerId, out var role) || role is null) + return true; - ReferenceHub hub = __instance.Hub; - if (hub is null || !DisguiseTeam.RoleBaseList.TryGetValue(hub.PlayerId, out PlayerRoleBase role) || role is null) - return true; + if (RoleSerializationContext.Active) + return true; - - if (RoleSerializationContext.Active) - return true; + __result = role; + return false; + } +} - __result = role; - return false; - } +internal static class TeamFakeContext +{ + [ThreadStatic] private static int _depth; + + internal static bool Active => _depth > 0; + + internal static void Enter() + { + _depth++; } - - internal static class TeamFakeContext + + internal static void Exit() { - [ThreadStatic] private static int _depth; + if (_depth > 0) + _depth--; + } +} - internal static bool Active => _depth > 0; +internal static class RoleSerializationContext +{ + [ThreadStatic] private static int _depth; - internal static void Enter() => _depth++; + internal static bool Active => _depth > 0; - internal static void Exit() - { - if (_depth > 0) - _depth--; - } + internal static void Enter() + { + _depth++; + } + + internal static void Exit() + { + if (_depth > 0) + _depth--; } - - internal static class RoleSerializationContext +} + +[HarmonyPatchCategory(TeamPatchManager.Category)] +[HarmonyPatch(typeof(RoleSyncInfo), MethodType.Constructor, typeof(ReferenceHub), typeof(RoleTypeId), + typeof(ReferenceHub), typeof(NetworkWriter))] +internal class RoleSyncInfoCtorPatch +{ + private static void Prefix() { - [ThreadStatic] private static int _depth; + RoleSerializationContext.Enter(); + } - internal static bool Active => _depth > 0; + private static void Finalizer() + { + RoleSerializationContext.Exit(); + } +} - internal static void Enter() => _depth++; +[HarmonyPatchCategory(TeamPatchManager.Category)] +[HarmonyPatch(typeof(PlayerRolesUtils), nameof(PlayerRolesUtils.GetRoleId))] +internal class PlayerRolesUtilsPatch +{ + private static readonly Dictionary _roleTeam = new() + { + { Team.ClassD, RoleTypeId.ClassD }, + { Team.SCPs, RoleTypeId.Scp0492 }, + { Team.Scientists, RoleTypeId.Scientist }, + { Team.ChaosInsurgency, RoleTypeId.ChaosConscript }, + { Team.FoundationForces, RoleTypeId.NtfPrivate }, + { Team.Flamingos, RoleTypeId.Flamingo }, + { Team.OtherAlive, RoleTypeId.Tutorial } + }; + + private static bool Prefix(ReferenceHub hub, ref RoleTypeId __result) + { + if (hub == null || !TeamFakeContext.Active) + return true; - internal static void Exit() + if (!DisguiseTeam.List.TryGetValue(hub.PlayerId, out var team)) + return true; + + if (_roleTeam.TryGetValue(team, out var fakeRole)) { - if (_depth > 0) - _depth--; + __result = fakeRole; + return false; } + + return true; } - [HarmonyPatchCategory(TeamPatchManager.Category)] - [HarmonyPatch(typeof(RoleSyncInfo), MethodType.Constructor, new[] { typeof(ReferenceHub), typeof(RoleTypeId), typeof(ReferenceHub), typeof(NetworkWriter) })] - internal class RoleSyncInfoCtorPatch + internal static RoleTypeId GetCombatRoleId(ReferenceHub hub) { - static void Prefix() => RoleSerializationContext.Enter(); + if (hub != null && DisguiseTeam.List.TryGetValue(hub.PlayerId, out var team) && + _roleTeam.TryGetValue(team, out var fakeRole)) + return fakeRole; - static void Finalizer() => RoleSerializationContext.Exit(); + return hub.GetRoleId(); } +} - [HarmonyPatchCategory(TeamPatchManager.Category)] - [HarmonyPatch(typeof(PlayerRolesUtils), nameof(PlayerRolesUtils.GetRoleId))] - internal class PlayerRolesUtilsPatch +[HarmonyPatchCategory(TeamPatchManager.Category)] +[HarmonyPatch(typeof(AttackerDamageHandler), nameof(AttackerDamageHandler.ProcessDamage))] +internal class ProcessDamageRolePatch +{ + private static IEnumerable Transpiler(IEnumerable instructions) { - private static readonly Dictionary _roleTeam = new() - { - { Team.ClassD, RoleTypeId.ClassD }, - { Team.SCPs, RoleTypeId.Scp0492 }, - { Team.Scientists, RoleTypeId.Scientist }, - { Team.ChaosInsurgency, RoleTypeId.ChaosConscript }, - { Team.FoundationForces, RoleTypeId.NtfPrivate }, - { Team.Flamingos, RoleTypeId.Flamingo }, - { Team.OtherAlive, RoleTypeId.Tutorial } - }; - - static bool Prefix(ReferenceHub hub, ref RoleTypeId __result) - { - if (hub == null || !TeamFakeContext.Active) - return true; + List code = new(instructions); + var original = Method(typeof(PlayerRolesUtils), nameof(PlayerRolesUtils.GetRoleId), [typeof(ReferenceHub)]); + var replacement = Method(typeof(PlayerRolesUtilsPatch), nameof(PlayerRolesUtilsPatch.GetCombatRoleId)); - if (!DisguiseTeam.List.TryGetValue(hub.PlayerId, out Team team)) - return true; + foreach (var instruction in code) + if (instruction.opcode == OpCodes.Call && instruction.operand is MethodInfo method && method == original) + instruction.operand = replacement; - if (_roleTeam.TryGetValue(team, out RoleTypeId fakeRole)) - { - __result = fakeRole; - return false; - } - - return true; - } - - internal static RoleTypeId GetCombatRoleId(ReferenceHub hub) - { - if (hub != null && DisguiseTeam.List.TryGetValue(hub.PlayerId, out Team team) && - _roleTeam.TryGetValue(team, out RoleTypeId fakeRole)) - return fakeRole; - - return hub.GetRoleId(); - } + return code; } +} - [HarmonyPatchCategory(TeamPatchManager.Category)] - [HarmonyPatch(typeof(AttackerDamageHandler), nameof(AttackerDamageHandler.ProcessDamage))] - internal class ProcessDamageRolePatch +[HarmonyPatchCategory(TeamPatchManager.Category)] +[HarmonyPatch] +internal class TeamFakeContextPatch +{ + private static IEnumerable TargetMethods() { - static IEnumerable Transpiler(IEnumerable instructions) - { - List code = new(instructions); - MethodInfo original = Method(typeof(PlayerRolesUtils), nameof(PlayerRolesUtils.GetRoleId), [typeof(ReferenceHub)]); - MethodInfo replacement = Method(typeof(PlayerRolesUtilsPatch), nameof(PlayerRolesUtilsPatch.GetCombatRoleId)); + return Declared(typeof(HitboxIdentity), nameof(HitboxIdentity.IsEnemy)) + .Concat(Declared(typeof(GeneralKillsHandler), nameof(GeneralKillsHandler.HandleAttackerKill))) + .Concat(Declared(typeof(TerminationRewards), nameof(TerminationRewards.EvaluateGainReason))) + .Concat(Declared(typeof(MimicryRecorder), nameof(MimicryRecorder.WasKilledByTeammate))) + .Concat(Declared(typeof(ExplosionGrenade), nameof(ExplosionGrenade.Explode))) + .Concat(Declared(typeof(FlashbangGrenade), nameof(FlashbangGrenade.ServerFuseEnd))) + .Concat(Declared(typeof(AttackerDamageHandler), nameof(AttackerDamageHandler.ProcessDamage))) + .Concat(Declared(typeof(LastHumanTracker), nameof(LastHumanTracker.IsLastTarget))) + .Concat(Declared(typeof(Scp079Recontainer), nameof(Scp079Recontainer.OnServerRoleChanged))); + } - foreach (CodeInstruction instruction in code) - if (instruction.opcode == OpCodes.Call && instruction.operand is MethodInfo method && method == original) - instruction.operand = replacement; + private static IEnumerable Declared(Type type, string name) + { + return type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | + BindingFlags.Static | BindingFlags.DeclaredOnly) + .Where(m => m.Name == name && !m.IsAbstract && !m.ContainsGenericParameters); + } - return code; - } + private static void Prefix() + { + TeamFakeContext.Enter(); } - [HarmonyPatchCategory(TeamPatchManager.Category)] - [HarmonyPatch] - internal class TeamFakeContextPatch + private static void Finalizer() { - static IEnumerable TargetMethods() => - Declared(typeof(HitboxIdentity), nameof(HitboxIdentity.IsEnemy)) - .Concat(Declared(typeof(GeneralKillsHandler), nameof(GeneralKillsHandler.HandleAttackerKill))) - .Concat(Declared(typeof(TerminationRewards), nameof(TerminationRewards.EvaluateGainReason))) - .Concat(Declared(typeof(MimicryRecorder), nameof(MimicryRecorder.WasKilledByTeammate))) - .Concat(Declared(typeof(ExplosionGrenade), nameof(ExplosionGrenade.Explode))) - .Concat(Declared(typeof(FlashbangGrenade), nameof(FlashbangGrenade.ServerFuseEnd))) - .Concat(Declared(typeof(AttackerDamageHandler), nameof(AttackerDamageHandler.ProcessDamage))) - .Concat(Declared(typeof(LastHumanTracker), nameof(LastHumanTracker.IsLastTarget))) - .Concat(Declared(typeof(Scp079Recontainer), nameof(Scp079Recontainer.OnServerRoleChanged))); - - static IEnumerable Declared(Type type, string name) => - type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly) - .Where(m => m.Name == name && !m.IsAbstract && !m.ContainsGenericParameters); - - static void Prefix() => TeamFakeContext.Enter(); - - static void Finalizer() => TeamFakeContext.Exit(); + TeamFakeContext.Exit(); } +} - [HarmonyPatchCategory(TeamPatchManager.Category)] - [HarmonyPatch(typeof(Footprint), MethodType.Constructor, new[] { typeof(ReferenceHub) })] - internal class FootprintContextPatch +[HarmonyPatchCategory(TeamPatchManager.Category)] +[HarmonyPatch(typeof(Footprint), MethodType.Constructor, typeof(ReferenceHub))] +internal class FootprintContextPatch +{ + private static void Prefix() { - static void Prefix() => TeamFakeContext.Enter(); + TeamFakeContext.Enter(); + } - static void Finalizer() => TeamFakeContext.Exit(); + private static void Finalizer() + { + TeamFakeContext.Exit(); } +} - [HarmonyPatchCategory(TeamPatchManager.Category)] - [HarmonyPatch(typeof(ExplosionGrenade), nameof(ExplosionGrenade.ExplodeDestructible))] - internal class GrenadeTranspiler +[HarmonyPatchCategory(TeamPatchManager.Category)] +[HarmonyPatch(typeof(ExplosionGrenade), nameof(ExplosionGrenade.ExplodeDestructible))] +internal class GrenadeTranspiler +{ + private static IEnumerable Transpiler(IEnumerable instructions) { - static IEnumerable Transpiler(IEnumerable instructions) - { - List newInstructions = new(instructions); - int index = -1; + List newInstructions = new(instructions); + var index = -1; - for (int i = 0; i < newInstructions.Count; i++) + for (var i = 0; i < newInstructions.Count; i++) + if (newInstructions[i].opcode == OpCodes.Call && newInstructions[i].operand is MethodInfo method && + method == Method(typeof(PlayerRolesUtils), nameof(PlayerRolesUtils.GetRoleId), + [typeof(ReferenceHub)])) { - if (newInstructions[i].opcode == OpCodes.Call && newInstructions[i].operand is MethodInfo method && method == Method(typeof(PlayerRolesUtils), nameof(PlayerRolesUtils.GetRoleId), new Type[] { typeof(ReferenceHub) })) - { - index = i; - break; - } + index = i; + break; } - newInstructions[index+1].operand = Method(typeof(PlayerRolesUtils), nameof(PlayerRolesUtils.GetTeam), [typeof(ReferenceHub)]); - newInstructions.RemoveAt(index); - + if (index is -1 || index + 1 >= newInstructions.Count) + { + LogManager.Error( + "GrenadeTranspiler could not find the expected GetRoleId call inside ExplosionGrenade.ExplodeDestructible - the method is left unpatched. Grenade friendly-fire checks may ignore fake teams."); return newInstructions; } + + newInstructions[index + 1].operand = Method(typeof(PlayerRolesUtils), nameof(PlayerRolesUtils.GetTeam), + [typeof(ReferenceHub)]); + newInstructions.RemoveAt(index); + + return newInstructions; + } +} + +[HarmonyPatchCategory(TeamPatchManager.Category)] +[HarmonyPatch(typeof(PickupSearchCompletor), nameof(PickupSearchCompletor.ValidateAny))] +public class PickupSearchCompletorPatch +{ + private static bool Prefix(PickupSearchCompletor __instance, ref bool __result) + { + if (!DisguiseTeam.List.TryGetValue(__instance.Hub.PlayerId, out var team) || team != Team.SCPs || + __instance.Hub.roleManager.CurrentRole.RoleTypeId.GetTeam() == Team.SCPs) return true; + __result = !__instance.TargetPickup.Info.Locked && !__instance.Hub.inventory.IsDisarmed() && + !__instance.Hub.interCoordinator.AnyBlocker(BlockedInteraction.GrabItems); + return false; } +} - [HarmonyPatchCategory(TeamPatchManager.Category)] - [HarmonyPatch(typeof(PickupSearchCompletor), nameof(PickupSearchCompletor.ValidateAny))] - public class PickupSearchCompletorPatch +[HarmonyPatchCategory(TeamPatchManager.Category)] +[HarmonyPatch] +public class DoorPermissionsPolicyPatch +{ + private static MethodBase TargetMethod() { - static bool Prefix(PickupSearchCompletor __instance, ref bool __result) - { - if (!DisguiseTeam.List.TryGetValue(__instance.Hub.PlayerId, out Team team) || team != Team.SCPs || - __instance.Hub.roleManager.CurrentRole.RoleTypeId.GetTeam() == Team.SCPs) return true; - __result = !__instance.TargetPickup.Info.Locked && !__instance.Hub.inventory.IsDisarmed() && - !__instance.Hub.interCoordinator.AnyBlocker(BlockedInteraction.GrabItems); - return false; - } + return Method(typeof(DoorPermissionsPolicy), "CheckPermissions", [ + typeof(ReferenceHub), typeof(IDoorPermissionRequester), typeof(PermissionUsed).MakeByRefType() + ]); } - - [HarmonyPatchCategory(TeamPatchManager.Category)] - [HarmonyPatch] - public class DoorPermissionsPolicyPatch + + private static bool Prefix(DoorPermissionsPolicy __instance, ReferenceHub hub, IDoorPermissionRequester requester, + out PermissionUsed callback, ref bool __result) { - static MethodBase TargetMethod() + callback = null; + if (__instance.RequiredPermissions == DoorPermissionFlags.None || hub.serverRoles.BypassMode) { - return Method(typeof(DoorPermissionsPolicy), "CheckPermissions", new[] { typeof(ReferenceHub), typeof(IDoorPermissionRequester), typeof(PermissionUsed).MakeByRefType() }); + __result = true; + return false; } - static bool Prefix(DoorPermissionsPolicy __instance, ReferenceHub hub, IDoorPermissionRequester requester, out PermissionUsed callback, ref bool __result) + if (hub.roleManager.CurrentRole is IDoorPermissionProvider currentRole && + (!DisguiseTeam.List.TryGetValue(hub.PlayerId, out var team) || team != Team.SCPs)) { - callback = null; - if (__instance.RequiredPermissions == DoorPermissionFlags.None || hub.serverRoles.BypassMode) - { - __result = true; - return false; - } - if (hub.roleManager.CurrentRole is IDoorPermissionProvider currentRole && - (!DisguiseTeam.List.TryGetValue(hub.PlayerId, out Team team) || team != Team.SCPs)) - { - __result = __instance.CheckPermissions(currentRole, requester, out callback); - return false; - } - ItemBase curInstance = hub.inventory.CurInstance; - __result = curInstance != null && curInstance is IDoorPermissionProvider provider && __instance.CheckPermissions(provider, requester, out callback); + __result = __instance.CheckPermissions(currentRole, requester, out callback); return false; } + + var curInstance = hub.inventory.CurInstance; + __result = curInstance != null && curInstance is IDoorPermissionProvider provider && + __instance.CheckPermissions(provider, requester, out callback); + return false; } - - [HarmonyPatchCategory(TeamPatchManager.Category)] - [HarmonyPatch(typeof(DoorPermissionsPolicyExtensions), nameof(DoorPermissionsPolicyExtensions.GetCombinedPermissions))] - public class DoorPermissionsPolicyExtensionsPatch +} + +[HarmonyPatchCategory(TeamPatchManager.Category)] +[HarmonyPatch(typeof(DoorPermissionsPolicyExtensions), nameof(DoorPermissionsPolicyExtensions.GetCombinedPermissions))] +public class DoorPermissionsPolicyExtensionsPatch +{ + private static bool Prefix(ReferenceHub hub, IDoorPermissionRequester requester, ref DoorPermissionFlags __result) { - static bool Prefix(ReferenceHub hub, IDoorPermissionRequester requester, ref DoorPermissionFlags __result) + if (hub == null) { - if (hub == null) - { - __result = DoorPermissionFlags.None; - return false; - } + __result = DoorPermissionFlags.None; + return false; + } - if (hub.serverRoles.BypassMode) - { - __result = DoorPermissionFlags.All; - return false; - } + if (hub.serverRoles.BypassMode) + { + __result = DoorPermissionFlags.All; + return false; + } - DoorPermissionFlags combinedPermissions = DoorPermissionFlags.None; + var combinedPermissions = DoorPermissionFlags.None; - if (hub.roleManager.CurrentRole is IDoorPermissionProvider currentRole && - (!DisguiseTeam.List.TryGetValue(hub.PlayerId, out Team team) || team != Team.SCPs)) - combinedPermissions |= currentRole.GetPermissions(requester); + if (hub.roleManager.CurrentRole is IDoorPermissionProvider currentRole && + (!DisguiseTeam.List.TryGetValue(hub.PlayerId, out var team) || team != Team.SCPs)) + combinedPermissions |= currentRole.GetPermissions(requester); - ItemBase curInstance = hub.inventory.CurInstance; - if (curInstance != null && curInstance is IDoorPermissionProvider permissionProvider) - combinedPermissions |= permissionProvider.GetPermissions(requester); + var curInstance = hub.inventory.CurInstance; + if (curInstance != null && curInstance is IDoorPermissionProvider permissionProvider) + combinedPermissions |= permissionProvider.GetPermissions(requester); - __result = combinedPermissions; - return false; - } + __result = combinedPermissions; + return false; } +} - [HarmonyPatchCategory(TeamPatchManager.Category)] - [HarmonyPatch(typeof(PlayerRolesUtils), nameof(PlayerRolesUtils.IsSCP), new[] { typeof(ReferenceHub), typeof(bool) })] - internal class IsScpPatch +[HarmonyPatchCategory(TeamPatchManager.Category)] +[HarmonyPatch(typeof(PlayerRolesUtils), nameof(PlayerRolesUtils.IsSCP), typeof(ReferenceHub), typeof(bool))] +internal class IsScpPatch +{ + private static bool Prefix(ReferenceHub hub, ref bool __result) { - static bool Prefix(ReferenceHub hub, ref bool __result) - { - if (hub == null || !DisguiseTeam.List.TryGetValue(hub.PlayerId, out Team team)) - return true; + if (hub == null || !DisguiseTeam.List.TryGetValue(hub.PlayerId, out var team)) + return true; - __result = team == Team.SCPs; - return false; - } + __result = team == Team.SCPs; + return false; } +} - [HarmonyPatchCategory(TeamPatchManager.Category)] - [HarmonyPatch(typeof(PlayerRolesUtils), nameof(PlayerRolesUtils.IsHuman), new[] { typeof(ReferenceHub) })] - internal class IsHumanPatch +[HarmonyPatchCategory(TeamPatchManager.Category)] +[HarmonyPatch(typeof(PlayerRolesUtils), nameof(PlayerRolesUtils.IsHuman), typeof(ReferenceHub))] +internal class IsHumanPatch +{ + private static bool Prefix(ReferenceHub hub, ref bool __result) { - static bool Prefix(ReferenceHub hub, ref bool __result) - { - if (hub == null || !DisguiseTeam.List.TryGetValue(hub.PlayerId, out Team team)) - return true; + if (hub == null || !DisguiseTeam.List.TryGetValue(hub.PlayerId, out var team)) + return true; - __result = team != Team.SCPs && team != Team.Dead && team != Team.Flamingos; - return false; - } + __result = team != Team.SCPs && team != Team.Dead && team != Team.Flamingos; + return false; } +} - [HarmonyPatchCategory(TeamPatchManager.Category)] - [HarmonyPatch(typeof(Scp079Recontainer), nameof(Scp079Recontainer.OnServerRoleChanged))] - public class Scp079RecontainerPatch +[HarmonyPatchCategory(TeamPatchManager.Category)] +[HarmonyPatch(typeof(Scp079Recontainer), nameof(Scp079Recontainer.OnServerRoleChanged))] +public class Scp079RecontainerPatch +{ + private static bool Prefix(Scp079Recontainer __instance, ReferenceHub hub, RoleTypeId newRole, + RoleChangeReason reason) { - static bool Prefix(Scp079Recontainer __instance, ReferenceHub hub, RoleTypeId newRole, RoleChangeReason reason) - { - Team team = hub.GetRoleId().GetTeam(); - if (DisguiseTeam.List.TryGetValue(hub.PlayerId, out Team t)) - team = t; - if (newRole != RoleTypeId.Spectator || !IsScpButNot079(hub.GetRoleId(), team) || Scp079Role.ActiveInstances.Count == 0 || - ReferenceHub.AllHubs.Any(x => - { - if (x == hub) - return false; - - Team effectiveTeam = x.GetRoleId().GetTeam(); - if (DisguiseTeam.List.TryGetValue(x.PlayerId, out Team fakeTeam)) - effectiveTeam = fakeTeam; - - return IsScpButNot079(x.GetRoleId(), effectiveTeam); - })) - return false; - __instance.SetContainmentDoors(true, true); - __instance.Recontain(true); - foreach (Scp079Generator allGenerator in Scp079Recontainer.AllGenerators) - allGenerator.Engaged = true; + var team = hub.GetRoleId().GetTeam(); + if (DisguiseTeam.List.TryGetValue(hub.PlayerId, out var t)) + team = t; + if (newRole != RoleTypeId.Spectator || !IsScpButNot079(hub.GetRoleId(), team) || + Scp079Role.ActiveInstances.Count == 0 || + ReferenceHub.AllHubs.Any(x => + { + if (x == hub) + return false; + + var effectiveTeam = x.GetRoleId().GetTeam(); + if (DisguiseTeam.List.TryGetValue(x.PlayerId, out var fakeTeam)) + effectiveTeam = fakeTeam; + + return IsScpButNot079(x.GetRoleId(), effectiveTeam); + })) return false; - } + __instance.SetContainmentDoors(true, true); + __instance.Recontain(true); + foreach (var allGenerator in Scp079Recontainer.AllGenerators) + allGenerator.Engaged = true; + return false; + } - private static bool IsScpButNot079(RoleTypeId roleTypeId, Team team) - { - return team == Team.SCPs && roleTypeId != RoleTypeId.Scp079; - } + private static bool IsScpButNot079(RoleTypeId roleTypeId, Team team) + { + return team == Team.SCPs && roleTypeId != RoleTypeId.Scp079; } } \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Patches/TeamPatchManager.cs b/UncomplicatedCustomRoles/Patches/TeamPatchManager.cs index 91b4ba2..ba9b7b2 100644 --- a/UncomplicatedCustomRoles/Patches/TeamPatchManager.cs +++ b/UncomplicatedCustomRoles/Patches/TeamPatchManager.cs @@ -12,92 +12,89 @@ using HarmonyLib; using UncomplicatedCustomRoles.Manager; -namespace UncomplicatedCustomRoles.Patches +namespace UncomplicatedCustomRoles.Patches; + +internal static class TeamPatchManager { - internal static class TeamPatchManager - { - internal const string Category = "UncomplicatedCustomRoles.DynamicTeamPatch"; + internal const string Category = "UncomplicatedCustomRoles.DynamicTeamPatch"; + + private static readonly object Sync = new(); - private static readonly object Sync = new(); + private static Harmony _harmony; - private static Harmony _harmony; + private static bool IsPatched { get; set; } - private static bool IsPatched { get; set; } - - internal static void Initialize() + internal static void Initialize() + { + lock (Sync) { - lock (Sync) - { - _harmony = new Harmony($"com.ucs.ucr_labapi.teampatch-{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}"); - IsPatched = false; - } + _harmony = new Harmony($"com.ucs.ucr_labapi.teampatch-{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}"); + IsPatched = false; } - - internal static void EnsurePatched() + } + + internal static void EnsurePatched() + { + if (IsPatched) + return; + + lock (Sync) { - if (IsPatched) + if (IsPatched || _harmony is null) return; - lock (Sync) + try { - if (IsPatched || _harmony is null) - return; - - try - { - _harmony.PatchCategory(Plugin.Assembly, Category); - IsPatched = true; - LogManager.Debug("Dynamic team patches applied - at least one player is now disguised."); - } - catch (Exception e) - { - LogManager.Error($"Failed to apply the dynamic team patches: {e}"); - } + _harmony.PatchCategory(Plugin.Assembly, Category); + IsPatched = true; + LogManager.Debug("Dynamic team patches applied - at least one player is now disguised."); + } + catch (Exception e) + { + LogManager.Error($"Failed to apply the dynamic team patches: {e}"); } } - - internal static void EnsureUnpatched() + } + + internal static void EnsureUnpatched() + { + if (!IsPatched) + return; + + lock (Sync) { - if (!IsPatched) + if (!IsPatched || _harmony is null) return; - lock (Sync) + try + { + _harmony.UnpatchCategory(Plugin.Assembly, Category); + IsPatched = false; + LogManager.Debug("Dynamic team patches removed - nobody is disguised anymore."); + } + catch (Exception e) { - if (!IsPatched || _harmony is null) - return; + LogManager.Error($"Failed to remove the dynamic team patches: {e}"); + } + } + } + internal static void Shutdown() + { + lock (Sync) + { + if (_harmony is not null && IsPatched) try { _harmony.UnpatchCategory(Plugin.Assembly, Category); - IsPatched = false; - LogManager.Debug("Dynamic team patches removed - nobody is disguised anymore."); } catch (Exception e) { - LogManager.Error($"Failed to remove the dynamic team patches: {e}"); - } - } - } - - internal static void Shutdown() - { - lock (Sync) - { - if (_harmony is not null && IsPatched) - { - try - { - _harmony.UnpatchCategory(Plugin.Assembly, Category); - } - catch (Exception e) - { - LogManager.Error($"Failed to remove the dynamic team patches during shutdown: {e}"); - } + LogManager.Error($"Failed to remove the dynamic team patches during shutdown: {e}"); } - _harmony = null; - IsPatched = false; - } + _harmony = null; + IsPatched = false; } } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Plugin.cs b/UncomplicatedCustomRoles/Plugin.cs index 235de39..b6086d0 100644 --- a/UncomplicatedCustomRoles/Plugin.cs +++ b/UncomplicatedCustomRoles/Plugin.cs @@ -1,8 +1,8 @@ /* * This file is a part of the UncomplicatedCustomRoles project. - * + * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * + * * This file is licensed under the GNU Affero General Public License v3.0. * You should have received a copy of the AGPL license along with this file. * If not, see . @@ -10,130 +10,132 @@ using System; using System.Collections.Generic; -using UncomplicatedCustomRoles.Integrations; -using UncomplicatedCustomRoles.Manager; -using UncomplicatedCustomRoles.API.Features; -using HarmonyLib; -using UncomplicatedCustomRoles.Manager.NET; -using UncomplicatedCustomRoles.Patches; +using System.Reflection; using System.Threading.Tasks; +using HarmonyLib; +using LabApi.Features; +using LabApi.Features.Wrappers; using LabApi.Loader.Features.Plugins; using LabApi.Loader.Features.Plugins.Enums; -using LabApi.Features.Wrappers; -using System.Reflection; -using LabApi.Features; using MEC; -using UncomplicatedCustomRoles.Events; +using UncomplicatedCustomRoles.API.Features; using UncomplicatedCustomRoles.API.Features.Controllers; +using UncomplicatedCustomRoles.Events; +using UncomplicatedCustomRoles.Integrations; +using UncomplicatedCustomRoles.Manager; +using UncomplicatedCustomRoles.Manager.NET; +using UncomplicatedCustomRoles.Patches; -namespace UncomplicatedCustomRoles -{ - internal class Plugin : Plugin - { - public override string Name => "UncomplicatedCustomRoles"; - - public override string Description => "Customize your SCP:SL server with Custom Roles!"; +namespace UncomplicatedCustomRoles; - public override string Author => "FoxWorn3365, Dr.Agenda, MedveMarci"; +internal class Plugin : Plugin +{ + internal static Plugin Instance; - public override Version Version { get; } = new(9, 5, 1, 0); + internal static HttpManager HttpManager; - public override Version RequiredApiVersion => new(LabApiProperties.CompiledVersion); + private Harmony _harmony; + public override string Name => "UncomplicatedCustomRoles"; - public override LoadPriority Priority => LoadPriority.High; + public override string Description => "Customize your SCP:SL server with Custom Roles!"; - public static Assembly Assembly => Assembly.GetExecutingAssembly(); + public override string Author => "FoxWorn3365, Dr.Agenda, MedveMarci"; - internal static Plugin Instance; + public override Version Version { get; } = new(9, 5, 1, 0); - internal static HttpManager HttpManager; + public override Version RequiredApiVersion => new(LabApiProperties.CompiledVersion); - private Harmony _harmony; + public override LoadPriority Priority => LoadPriority.High; - public override void Enable() - { - Instance = this; + public static Assembly Assembly => Assembly.GetExecutingAssembly(); - // QoL things - LogManager.History.Clear(); - API.Features.Escape.Bucket.Clear(); + public override void Enable() + { + Instance = this; - HttpManager = new("ucr"); + // QoL things + LogManager.History.Clear(); + API.Features.Escape.Bucket.Clear(); - CustomRole.CustomRoles.Clear(); - CustomRole.NotLoadedRoles.Clear(); + HttpManager = new HttpManager("ucr"); - EventHandlerBase.Register(new List() - { - new ServerEventHandler(), - new PlayerEventHandler(), - new ScpEventHandler() - }); + CustomRole.CustomRoles.Clear(); + CustomRole.NotLoadedRoles.Clear(); - Task.Run(delegate - { - if (HttpManager.LatestVersion.CompareTo(Version) > 0) - LogManager.Warn($"You are NOT using the latest version of UncomplicatedCustomRoles!\nCurrent: v{Version} | Latest available: v{HttpManager.LatestVersion}\nDownload it from GitHub: https://github.com/FoxWorn3365/UncomplicatedCustomRoles/releases/latest"); + EventHandlerBase.Register(new List + { + new ServerEventHandler(), + new PlayerEventHandler(), + new ScpEventHandler() + }); - VersionManager.Init(); - }); + Task.Run(delegate + { + if (HttpManager.LatestVersion.CompareTo(Version) > 0) + LogManager.Warn( + $"You are NOT using the latest version of UncomplicatedCustomRoles!\nCurrent: v{Version} | Latest available: v{HttpManager.LatestVersion}\nDownload it from GitHub: https://github.com/FoxWorn3365/UncomplicatedCustomRoles/releases/latest"); - ImportManager.Unload(); + VersionManager.Init(); + }); - FileConfigs.Welcome(); - FileConfigs.Welcome(Server.Port.ToString()); - FileConfigs.LoadAll(); - FileConfigs.LoadAll(Server.Port.ToString()); + ImportManager.Unload(); - SpawnPointApiCommunicator.Init(); + FileConfigs.Welcome(); + FileConfigs.Welcome(Server.Port.ToString()); + FileConfigs.LoadAll(); + FileConfigs.LoadAll(Server.Port.ToString()); - DisguiseTeam.Clear(); - - TeamPatchManager.Initialize(); + SpawnPointApiCommunicator.Init(); - _harmony = new($"com.ucs.ucr_labapi-{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}"); - _harmony.PatchAllUncategorized(); + DisguiseTeam.Clear(); - PlayerEventPrefix.Patch(_harmony); + TeamPatchManager.Initialize(); - // Add presence - if (Config.EnableTelemetry) - Timing.RunCoroutine(Presence.PresenceCoroutine(), "UCR_Presence"); - } + _harmony = new Harmony($"com.ucs.ucr_labapi-{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}"); + _harmony.PatchAllUncategorized(); - public override void Disable() - { - Timing.KillCoroutines("UCR_Presence"); + PlayerEventPrefix.Patch(_harmony); - ScriptedEvents.UnregisterCustomActions(); + // Add presence + if (Config.EnableTelemetry) + Timing.RunCoroutine(Presence.PresenceCoroutine(), "UCR_Presence"); + } - PlayerEventPrefix.Unpatch(_harmony); + public override void Disable() + { + Timing.KillCoroutines("UCR_Presence"); - _harmony.UnpatchAll(); + ScriptedEvents.UnregisterCustomActions(); - TeamPatchManager.Shutdown(); + PlayerEventPrefix.Unpatch(_harmony); - EventHandlerBase.UnregisterAll(); + _harmony.UnpatchAll(_harmony.Id); - HttpManager.UnregisterEvents(); + TeamPatchManager.Shutdown(); - Instance = null; - } + EventHandlerBase.UnregisterAll(); - /// - /// Invoked after the server finish to load every plugin - /// - public void OnFinishedLoadingPlugins() - { - // Register ScriptedEvents integration - ScriptedEvents.RegisterCustomActions(); + HttpManager.UnregisterEvents(); - // Run the import managet - ImportManager.Init(); + Instance = null; + } - if (Config is not { EnableBasicLogs: true }) return; - LogManager.Info($"Thanks for using UncomplicatedCustomRoles v{Version.ToString(3)} by {Author}!", ConsoleColor.Blue); - LogManager.Info("To receive support and to stay up-to-date, join our official Discord server: https://discord.gg/5StRGu8EJV", ConsoleColor.DarkYellow); - } + /// + /// Invoked after the server finish to load every plugin + /// + public void OnFinishedLoadingPlugins() + { + // Register ScriptedEvents integration + ScriptedEvents.RegisterCustomActions(); + + // Run the import managet + ImportManager.Init(); + + if (Config is not { EnableBasicLogs: true }) return; + LogManager.Info($"Thanks for using UncomplicatedCustomRoles v{Version.ToString(3)} by {Author}!", + ConsoleColor.Blue); + LogManager.Info( + "To receive support and to stay up-to-date, join our official Discord server: https://discord.gg/5StRGu8EJV", + ConsoleColor.DarkYellow); } } \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Properties/AssemblyInfo.cs b/UncomplicatedCustomRoles/Properties/AssemblyInfo.cs index 689d43c..6d334b3 100644 --- a/UncomplicatedCustomRoles/Properties/AssemblyInfo.cs +++ b/UncomplicatedCustomRoles/Properties/AssemblyInfo.cs @@ -32,4 +32,4 @@ // usando l'asterisco '*' come illustrato di seguito: // [assembly: AssemblyVersion("1.0.*")] [assembly: AssemblyVersion("9.5.1.0")] -[assembly: AssemblyFileVersion("9.5.1.0")] +[assembly: AssemblyFileVersion("9.5.1.0")] \ No newline at end of file diff --git a/UncomplicatedCustomRoles/UncomplicatedCustomRoles.csproj b/UncomplicatedCustomRoles/UncomplicatedCustomRoles.csproj index 72cf865..0c1d251 100644 --- a/UncomplicatedCustomRoles/UncomplicatedCustomRoles.csproj +++ b/UncomplicatedCustomRoles/UncomplicatedCustomRoles.csproj @@ -30,9 +30,9 @@ - - - + + + diff --git a/UncomplicatedCustomRoles/app.config b/UncomplicatedCustomRoles/app.config index 71c4034..a0d1004 100644 --- a/UncomplicatedCustomRoles/app.config +++ b/UncomplicatedCustomRoles/app.config @@ -1,15 +1,18 @@ - - - - - - - - - - - - - + + + + + + + + + + + + + + + + From e70ec36ecc0baa37ccd2f8f1ef6b1f30e433743f Mon Sep 17 00:00:00 2001 From: MedveMarci Date: Sun, 12 Jul 2026 21:31:34 +0200 Subject: [PATCH 12/47] Added null checks for PlaceholderManager --- UncomplicatedCustomRoles/Manager/PlaceholderManager.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/UncomplicatedCustomRoles/Manager/PlaceholderManager.cs b/UncomplicatedCustomRoles/Manager/PlaceholderManager.cs index e2519f5..7da2957 100644 --- a/UncomplicatedCustomRoles/Manager/PlaceholderManager.cs +++ b/UncomplicatedCustomRoles/Manager/PlaceholderManager.cs @@ -36,9 +36,9 @@ public static string ApplyPlaceholders(string? origin, Player player, ICustomRol : string.Empty }, { "rolename", player.Role.GetFullName() }, - { "customrolename", role?.Name }, - { "customroleid", role?.Id }, - { "customrolebadge", role?.BadgeName }, + { "customrolename", role?.Name ?? string.Empty }, + { "customroleid", role?.Id ?? 0 }, + { "customrolebadge", role?.BadgeName ?? string.Empty }, { "health", player.Health }, { "max_health", player.MaxHealth }, { "ahp", player.ArtificialHealth }, From 625aed06927802ca6bb11c5dc2c5b4d68bbca798 Mon Sep 17 00:00:00 2001 From: MedveMarci Date: Mon, 13 Jul 2026 12:54:07 +0200 Subject: [PATCH 13/47] Renamed colorMap to ColorMap for consistency; Added white color --- UncomplicatedCustomRoles/API/Features/SummonedCustomRole.cs | 2 +- UncomplicatedCustomRoles/Commands/Info.cs | 2 +- UncomplicatedCustomRoles/Manager/RoleValidator.cs | 4 ++-- UncomplicatedCustomRoles/Manager/SpawnManager.cs | 3 ++- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/UncomplicatedCustomRoles/API/Features/SummonedCustomRole.cs b/UncomplicatedCustomRoles/API/Features/SummonedCustomRole.cs index 8525741..b2c81f9 100644 --- a/UncomplicatedCustomRoles/API/Features/SummonedCustomRole.cs +++ b/UncomplicatedCustomRoles/API/Features/SummonedCustomRole.cs @@ -740,7 +740,7 @@ public static void TryParseRemoteAdmin(ReferenceHub player, StringBuilder builde if (Plugin.HttpManager.Credits.TryGetValue(player.authManager.UserId, out var tag) && !string.IsNullOrEmpty(tag.First) && !string.IsNullOrEmpty(tag.Second)) { - if (!SpawnManager.colorMap.TryGetValue(tag.Second, out var tagColor)) + if (!SpawnManager.ColorMap.TryGetValue(tag.Second, out var tagColor)) tagColor = "white"; if (Plugin.HttpManager.IsJobRole.Contains(player.authManager.UserId)) diff --git a/UncomplicatedCustomRoles/Commands/Info.cs b/UncomplicatedCustomRoles/Commands/Info.cs index a8e9fe5..4e95452 100644 --- a/UncomplicatedCustomRoles/Commands/Info.cs +++ b/UncomplicatedCustomRoles/Commands/Info.cs @@ -57,7 +57,7 @@ public static string BuildInfo(ICustomRole role) { "👤 Role:", $"{role.Role}" }, { "💳 Badge:", - $"{(role.BadgeName != null ? role.BadgeName.Replace("@hidden", string.Empty) : string.Empty)}{(role.BadgeName != null && role.BadgeName.EndsWith("@hidden") ? " [HIDDEN]" : string.Empty)}" + $"{(role.BadgeName != null ? role.BadgeName.Replace("@hidden", string.Empty) : string.Empty)}{(role.BadgeName != null && role.BadgeName.EndsWith("@hidden") ? " [HIDDEN]" : string.Empty)}" }, { "❤️ Health:", $"{role?.Health.Amount ?? 0}/{role?.Health.Maximum ?? 0}" }, { "💉 AHP:", $"{role?.Ahp.Amount ?? 0}/{role?.Ahp.Limit ?? 0}" }, diff --git a/UncomplicatedCustomRoles/Manager/RoleValidator.cs b/UncomplicatedCustomRoles/Manager/RoleValidator.cs index bcc4abe..2ef82fc 100644 --- a/UncomplicatedCustomRoles/Manager/RoleValidator.cs +++ b/UncomplicatedCustomRoles/Manager/RoleValidator.cs @@ -143,9 +143,9 @@ private static void ValidateBadge(ICustomRole role, List warnings) } if (nameUsable && colorUsable && role.BadgeColor is not "default" && - !SpawnManager.colorMap.ContainsKey(role.BadgeColor)) + !SpawnManager.ColorMap.ContainsKey(role.BadgeColor)) warnings.Add( - $"'badge_color' '{role.BadgeColor}' is not a badge color the game knows, clients may show it as white. Known colors: default, {string.Join(", ", SpawnManager.colorMap.Keys)}."); + $"'badge_color' '{role.BadgeColor}' is not a badge color the game knows, clients may show it as white. Known colors: default, {string.Join(", ", SpawnManager.ColorMap.Keys)}."); } private static void ValidateRoles(ICustomRole role, List errors, List warnings) diff --git a/UncomplicatedCustomRoles/Manager/SpawnManager.cs b/UncomplicatedCustomRoles/Manager/SpawnManager.cs index aed9ba9..82b3ba5 100644 --- a/UncomplicatedCustomRoles/Manager/SpawnManager.cs +++ b/UncomplicatedCustomRoles/Manager/SpawnManager.cs @@ -44,8 +44,9 @@ namespace UncomplicatedCustomRoles.Manager; internal class SpawnManager { - public static readonly IReadOnlyDictionary colorMap = new Dictionary + public static readonly IReadOnlyDictionary ColorMap = new Dictionary { + { "white", "#FFFFFF" }, { "pink", "#FF96DE" }, { "red", "#C50000" }, { "brown", "#944710" }, From 39e216bb2fa94ec3458fd498bad8ba0f6c1828f0 Mon Sep 17 00:00:00 2001 From: MedveMarci Date: Tue, 14 Jul 2026 13:44:59 +0200 Subject: [PATCH 14/47] feat(api): add custom role event system and role extension helpers - Add CustomRoleExtension with helper methods for custom role - Refactor CustomRole.cs and add new methods - Update CompatibilityManager with whitespace cleanup --- .../API/Enums/LoadStatusType.cs | 3 +- .../API/Events/CustomRoleEvents.cs | 97 ++++++++ .../API/Events/EventArgs.cs | 98 ++++++++ .../API/Features/CustomRole.cs | 147 +++++++++++- .../API/Features/EventCustomRole.cs | 221 +----------------- .../API/Features/SummonedCustomRole.cs | 29 +-- .../Compatibility/CompatibilityManager.cs | 6 +- .../Events/PlayerEventHandler.cs | 20 +- .../Events/ServerEventHandler.cs | 8 + .../Extensions/CustomRoleExtension.cs | 103 ++++++++ .../Extensions/PlayerExtension.cs | 47 +++- .../Manager/LogManager.cs | 2 +- .../Manager/NET/HttpManager.cs | 3 +- .../Manager/SpawnManager.cs | 97 ++++---- .../Patches/PlayerEventPrefix.cs | 4 +- UncomplicatedCustomRoles/Plugin.cs | 19 +- 16 files changed, 586 insertions(+), 318 deletions(-) create mode 100644 UncomplicatedCustomRoles/API/Events/CustomRoleEvents.cs create mode 100644 UncomplicatedCustomRoles/API/Events/EventArgs.cs create mode 100644 UncomplicatedCustomRoles/Extensions/CustomRoleExtension.cs diff --git a/UncomplicatedCustomRoles/API/Enums/LoadStatusType.cs b/UncomplicatedCustomRoles/API/Enums/LoadStatusType.cs index 00b98e6..851e5ee 100644 --- a/UncomplicatedCustomRoles/API/Enums/LoadStatusType.cs +++ b/UncomplicatedCustomRoles/API/Enums/LoadStatusType.cs @@ -14,5 +14,6 @@ public enum LoadStatusType { Success, ValidatorError, - SameId + SameId, + Denied } \ No newline at end of file diff --git a/UncomplicatedCustomRoles/API/Events/CustomRoleEvents.cs b/UncomplicatedCustomRoles/API/Events/CustomRoleEvents.cs new file mode 100644 index 0000000..67a7cd0 --- /dev/null +++ b/UncomplicatedCustomRoles/API/Events/CustomRoleEvents.cs @@ -0,0 +1,97 @@ +/* + * This file is a part of the UncomplicatedCustomRoles project. + * + * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) + * + * This file is licensed under the GNU Affero General Public License v3.0. + * You should have received a copy of the AGPL license along with this file. + * If not, see . + */ + +using System; +using UncomplicatedCustomRoles.Manager; + +namespace UncomplicatedCustomRoles.API.Events; + +public static class CustomRoleEvents +{ + /// + /// Invoked before a is registered. + /// Set to false to deny the registration. + /// + public static event Action Registering; + + /// + /// Invoked after a has been successfully registered. + /// + public static event Action Registered; + + /// + /// Invoked after a has been unregistered. + /// + public static event Action Unregistered; + + /// + /// Invoked before a player is spawned as a custom role. + /// Set to false to deny the spawn. + /// + public static event Action Spawning; + + /// + /// Invoked after a player has been spawned as a custom role and the related + /// instance has been created. + /// + public static event Action Spawned; + + /// + /// Invoked after a custom role has been removed from a player. + /// + public static event Action Removed; + + internal static void OnRegistering(CustomRoleRegisteringEventArgs args) + { + InvokeSafely(Registering, args, nameof(Registering)); + } + + internal static void OnRegistered(CustomRoleRegisteredEventArgs args) + { + InvokeSafely(Registered, args, nameof(Registered)); + } + + internal static void OnUnregistered(CustomRoleUnregisteredEventArgs args) + { + InvokeSafely(Unregistered, args, nameof(Unregistered)); + } + + internal static void OnSpawning(CustomRoleSpawningEventArgs args) + { + InvokeSafely(Spawning, args, nameof(Spawning)); + } + + internal static void OnSpawned(CustomRoleSpawnedEventArgs args) + { + InvokeSafely(Spawned, args, nameof(Spawned)); + } + + internal static void OnRemoved(CustomRoleRemovedEventArgs args) + { + InvokeSafely(Removed, args, nameof(Removed)); + } + + private static void InvokeSafely(Action ev, T args, string name) + { + if (ev is null) + return; + + foreach (var handler in ev.GetInvocationList()) + try + { + ((Action)handler)(args); + } + catch (Exception e) + { + LogManager.Error( + $"An exception has been thrown by an external handler of the event CustomRoleEvents.{name} ({handler.Method?.DeclaringType?.FullName}::{handler.Method?.Name}): {e}"); + } + } +} diff --git a/UncomplicatedCustomRoles/API/Events/EventArgs.cs b/UncomplicatedCustomRoles/API/Events/EventArgs.cs new file mode 100644 index 0000000..3390d6e --- /dev/null +++ b/UncomplicatedCustomRoles/API/Events/EventArgs.cs @@ -0,0 +1,98 @@ +/* + * This file is a part of the UncomplicatedCustomRoles project. + * + * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) + * + * This file is licensed under the GNU Affero General Public License v3.0. + * You should have received a copy of the AGPL license along with this file. + * If not, see . + */ + +using LabApi.Features.Wrappers; +using UncomplicatedCustomRoles.API.Features; +using UncomplicatedCustomRoles.API.Interfaces; + +namespace UncomplicatedCustomRoles.API.Events; + +public class CustomRoleRegisteringEventArgs(ICustomRole role) +{ + /// + /// Gets the that is being registered. + /// + public ICustomRole Role { get; } = role; + + /// + /// Gets or sets whether the registration is allowed. + /// + public bool IsAllowed { get; set; } = true; +} + +public class CustomRoleRegisteredEventArgs(ICustomRole role) +{ + /// + /// Gets the that has been registered. + /// + public ICustomRole Role { get; } = role; +} + +public class CustomRoleUnregisteredEventArgs(ICustomRole role) +{ + /// + /// Gets the that has been unregistered. + /// + public ICustomRole Role { get; } = role; +} + +public class CustomRoleSpawningEventArgs(Player player, ICustomRole role) +{ + /// + /// Gets the that is being spawned as a custom role. + /// + public Player Player { get; } = player; + + /// + /// Gets the that is being applied. + /// + public ICustomRole Role { get; } = role; + + /// + /// Gets or sets whether the spawn is allowed. + /// + public bool IsAllowed { get; set; } = true; +} + +public class CustomRoleSpawnedEventArgs(SummonedCustomRole instance) +{ + /// + /// Gets the instance that has been created. + /// + public SummonedCustomRole Instance { get; } = instance; + + /// + /// Gets the that has been spawned as a custom role. + /// + public Player Player => Instance.Player; + + /// + /// Gets the that has been applied. + /// + public ICustomRole Role => Instance.Role; +} + +public class CustomRoleRemovedEventArgs(SummonedCustomRole instance) +{ + /// + /// Gets the (now invalid) instance that has been removed. + /// + public SummonedCustomRole Instance { get; } = instance; + + /// + /// Gets the the custom role has been removed from. + /// + public Player Player => Instance.Player; + + /// + /// Gets the that has been removed. + /// + public ICustomRole Role => Instance.Role; +} diff --git a/UncomplicatedCustomRoles/API/Features/CustomRole.cs b/UncomplicatedCustomRoles/API/Features/CustomRole.cs index 04bb05a..461fe48 100644 --- a/UncomplicatedCustomRoles/API/Features/CustomRole.cs +++ b/UncomplicatedCustomRoles/API/Features/CustomRole.cs @@ -8,8 +8,10 @@ * If not, see . */ +using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Linq; using System.Text.RegularExpressions; using PlayerRoles; using UncomplicatedCustomRoles.API.Enums; @@ -238,6 +240,14 @@ public virtual void OnSpawned(SummonedCustomRole role) { } + /// + /// Invoked when the custom role is removed from the player + /// + /// + public virtual void OnRemoved(SummonedCustomRole role) + { + } + public override string ToString() { return $"{Regex.Replace(Name, "(.*?)", "$1")} ({Id})"; @@ -252,16 +262,46 @@ public override string ToString() /// if the operation was successfull. public static bool TryGet(int id, out ICustomRole customRole) { - if (CustomRoles.ContainsKey(id)) - { - customRole = CustomRoles[id]; - return true; - } + return CustomRoles.TryGetValue(id, out customRole); + } + /// + /// Try to get a registered by it's (case-insensitive). + /// If more roles share the same name the first registered one is returned. + /// + /// + /// + /// if a role with the given name was found. + public static bool TryGet(string name, out ICustomRole customRole) + { customRole = null; + + if (string.IsNullOrEmpty(name)) + return false; + + foreach (var role in CustomRoles.Values) + if (string.Equals(role.Name, name, StringComparison.OrdinalIgnoreCase)) + { + customRole = role; + return true; + } + return false; } + /// + /// Try to get the first registered of the given type. + /// Useful for plugins that register their roles as classes. + /// + /// + /// + /// if a role of the given type was found. + public static bool TryGet(out T customRole) where T : class, ICustomRole + { + customRole = CustomRoles.Values.OfType().FirstOrDefault(); + return customRole is not null; + } + /// /// Get a registered by it's Id /// @@ -275,6 +315,47 @@ public static ICustomRole Get(int id) return null; } + /// + /// Get a registered by it's (case-insensitive) + /// + /// + /// The first with the given name or if not found. + public static ICustomRole Get(string name) + { + return TryGet(name, out var customRole) ? customRole : null; + } + + /// + /// Get the first registered of the given type + /// + /// + /// The first role of the given type or if not found. + public static T Get() where T : class, ICustomRole + { + return TryGet(out var customRole) ? customRole : null; + } + + /// + /// Gets whether a with the given Id is registered + /// + /// + /// + public static bool IsRegistered(int id) + { + return CustomRoles.ContainsKey(id); + } + + /// + /// Gets the first Id that is not used by any registered . + /// Useful when creating roles at runtime. + /// + /// The Id from which the search starts + /// + public static int GetFirstFreeId(int start = 1) + { + return CompatibilityManager.GetFirstFreeId(start); + } + /// /// Register a new instance. /// @@ -288,9 +369,50 @@ public static LoadStatusType Register(ICustomRole customRole) /// Unregister a registered . /// /// - public static void Unregister(ICustomRole customRole) + /// + /// If true every player currently playing this role will lose it (the + /// instances get destroyed) + /// + /// if the role was registered and has been removed. + public static bool Unregister(ICustomRole customRole, bool removeFromPlayers = false) { - CustomRoles.TryRemove(customRole.Id, out _); + return customRole is not null && Unregister(customRole.Id, removeFromPlayers); + } + + /// + /// Unregister a registered by it's Id. + /// + /// + /// + /// If true every player currently playing this role will lose it (the + /// instances get destroyed) + /// + /// if the role was registered and has been removed. + public static bool Unregister(int id, bool removeFromPlayers = false) + { + if (!CustomRoles.TryRemove(id, out var customRole)) + return false; + + if (removeFromPlayers) + foreach (var summoned in SummonedCustomRole.List.Values.Where(scr => scr.Role.Id == id).ToList()) + summoned.Destroy(); + + Events.CustomRoleEvents.OnUnregistered(new Events.CustomRoleUnregisteredEventArgs(customRole)); + return true; + } + + /// + /// Validate a without registering it. + /// Useful to check roles that are being built at runtime before calling . + /// + /// + /// The list of blocking problems - if not empty the role can't be registered + /// The list of non-blocking problems + /// if the role has no blocking problems. + public static bool Validate(ICustomRole role, out List errors, out List warnings) + { + RoleValidator.Validate(role, out errors, out warnings); + return errors.Count == 0; } internal static bool Validate(ICustomRole role, out string error) @@ -308,8 +430,15 @@ internal static LoadStatusType InternalRegister(ICustomRole customRole) if (errors.Count > 0) return LoadStatusType.ValidatorError; - if (CustomRoles.TryAdd(customRole.Id, customRole)) return LoadStatusType.Success; + var registeringArgs = new Events.CustomRoleRegisteringEventArgs(customRole); + Events.CustomRoleEvents.OnRegistering(registeringArgs); + if (!registeringArgs.IsAllowed) + return LoadStatusType.Denied; + + if (!CustomRoles.TryAdd(customRole.Id, customRole)) + return LoadStatusType.SameId; - return LoadStatusType.SameId; + Events.CustomRoleEvents.OnRegistered(new Events.CustomRoleRegisteredEventArgs(customRole)); + return LoadStatusType.Success; } } \ No newline at end of file diff --git a/UncomplicatedCustomRoles/API/Features/EventCustomRole.cs b/UncomplicatedCustomRoles/API/Features/EventCustomRole.cs index c578b42..e5178ef 100644 --- a/UncomplicatedCustomRoles/API/Features/EventCustomRole.cs +++ b/UncomplicatedCustomRoles/API/Features/EventCustomRole.cs @@ -9,235 +9,16 @@ */ using System; -using System.Collections.Generic; -using System.Text.RegularExpressions; using LabApi.Events.Arguments.ObjectiveEvents; using LabApi.Events.Arguments.PlayerEvents; using LabApi.Events.Arguments.Scp127Events; using LabApi.Events.Arguments.Scp3114Events; using LabApi.Events.Arguments.ServerEvents; -using PlayerRoles; -using UncomplicatedCustomRoles.API.Features.Behaviour; -using UncomplicatedCustomRoles.API.Interfaces; -using UncomplicatedCustomRoles.Manager; -using UnityEngine; namespace UncomplicatedCustomRoles.API.Features; #nullable enable -public class EventCustomRole : ICustomRole +public class EventCustomRole : CustomRole { - /// - /// Gets or sets the unique Id - /// - public virtual int Id { get; set; } = 1; - - /// - /// Gets or sets the name of the custom role.

- /// Thisn won't be shown to players, just a thing to help you recognize better your custom roles. - ///
- public virtual string Name { get; set; } = "Janitor"; - - /// - /// Gets or sets whether the name should be hidden in favor of the - /// - public virtual bool OverrideRoleName { get; set; } = false; - - /// - /// Gets or sets the nickname that will be set to the player if not null. - /// - public virtual string? Nickname { get; set; } = "D-%dnumber%"; - - /// - /// Gets or sets the CustomInfo that will be give to the player.

- /// Will be visible only to other players - ///
- public virtual string CustomInfo { get; set; } = "Janitor"; - - /// - /// Gets or sets the badge name - /// - public virtual string BadgeName { get; set; } = "Janitor"; - - /// - /// Gets or sets the badge color - /// - public virtual string BadgeColor { get; set; } = "pumpkin"; - - /// - /// Gets or sets the of the player - /// - public virtual RoleTypeId Role { get; set; } = RoleTypeId.ClassD; - - /// - /// Gets or sets the of the player - /// - public virtual Team? Team { get; set; } = null; - - /// - /// Gets or sets the the Role Appeareance for the player.

- /// If it's equal to then won't be applied - ///
- public virtual RoleTypeId RoleAppearance { get; set; } = RoleTypeId.ClassD; - - /// - /// Gets or sets the (s) that will be "friends" with this custom role - /// - public virtual List IsFriendOf { get; set; } = []; - - /// - /// Gets or sets the - /// - public virtual HealthBehaviour Health { get; set; } = new(); - - /// - /// Gets or sets the - /// - public virtual AhpBehaviour Ahp { get; set; } = new(); - - /// - /// Gets or sets the - /// - public virtual HumeShieldBehaviour HumeShield { get; set; } = new(); - - /// - /// Gets or sets the - /// - public virtual List? Effects { get; set; } = []; - - /// - /// Gets or sets the - /// - public virtual StaminaBehaviour Stamina { get; set; } = new(); - - /// - /// Gets or sets the maximum number of candies that can be took by the player without losing hands - /// - public virtual int MaxScp330Candies { get; set; } = 2; - - /// - /// Gets or sets whether the player can escape or not - /// - public virtual bool CanEscape { get; set; } = true; - - /// - /// Gets or sets the role after escape - /// - public virtual Dictionary RoleAfterEscape { get; set; } = new() - { - { - "default", - "InternalRole Spectator" - }, - { - "cuffed by InternalTeam ChaosInsurgency", - "InternalRole ClassD" - } - }; - - /// - /// Gets or sets the scale of the player - /// - public virtual Vector3 Scale { get; set; } = Vector3.one; - - /// - /// Gets or sets the broadcast that will be shown to the player when spawned - /// - public virtual string SpawnBroadcast { get; set; } = - "You are a Janitor!\nClean the Light Containment Zone!"; - - /// - /// Gets or sets the broadcast duration - /// - public virtual ushort SpawnBroadcastDuration { get; set; } = 5; - - /// - /// Gets or sets the hint that will be shown to the player when spawned - /// - public virtual string SpawnHint { get; set; } = "This hint will be shown when you will spawn as a Janitor!"; - - /// - /// Gets or sets hint duration - /// - public virtual float SpawnHintDuration { get; set; } = 5; - - /// - /// Gets or sets the custom inventory limits to override the default ones - /// - public virtual Dictionary CustomInventoryLimits { get; set; } = new() - { - { - ItemCategory.Medical, - 2 - } - }; - - /// - /// Gets or sets the inventory of the player - /// - public virtual List Inventory { get; set; } = - [ - ItemType.Flashlight, - ItemType.KeycardJanitor - ]; - - /// - /// Gets or sets the custom items inventory of the player - /// - public virtual List CustomItemsInventory { get; set; } = []; - - /// - /// Gets or sets the ammo inventory of the player - /// - public virtual Dictionary Ammo { get; set; } = new() - { - { - ItemType.Ammo9x19, - 10 - } - }; - - /// - /// Gets or sets the damage multiplier.

- /// This will increase - keep normal - or decrease the damage that this role will do - ///
- public virtual float DamageMultiplier { get; set; } = 1; - - /// - /// Gets or sets the - /// - public virtual SpawnBehaviour? SpawnSettings { get; set; } = new(); - - /// - /// Gets or sets the of the custom role - /// - public virtual List? CustomFlags { get; set; } = null; - - /// - /// Gets or sets whether the custom role should be evaluated during normal spawn events or not - /// - public virtual bool IgnoreSpawnSystem { get; set; } = false; - - public override string ToString() - { - return $"{Regex.Replace(Name, "(.*?)", "$1")} ({Id})"; - } - - /// - /// Invoked when the Custom Role is spawned - /// - /// - public virtual void OnSpawned(SummonedCustomRole role) - { - } - - /// - /// Invoked when the Custom Role is spawned - /// - /// - public virtual void OnRemoved(SummonedCustomRole role) - { - } - /// /// Called before kicking a from the server. /// diff --git a/UncomplicatedCustomRoles/API/Features/SummonedCustomRole.cs b/UncomplicatedCustomRoles/API/Features/SummonedCustomRole.cs index b2c81f9..a172e84 100644 --- a/UncomplicatedCustomRoles/API/Features/SummonedCustomRole.cs +++ b/UncomplicatedCustomRoles/API/Features/SummonedCustomRole.cs @@ -85,8 +85,8 @@ internal SummonedCustomRole(Player player, ICustomRole role, Triplet public bool IsDefaultCoroutineRole => - (Role.HumeShield?.Amount ?? 0) > 0 && (Role.HumeShield?.RegenerationAmount ?? 0) > 0; + (Role.HumeShield?.Maximum ?? 0) > 0 && (Role.HumeShield?.RegenerationAmount ?? 0) > 0; /// /// Gets if the current SummonedCustomRole is valid or not @@ -333,8 +333,7 @@ public void Remove() _customModules.Remove(module); } - if (Role.BadgeName is not null && Role.BadgeName.Length > 1 && Role.BadgeColor is not null && - Role.BadgeColor.Length > 2 && Badge is not null && Badge is Triplet badge) + if (Badge is { } badge) { Player.ReferenceHub.serverRoles.SetText(badge.First); Player.ReferenceHub.serverRoles.SetColor(badge.Second); @@ -362,14 +361,13 @@ public void Remove() DisguiseTeam.Remove(Player.PlayerId); // Reset ammo limit - if (Role.Ammo is Dictionary ammoList && ammoList.Count > 0) - foreach (var ammo in ammoList.Keys) + if (Role.Ammo is { Count: > 0 }) + foreach (var ammo in Role.Ammo.Keys) Player.ResetAmmoLimit(ammo); // Reset category limit - if (Role.CustomInventoryLimits is Dictionary inventoryLimits && - inventoryLimits.Count > 0) - foreach (var category in inventoryLimits.Keys) + if (Role.CustomInventoryLimits is { Count: > 0 }) + foreach (var category in Role.CustomInventoryLimits.Keys) Player.ResetCategoryLimit(category); if (IsCustomNickname) @@ -385,8 +383,8 @@ public void Remove() if (Appearance != RoleTypeId.None && LabApiExtensions.IsAvailable) LabApiExtensions.RemoveFakeRole(Player); - if (Role is EventCustomRole eventCustomRole) - eventCustomRole.OnRemoved(this); + if (Role is CustomRole customRole) + customRole.OnRemoved(this); } catch (Exception e) { @@ -402,7 +400,12 @@ public void Remove() _eventModuleCount = 0; _customModules.Clear(); + + var wasValid = _internalValid; _internalValid = false; + + if (wasValid) + API.Events.CustomRoleEvents.OnRemoved(new API.Events.CustomRoleRemovedEventArgs(this)); } /// @@ -587,7 +590,7 @@ public static SummonedCustomRole Get(ReferenceHub player) /// public static SummonedCustomRole Get(string id) { - return List.Values.FirstOrDefault(scr => scr.Id == id); + return id is not null && List.TryGetValue(id, out var role) ? role : null; } /// diff --git a/UncomplicatedCustomRoles/Compatibility/CompatibilityManager.cs b/UncomplicatedCustomRoles/Compatibility/CompatibilityManager.cs index 187a0cc..5e2cc99 100644 --- a/UncomplicatedCustomRoles/Compatibility/CompatibilityManager.cs +++ b/UncomplicatedCustomRoles/Compatibility/CompatibilityManager.cs @@ -53,7 +53,7 @@ public static void ParseAndLoadCustomRole(string file) role = YamlConfigParser.Deserializer.Deserialize(content); } - catch (Exception ex) + catch (Exception) { // Try to decode older roles in order to make everything work foreach (var kvp in previousVersionRoles) @@ -72,7 +72,7 @@ public static void ParseAndLoadCustomRole(string file) } if (role is null) - throw ex; + throw; } RolePaths.TryAdd(role, file); @@ -141,7 +141,7 @@ public static string GetRoleFileElement(string[] pieces, string rowPart, bool re var el = pieces.FirstOrDefault(l => l.Contains(rowPart)) ?? "N/D"; if (removeSpaces) - el.Replace(" ", string.Empty); + el = el.Replace(" ", string.Empty); return el.Replace($"{rowPart} ", string.Empty).Replace(rowPart, string.Empty); } diff --git a/UncomplicatedCustomRoles/Events/PlayerEventHandler.cs b/UncomplicatedCustomRoles/Events/PlayerEventHandler.cs index c21815c..7d06908 100644 --- a/UncomplicatedCustomRoles/Events/PlayerEventHandler.cs +++ b/UncomplicatedCustomRoles/Events/PlayerEventHandler.cs @@ -170,11 +170,9 @@ public void OnDeath(PlayerDeathEventArgs ev) // Try change appearance of the killer if (ev.Attacker.TryGetSummonedInstance(out var attackerCustomRole) && - attackerCustomRole.TryGetModule(out ChangeAppearanceOnKill changeAppearanceOnKill)) + attackerCustomRole.TryGetModule(out ChangeAppearanceOnKill changeAppearanceOnKill) && + !(changeAppearanceOnKill.Forever && changeAppearanceOnKill.AlreadyChanged)) { - if (changeAppearanceOnKill.Forever && changeAppearanceOnKill.AlreadyChanged) - return; - changeAppearanceOnKill.AlreadyChanged = true; // Change @@ -203,8 +201,6 @@ public void OnDeath(PlayerDeathEventArgs ev) } }); } - - // DON'T DO ANYTHING HERE AS THERE ARE TWO return UP THERE! } public void OnRagdollSpawn(PlayerSpawningRagdollEventArgs ev) @@ -227,12 +223,12 @@ public void OnChangingRole(PlayerChangingRoleEventArgs ev) if (ev.Player is null) return; - // Let's clear for custom types - SpawnManager.ClearCustomTypes(ev.Player); - if (!ev.IsAllowed) return; + // Let's clear for custom types + SpawnManager.ClearCustomTypes(ev.Player); + if (!Round.IsRoundStarted) return; @@ -293,7 +289,7 @@ public void OnHurting(PlayerHurtingEventArgs Hurting) return; } - if (attackerCustomRole?.HasModule() ?? false) + if (attackerCustomRole.HasModule()) attackerCustomRole.RemoveModules(); if (Hurting.DamageHandler is StandardDamageHandler standardDamageHandler) @@ -311,7 +307,7 @@ public void OnHurting(PlayerHurtingEventArgs Hurting) return; } - if (playerCustomRole?.HasModule() ?? false) + if (playerCustomRole.HasModule()) Hurting.IsAllowed = false; } } @@ -401,7 +397,7 @@ public void OnEscaping(PlayerEscapingEventArgs Escaping) { LogManager.Silent( "Successfully activated the call to method SpawnManager::SummonCustomSubclass(<...>) as the player is not inside the Escape::Bucket bucket! - Adding it..."); - API.Features.Escape.Bucket.Add(Escaping.Player.PlayerId); + API.Features.Escape.AddBucket(Escaping.Player); SpawnManager.SummonCustomSubclass(Escaping.Player, role.Id); } else diff --git a/UncomplicatedCustomRoles/Events/ServerEventHandler.cs b/UncomplicatedCustomRoles/Events/ServerEventHandler.cs index 17e9c24..34f6ebb 100644 --- a/UncomplicatedCustomRoles/Events/ServerEventHandler.cs +++ b/UncomplicatedCustomRoles/Events/ServerEventHandler.cs @@ -62,6 +62,14 @@ public void OnRoundEnded(RoundEndedEventArgs _) public void OnRoundRestarted() { Announcer.SavedCustomAnnouncements.Clear(); + + RespawnInventoryQueue.Clear(); + RagdollAppearanceQueue.Clear(); + TerminationQueue.Clear(); + FirstRoundPlayers.Clear(); + Spawn.SpawnQueue.Clear(); + Spawn.Spawning.Clear(); + API.Features.Escape.Bucket.Clear(); } public void OnWaveRespawning(WaveRespawningEventArgs ev) diff --git a/UncomplicatedCustomRoles/Extensions/CustomRoleExtension.cs b/UncomplicatedCustomRoles/Extensions/CustomRoleExtension.cs new file mode 100644 index 0000000..12941f9 --- /dev/null +++ b/UncomplicatedCustomRoles/Extensions/CustomRoleExtension.cs @@ -0,0 +1,103 @@ +/* + * This file is a part of the UncomplicatedCustomRoles project. + * + * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) + * + * This file is licensed under the GNU Affero General Public License v3.0. + * You should have received a copy of the AGPL license along with this file. + * If not, see . + */ + +using System.Collections.Generic; +using LabApi.Features.Wrappers; +using UncomplicatedCustomRoles.API.Enums; +using UncomplicatedCustomRoles.API.Features; +using UncomplicatedCustomRoles.API.Interfaces; + +namespace UncomplicatedCustomRoles.Extensions; + +public static class CustomRoleExtension +{ + /// + /// Spawn the given as this . + /// Works both for roles with and without . + /// + /// + /// + /// The created instance or if the spawn failed. + public static SummonedCustomRole Spawn(this ICustomRole role, Player player) + { + if (role is null || player is null) + return null; + + return SummonedCustomRole.Summon(player, role); + } + + /// + /// Remove this from the given if they are currently playing it. + /// + /// + /// + /// if the player was playing this role, and it has been removed. + public static bool RemoveFrom(this ICustomRole role, Player player) + { + if (role is null || !SummonedCustomRole.TryGet(player, out var summoned) || summoned.Role.Id != role.Id) + return false; + + summoned.Destroy(); + return true; + } + + /// + /// Gets every active instance of this . + /// + /// + /// + public static List GetSpawnedInstances(this ICustomRole role) + { + return role is null ? [] : SummonedCustomRole.Get(role); + } + + /// + /// Gets the number of players currently playing this . + /// + /// + /// + public static int GetSpawnedCount(this ICustomRole role) + { + return role is null ? 0 : SummonedCustomRole.Count(role); + } + + /// + /// Gets whether a with this role's Id is currently registered. + /// + /// + /// + public static bool IsRegistered(this ICustomRole role) + { + return role is not null && CustomRole.IsRegistered(role.Id); + } + + /// + /// Register this inside UCR. + /// + /// + /// The result of the registration. + public static LoadStatusType Register(this ICustomRole role) + { + return CustomRole.Register(role); + } + + /// + /// Unregister this from UCR. + /// + /// + /// + /// If true every player currently playing this role will lose it + /// + /// if the role was registered and has been removed. + public static bool Unregister(this ICustomRole role, bool removeFromPlayers = false) + { + return CustomRole.Unregister(role, removeFromPlayers); + } +} diff --git a/UncomplicatedCustomRoles/Extensions/PlayerExtension.cs b/UncomplicatedCustomRoles/Extensions/PlayerExtension.cs index ac6806d..598bb4d 100644 --- a/UncomplicatedCustomRoles/Extensions/PlayerExtension.cs +++ b/UncomplicatedCustomRoles/Extensions/PlayerExtension.cs @@ -34,6 +34,39 @@ public static bool HasCustomRole(this Player player) return SummonedCustomRole.TryGet(player, out _); } + /// + /// Check if a is currently playing the with the given Id. + /// + /// + /// + /// if the player is playing the given custom role. + public static bool HasCustomRole(this Player player, int id) + { + return SummonedCustomRole.TryGet(player, out var summoned) && summoned.Role.Id == id; + } + + /// + /// Get the definition the is currently playing. + /// + /// + /// The or if the player has no custom role. + public static ICustomRole GetCustomRole(this Player player) + { + return SummonedCustomRole.Get(player)?.Role; + } + + /// + /// Try to get the definition the is currently playing. + /// + /// + /// + /// if the player has a custom role. + public static bool TryGetCustomRole(this Player player, out ICustomRole role) + { + role = player.GetCustomRole(); + return role is not null; + } + internal static void ForceApplyEffect(this ReferenceHub hub, string effectName, byte intensity, float duration, bool addDuration = false) { @@ -49,10 +82,14 @@ internal static void ForceApplyEffect(this ReferenceHub hub, string effectName, /// /// /// - public static void SetCustomRoleSync(this Player player, ICustomRole role) + /// The created instance or if the spawn failed. + public static SummonedCustomRole SetCustomRoleSync(this Player player, ICustomRole role) { + if (role is null) + return null; + SpawnManager.ClearCustomTypes(player); - SpawnManager.SummonCustomSubclass(player, role.Id); + return SummonedCustomRole.Summon(player, role); } /// @@ -60,10 +97,10 @@ public static void SetCustomRoleSync(this Player player, ICustomRole role) /// /// /// - public static void SetCustomRoleSync(this Player player, int role) + /// The created instance or if the spawn failed. + public static SummonedCustomRole SetCustomRoleSync(this Player player, int role) { - SpawnManager.ClearCustomTypes(player); - SpawnManager.SummonCustomSubclass(player, role); + return CustomRole.TryGet(role, out var customRole) ? player.SetCustomRoleSync(customRole) : null; } /// diff --git a/UncomplicatedCustomRoles/Manager/LogManager.cs b/UncomplicatedCustomRoles/Manager/LogManager.cs index 3d7593b..5ad8915 100644 --- a/UncomplicatedCustomRoles/Manager/LogManager.cs +++ b/UncomplicatedCustomRoles/Manager/LogManager.cs @@ -55,7 +55,7 @@ public static void Warn(string message, string error = "CS0000") public static void Error(string message, string error = "CS0000") { - History.Add(new LogEntry(DateTimeOffset.Now.ToUnixTimeMilliseconds(), nameof(LogLevel.Warn), message, error)); + History.Add(new LogEntry(DateTimeOffset.Now.ToUnixTimeMilliseconds(), nameof(LogLevel.Error), message, error)); Logger.Error(message); } diff --git a/UncomplicatedCustomRoles/Manager/NET/HttpManager.cs b/UncomplicatedCustomRoles/Manager/NET/HttpManager.cs index badc927..f955247 100644 --- a/UncomplicatedCustomRoles/Manager/NET/HttpManager.cs +++ b/UncomplicatedCustomRoles/Manager/NET/HttpManager.cs @@ -132,6 +132,7 @@ public void LoadLatestVersion() public void LoadCreditTags() { Credits = new Dictionary>(); + IsJobRole.Clear(); try { var Data = JsonSerializer.Deserialize>>( @@ -156,7 +157,7 @@ public void LoadCreditTags() _ => false }; var isJob = kvp.Value["job"].ValueKind == JsonValueKind.True; - Credits.Add(kvp.Key, new Triplet(role, color, overrideStr)); + Credits[kvp.Key] = new Triplet(role, color, overrideStr); if (isJob) IsJobRole.Add(kvp.Key); } diff --git a/UncomplicatedCustomRoles/Manager/SpawnManager.cs b/UncomplicatedCustomRoles/Manager/SpawnManager.cs index 82b3ba5..bf97447 100644 --- a/UncomplicatedCustomRoles/Manager/SpawnManager.cs +++ b/UncomplicatedCustomRoles/Manager/SpawnManager.cs @@ -134,6 +134,15 @@ public static void SummonCustomSubclass(Player player, int id, bool doBypassRole return; } + var spawningArgs = new API.Events.CustomRoleSpawningEventArgs(player, Role); + API.Events.CustomRoleEvents.OnSpawning(spawningArgs); + if (!spawningArgs.IsAllowed) + { + LogManager.Debug( + $"Spawn of player {player.Nickname} as CustomRole {Role.Name} ({Role.Id}) denied by an external plugin through CustomRoleEvents.Spawning"); + return; + } + // This will allow us to avoid the loop of another OnSpawning Spawn.Spawning.Add(player.PlayerId); @@ -252,28 +261,41 @@ public static void SummonCustomSubclass(Player player, int id, bool doBypassRole break; } - SummonSubclassApplier(player, Role); + SummonSubclassApplier(player, Role, true); } catch (Exception ex) { LogManager.Error(ex.ToString(), "SP0002"); } + finally + { + Spawn.Spawning.Remove(player.PlayerId); + } } - public static void SummonSubclassApplier(Player Player, ICustomRole Role) + internal static void SummonSubclassApplier(Player Player, ICustomRole Role, bool spawningEventAlreadyFired = false) { try { - if (Role.CustomInventoryLimits is Dictionary inventoryLimits && - inventoryLimits.Count > 0) + if (!spawningEventAlreadyFired) + { + var spawningArgs = new API.Events.CustomRoleSpawningEventArgs(Player, Role); + API.Events.CustomRoleEvents.OnSpawning(spawningArgs); + if (!spawningArgs.IsAllowed) + { + LogManager.Debug( + $"Spawn of player {Player.Nickname} as CustomRole {Role.Name} ({Role.Id}) denied by an external plugin through CustomRoleEvents.Spawning"); + return; + } + } + + if (Role.CustomInventoryLimits is { Count: > 0 } inventoryLimits) foreach (var category in inventoryLimits) Player.SetCategoryLimit(category.Key, category.Value); Player.ResetInventory(Role.Inventory); - LogManager.Silent($"Can we give any CustomItem? {Role.CustomItemsInventory.Count}"); - - if (Role.CustomItemsInventory.Any()) + if (Role.CustomItemsInventory is { Count: > 0 }) foreach (var itemId in Role.CustomItemsInventory) if (!Player.IsInventoryFull) try @@ -297,7 +319,7 @@ public static void SummonSubclassApplier(Player Player, ICustomRole Role) Player.ClearAmmo(); - if (Role.Ammo is not null && Role.Ammo.GetType() == typeof(Dictionary) && Role.Ammo.Any()) + if (Role.Ammo is { Count: > 0 }) foreach (var Ammo in Role.Ammo) { if (Ammo.Value > Player.GetAmmoLimit(Ammo.Key)) @@ -372,13 +394,13 @@ public static void SummonSubclassApplier(Player Player, ICustomRole Role) LogManager.Silent($"Found {PermanentEffects.Count} permament effects"); - if (Role.SpawnBroadcast != string.Empty) + if (!string.IsNullOrEmpty(Role.SpawnBroadcast)) { Player.ClearBroadcasts(); Player.SendBroadcast(Role.SpawnBroadcast, Role.SpawnBroadcastDuration); } - if (Role.SpawnHint != string.Empty) + if (!string.IsNullOrEmpty(Role.SpawnHint)) Player.SendHint(Role.SpawnHint, Role.SpawnHintDuration); Triplet? Badge = null; @@ -440,6 +462,8 @@ public static void SummonSubclassApplier(Player Player, ICustomRole Role) if (API.Features.Escape.Bucket.Contains(Player.PlayerId)) API.Features.Escape.Bucket.Remove(Player.PlayerId); + API.Events.CustomRoleEvents.OnSpawned(new API.Events.CustomRoleSpawnedEventArgs(roleInstance)); + LogManager.Debug($"{Player} successfully spawned as {Role.Name} ({Role.Id})! [2VDS]"); } catch (Exception ex) @@ -466,27 +490,13 @@ public static void SummonSubclassApplier(Player Player, ICustomRole Role) } else { - var Elements = kvp.Key.Split(' ').ToList(); + var Elements = kvp.Key.Split(' '); - if (Elements.Count != 4) + if (Elements.Length != 4 || Elements[0] is not "cuffed" || Elements[1] is not "by") { LogManager.Warn( - $"Failed to parse an EscapeRole[key]: syntax should be cuffed by , found {Elements.Count} args!\nSource: {kvp.Key}"); - return new KeyValuePair(false, RoleTypeId.Spectator); - } - - if (Elements[0] is not "cuffed") - { - LogManager.Warn( - $"Failed to parse an EscapeRole[key]: syntax should be cuffed by , found {Elements.Count} args!\nSource: {kvp.Key}"); - return new KeyValuePair(false, RoleTypeId.Spectator); - } - - if (Elements[1] is not "by") - { - LogManager.Warn( - $"Failed to parse an EscapeRole[key]: syntax should be cuffed by , found {Elements.Count} args!\nSource: {kvp.Key}"); - return new KeyValuePair(false, RoleTypeId.Spectator); + $"Failed to parse an EscapeRole[key]: syntax should be 'cuffed by ' (4 args), found {Elements.Length} args!\nSource: {kvp.Key}"); + continue; } if ((Elements[2] is "InternalTeam" || Elements[2] is "IT") && Enum.TryParse(Elements[3], out Team team)) @@ -560,12 +570,16 @@ public static void SummonSubclassApplier(Player Player, ICustomRole Role) return null; } - Dictionary> RolePercentage = new(); - foreach (var evaluated in SpawnEvaluatedRoles) - RolePercentage[evaluated] = []; + if (!SpawnEvaluatedRoles.Contains(NewRole)) + return null; + + var readyPlayers = Player.ReadyList.Count(); + List candidates = []; - foreach (var Role in CustomRole.CustomRoles.Values.Where(cr => cr.SpawnSettings is not null)) - if (!Role.IgnoreSpawnSystem && Player.ReadyList.Count() >= Role.SpawnSettings?.MinPlayers && + foreach (var Role in CustomRole.CustomRoles.Values) + if (Role.SpawnSettings is not null && !Role.IgnoreSpawnSystem && + Role.SpawnSettings.CanReplaceRoles is { } canReplaceRoles && canReplaceRoles.Contains(NewRole) && + readyPlayers >= Role.SpawnSettings.MinPlayers && SummonedCustomRole.Count(Role) < Role.SpawnSettings.MaxPlayers) { if (Role.SpawnSettings.RequiredPermission is not null) @@ -613,20 +627,13 @@ static IEnumerable ExtractPermissions(object obj) } } } - - foreach (var RoleType in Role.SpawnSettings.CanReplaceRoles) - { - if (!RolePercentage.TryGetValue(RoleType, out var bucket)) - continue; - - for (var a = 0; a < Role.SpawnSettings.SpawnChance; a++) - bucket.Add(Role); - } + + for (var a = 0; a < Role.SpawnSettings.SpawnChance; a++) + candidates.Add(Role); } - if (RolePercentage.ContainsKey(NewRole)) - if (Random.Range(0, 100) < RolePercentage[NewRole].Count) - return CustomRole.CustomRoles[RolePercentage[NewRole].RandomItem().Id]; + if (candidates.Count > 0 && Random.Range(0, 100) < candidates.Count) + return candidates.RandomItem(); return null; } diff --git a/UncomplicatedCustomRoles/Patches/PlayerEventPrefix.cs b/UncomplicatedCustomRoles/Patches/PlayerEventPrefix.cs index e31cc1f..55a4c10 100644 --- a/UncomplicatedCustomRoles/Patches/PlayerEventPrefix.cs +++ b/UncomplicatedCustomRoles/Patches/PlayerEventPrefix.cs @@ -23,7 +23,7 @@ namespace UncomplicatedCustomRoles.Patches; internal class PlayerEventPrefix { - private static IEnumerable _patchedMethods = new List(); + private static List _patchedMethods = []; private static readonly Dictionary EventNameCache = new(); @@ -62,7 +62,7 @@ internal static void Patch(Harmony harmony) _patchedMethods = typeof(PlayerEvents).GetMethods().Where(m => m.Name.StartsWith("On") && m.GetParameters().Length > 0 && - typeof(IPlayerEvent).IsAssignableFrom(m.GetParameters()[0].ParameterType)); + typeof(IPlayerEvent).IsAssignableFrom(m.GetParameters()[0].ParameterType)).ToList(); foreach (var method in _patchedMethods) harmony.Patch(method, prefixMethod); diff --git a/UncomplicatedCustomRoles/Plugin.cs b/UncomplicatedCustomRoles/Plugin.cs index b6086d0..c3e2092 100644 --- a/UncomplicatedCustomRoles/Plugin.cs +++ b/UncomplicatedCustomRoles/Plugin.cs @@ -35,6 +35,8 @@ internal class Plugin : Plugin internal static HttpManager HttpManager; private Harmony _harmony; + + private bool _welcomeShown; public override string Name => "UncomplicatedCustomRoles"; public override string Description => "Customize your SCP:SL server with Custom Roles!"; @@ -107,15 +109,19 @@ public override void Disable() ScriptedEvents.UnregisterCustomActions(); - PlayerEventPrefix.Unpatch(_harmony); - - _harmony.UnpatchAll(_harmony.Id); + if (_harmony is not null) + { + PlayerEventPrefix.Unpatch(_harmony); + _harmony.UnpatchAll(_harmony.Id); + _harmony = null; + } TeamPatchManager.Shutdown(); EventHandlerBase.UnregisterAll(); - HttpManager.UnregisterEvents(); + HttpManager?.UnregisterEvents(); + HttpManager = null; Instance = null; } @@ -128,10 +134,11 @@ public void OnFinishedLoadingPlugins() // Register ScriptedEvents integration ScriptedEvents.RegisterCustomActions(); - // Run the import managet + // Run the import manager ImportManager.Init(); - if (Config is not { EnableBasicLogs: true }) return; + if (_welcomeShown || Config is not { EnableBasicLogs: true }) return; + _welcomeShown = true; LogManager.Info($"Thanks for using UncomplicatedCustomRoles v{Version.ToString(3)} by {Author}!", ConsoleColor.Blue); LogManager.Info( From 8e040d1f2776ee9b4f14cf920dcb87c630a9f7c4 Mon Sep 17 00:00:00 2001 From: MedveMarci Date: Wed, 15 Jul 2026 12:08:39 +0200 Subject: [PATCH 15/47] Fixed CustomInfo removing --- .../API/Features/CustomInfo.cs | 11 ++++++++ .../API/Features/SummonedCustomRole.cs | 27 ++++++++++--------- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/UncomplicatedCustomRoles/API/Features/CustomInfo.cs b/UncomplicatedCustomRoles/API/Features/CustomInfo.cs index 193f3c2..e071166 100644 --- a/UncomplicatedCustomRoles/API/Features/CustomInfo.cs +++ b/UncomplicatedCustomRoles/API/Features/CustomInfo.cs @@ -23,6 +23,8 @@ public class CustomInfo { private Player _lastOwner; + private bool _detached; + public CustomInfo(string nickname, string role, string info) { Nickname = nickname; @@ -82,9 +84,18 @@ public string Info } internal static bool SuppressExternalSync { get; set; } + + internal void Detach() + { + _detached = true; + _lastOwner = null; + } public void UpdateInfo(Player player) { + if (_detached) + return; + _lastOwner = player; var previousSuppress = SuppressExternalSync; diff --git a/UncomplicatedCustomRoles/API/Features/SummonedCustomRole.cs b/UncomplicatedCustomRoles/API/Features/SummonedCustomRole.cs index a172e84..8b846da 100644 --- a/UncomplicatedCustomRoles/API/Features/SummonedCustomRole.cs +++ b/UncomplicatedCustomRoles/API/Features/SummonedCustomRole.cs @@ -342,16 +342,10 @@ public void Remove() LogManager.Debug("Badge detected, fixed"); } - CustomInfo.SuppressExternalSync = true; - try - { - Player.ReferenceHub.nicknameSync.Network_playerInfoToShow = PlayerInfoArea; - Player.ReferenceHub.nicknameSync.Network_customPlayerInfoString = string.Empty; - } - finally - { - CustomInfo.SuppressExternalSync = false; - } + CustomInfo?.Detach(); + + if (IsCustomNickname) + Player.DisplayName = null!; LogManager.Debug("Scale reset to 1, 1, 1"); Player.Scale = new Vector3(1, 1, 1); @@ -370,8 +364,17 @@ public void Remove() foreach (var category in Role.CustomInventoryLimits.Keys) Player.ResetCategoryLimit(category); - if (IsCustomNickname) - Player.DisplayName = null; + // Clear the custom info last so nothing re-applies it afterwards + CustomInfo.SuppressExternalSync = true; + try + { + Player.ReferenceHub.nicknameSync.Network_playerInfoToShow = PlayerInfoArea; + Player.ReferenceHub.nicknameSync.Network_customPlayerInfoString = string.Empty; + } + finally + { + CustomInfo.SuppressExternalSync = false; + } if (IsDefaultCoroutineRole && GenericCoroutine.IsRunning) Timing.KillCoroutines(GenericCoroutine); From b011533962bddd96352081ff7fd1936c43ff10a0 Mon Sep 17 00:00:00 2001 From: MedveMarci Date: Wed, 15 Jul 2026 12:43:46 +0200 Subject: [PATCH 16/47] Fixed escaping when there is no EscapeScenarioType for that role; Fixed KeepInventoryOnEscape module --- .../Events/PlayerEventHandler.cs | 30 +++++++++++-------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/UncomplicatedCustomRoles/Events/PlayerEventHandler.cs b/UncomplicatedCustomRoles/Events/PlayerEventHandler.cs index 7d06908..b1a173e 100644 --- a/UncomplicatedCustomRoles/Events/PlayerEventHandler.cs +++ b/UncomplicatedCustomRoles/Events/PlayerEventHandler.cs @@ -371,13 +371,15 @@ public void OnEscaping(PlayerEscapingEventArgs Escaping) if (!NewRole.Key) { - // Natural role, let's try to parse it - if (Enum.TryParse(NewRole.Value.ToString(), out RoleTypeId role)) - if (role is not RoleTypeId.None) - { - Escaping.NewRole = role; - Escaping.IsAllowed = true; - } + // Natural (internal) role, let's try to parse it + if (Enum.TryParse(NewRole.Value.ToString(), out RoleTypeId role) && role is not RoleTypeId.None) + { + Escaping.NewRole = role; + Escaping.IsAllowed = true; + + if (Escaping.EscapeScenario is Escape.EscapeScenarioType.None) + Escaping.EscapeScenario = Escape.EscapeScenarioType.Custom; + } } else { @@ -386,17 +388,19 @@ public void OnEscaping(PlayerEscapingEventArgs Escaping) { LogManager.Silent("Role found!"); - if (summoned.TryGetModule(out KeepInventoryOnEscape module)) - RespawnInventoryQueue.TryAdd(Escaping.Player.PlayerId, - new Tuple, Dictionary, bool>( - [..Escaping.Player.Items.Select(i => i.Type)], - new Dictionary(Escaping.Player.Ammo), module.DropItems)); - Escaping.IsAllowed = false; if (!API.Features.Escape.Bucket.Contains(Escaping.Player.PlayerId)) { LogManager.Silent( "Successfully activated the call to method SpawnManager::SummonCustomSubclass(<...>) as the player is not inside the Escape::Bucket bucket! - Adding it..."); + + var dropOldInventory = !summoned.TryGetModule(out KeepInventoryOnEscape module) || + module.DropItems; + RespawnInventoryQueue[Escaping.Player.PlayerId] = + new Tuple, Dictionary, bool>( + [..Escaping.Player.Items.Select(i => i.Type)], + new Dictionary(Escaping.Player.Ammo), dropOldInventory); + API.Features.Escape.AddBucket(Escaping.Player); SpawnManager.SummonCustomSubclass(Escaping.Player, role.Id); } From a89e5355cd40d58b9be738b81e4e36be3f9e8621 Mon Sep 17 00:00:00 2001 From: MedveMarci Date: Wed, 15 Jul 2026 12:44:02 +0200 Subject: [PATCH 17/47] Fixed UCI Integration --- UncomplicatedCustomRoles/Integrations/DynamicInvoke.cs | 10 ++++++---- UncomplicatedCustomRoles/Integrations/UCI.cs | 8 +++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/UncomplicatedCustomRoles/Integrations/DynamicInvoke.cs b/UncomplicatedCustomRoles/Integrations/DynamicInvoke.cs index fa41433..03313c3 100644 --- a/UncomplicatedCustomRoles/Integrations/DynamicInvoke.cs +++ b/UncomplicatedCustomRoles/Integrations/DynamicInvoke.cs @@ -130,12 +130,13 @@ public static MethodInfo GetMethod(string plugin, string address, bool isLabapi } } - private static Assembly GetLabAPIAssembly(string pluginName) + internal static Assembly GetLabAPIAssembly(string pluginName) { try { KeyValuePair? plugin = - PluginLoader.Plugins.FirstOrDefault(p => p.Key.Name == pluginName); + PluginLoader.Plugins.FirstOrDefault(p => + p.Key.Name.Contains(pluginName, StringComparison.CurrentCultureIgnoreCase)); if (plugin is not null) return plugin.Value.Value; @@ -149,11 +150,12 @@ private static Assembly GetLabAPIAssembly(string pluginName) } } - private static Assembly GetExiledAssembly(string pluginName) + internal static Assembly GetExiledAssembly(string pluginName) { try { - var assembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(p => p.FullName.Contains(pluginName)); + var assembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(p => + p.FullName.Contains(pluginName, StringComparison.CurrentCultureIgnoreCase)); return assembly; } catch (Exception e) diff --git a/UncomplicatedCustomRoles/Integrations/UCI.cs b/UncomplicatedCustomRoles/Integrations/UCI.cs index 376471f..6959153 100644 --- a/UncomplicatedCustomRoles/Integrations/UCI.cs +++ b/UncomplicatedCustomRoles/Integrations/UCI.cs @@ -9,20 +9,18 @@ */ using System; -using System.Linq; using System.Reflection; using LabApi.Features.Wrappers; -using LabApi.Loader; using UncomplicatedCustomRoles.Manager; namespace UncomplicatedCustomRoles.Integrations; internal static class UCI { - public static Assembly Assembly = - PluginLoader.Plugins.FirstOrDefault(p => p.Key.Name is "UncomplicatedCustomItems").Value; + public static readonly Assembly Assembly = DynamicInvoke.GetLabAPIAssembly("UncomplicatedCustomItems") ?? + DynamicInvoke.GetExiledAssembly("UncomplicatedCustomItems"); - public static Type SummonedCustomItem = + public static readonly Type SummonedCustomItem = Assembly?.GetType("UncomplicatedCustomItems.API.Features.SummonedCustomItem"); public static bool HasCustomItem(uint id, out object customItem) From d416c5d0811f8c357e83265aa09d62089b72f7a1 Mon Sep 17 00:00:00 2001 From: MedveMarci Date: Wed, 15 Jul 2026 12:44:37 +0200 Subject: [PATCH 18/47] Added config option to disable config validator --- .../API/Features/CustomRole.cs | 22 +++++++++++-------- UncomplicatedCustomRoles/Config.cs | 3 +++ .../Events/ServerEventHandler.cs | 4 +++- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/UncomplicatedCustomRoles/API/Features/CustomRole.cs b/UncomplicatedCustomRoles/API/Features/CustomRole.cs index 461fe48..639cba5 100644 --- a/UncomplicatedCustomRoles/API/Features/CustomRole.cs +++ b/UncomplicatedCustomRoles/API/Features/CustomRole.cs @@ -15,6 +15,7 @@ using System.Text.RegularExpressions; using PlayerRoles; using UncomplicatedCustomRoles.API.Enums; +using UncomplicatedCustomRoles.API.Events; using UncomplicatedCustomRoles.API.Features.Behaviour; using UncomplicatedCustomRoles.API.Interfaces; using UncomplicatedCustomRoles.Compatibility; @@ -397,7 +398,7 @@ public static bool Unregister(int id, bool removeFromPlayers = false) foreach (var summoned in SummonedCustomRole.List.Values.Where(scr => scr.Role.Id == id).ToList()) summoned.Destroy(); - Events.CustomRoleEvents.OnUnregistered(new Events.CustomRoleUnregisteredEventArgs(customRole)); + CustomRoleEvents.OnUnregistered(new CustomRoleUnregisteredEventArgs(customRole)); return true; } @@ -422,23 +423,26 @@ internal static bool Validate(ICustomRole role, out string error) internal static LoadStatusType InternalRegister(ICustomRole customRole) { - RoleValidator.Validate(customRole, out var errors, out var warnings); + if (Plugin.Instance.Config.EnableValidator) + { + RoleValidator.Validate(customRole, out var errors, out var warnings); - foreach (var warning in warnings) - LogManager.Warn($"[Role Validator] {customRole}: {warning}"); + foreach (var warning in warnings) + LogManager.Warn($"[Role Validator] {customRole}: {warning}"); - if (errors.Count > 0) - return LoadStatusType.ValidatorError; + if (errors.Count > 0) + return LoadStatusType.ValidatorError; + } - var registeringArgs = new Events.CustomRoleRegisteringEventArgs(customRole); - Events.CustomRoleEvents.OnRegistering(registeringArgs); + var registeringArgs = new CustomRoleRegisteringEventArgs(customRole); + CustomRoleEvents.OnRegistering(registeringArgs); if (!registeringArgs.IsAllowed) return LoadStatusType.Denied; if (!CustomRoles.TryAdd(customRole.Id, customRole)) return LoadStatusType.SameId; - Events.CustomRoleEvents.OnRegistered(new Events.CustomRoleRegisteredEventArgs(customRole)); + CustomRoleEvents.OnRegistered(new CustomRoleRegisteredEventArgs(customRole)); return LoadStatusType.Success; } } \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Config.cs b/UncomplicatedCustomRoles/Config.cs index b9fc71a..a7eb65e 100644 --- a/UncomplicatedCustomRoles/Config.cs +++ b/UncomplicatedCustomRoles/Config.cs @@ -49,6 +49,9 @@ internal class Config [Description("Auto load the Custom Role ID from the file, bypassing YAML")] public bool UseIdFixer { get; set; } = false; + [Description("Enable the role validator. It shows warnings and errors in the console about the roles.")] + public bool EnableValidator { get; set; } = true; + [Description( "The content that will be replaced instead of {CUSTOM_ROLE} on your RespawnTimer display config if the current spectated player is a custom role. %customrole% is the role name")] public string RespawnTimerContent { get; set; } = "Player has custom role %customrole%"; diff --git a/UncomplicatedCustomRoles/Events/ServerEventHandler.cs b/UncomplicatedCustomRoles/Events/ServerEventHandler.cs index 34f6ebb..2d1b5c8 100644 --- a/UncomplicatedCustomRoles/Events/ServerEventHandler.cs +++ b/UncomplicatedCustomRoles/Events/ServerEventHandler.cs @@ -39,7 +39,8 @@ public void OnWaitingForPlayers() { Started = false; Plugin.Instance.OnFinishedLoadingPlugins(); - MapSpawnValidator.ValidateAll(); + if (Plugin.Instance.Config.EnableValidator) + MapSpawnValidator.ValidateAll(); } public void OnPlayersSpawned() @@ -63,6 +64,7 @@ public void OnRoundRestarted() { Announcer.SavedCustomAnnouncements.Clear(); + // Round-scoped state must not leak into the next round RespawnInventoryQueue.Clear(); RagdollAppearanceQueue.Clear(); TerminationQueue.Clear(); From 9f56bd845495d6b7de1a40d6e9137752c0728f34 Mon Sep 17 00:00:00 2001 From: MedveMarci Date: Wed, 15 Jul 2026 12:44:54 +0200 Subject: [PATCH 19/47] Cleanup again --- .../API/Events/CustomRoleEvents.cs | 2 +- UncomplicatedCustomRoles/API/Events/EventArgs.cs | 2 +- UncomplicatedCustomRoles/API/Features/CustomInfo.cs | 5 ++--- .../API/Features/SummonedCustomRole.cs | 3 ++- .../Extensions/CustomRoleExtension.cs | 2 +- .../Extensions/MirrorExtension.cs | 2 +- UncomplicatedCustomRoles/Manager/RoleValidator.cs | 12 +----------- UncomplicatedCustomRoles/Manager/SpawnManager.cs | 13 +++++++------ .../Patches/PlayerInfoSyncPatch.cs | 2 +- 9 files changed, 17 insertions(+), 26 deletions(-) diff --git a/UncomplicatedCustomRoles/API/Events/CustomRoleEvents.cs b/UncomplicatedCustomRoles/API/Events/CustomRoleEvents.cs index 67a7cd0..3e5863f 100644 --- a/UncomplicatedCustomRoles/API/Events/CustomRoleEvents.cs +++ b/UncomplicatedCustomRoles/API/Events/CustomRoleEvents.cs @@ -94,4 +94,4 @@ private static void InvokeSafely(Action ev, T args, string name) $"An exception has been thrown by an external handler of the event CustomRoleEvents.{name} ({handler.Method?.DeclaringType?.FullName}::{handler.Method?.Name}): {e}"); } } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/API/Events/EventArgs.cs b/UncomplicatedCustomRoles/API/Events/EventArgs.cs index 3390d6e..dc71dd7 100644 --- a/UncomplicatedCustomRoles/API/Events/EventArgs.cs +++ b/UncomplicatedCustomRoles/API/Events/EventArgs.cs @@ -95,4 +95,4 @@ public class CustomRoleRemovedEventArgs(SummonedCustomRole instance) /// Gets the that has been removed. /// public ICustomRole Role => Instance.Role; -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/API/Features/CustomInfo.cs b/UncomplicatedCustomRoles/API/Features/CustomInfo.cs index e071166..380d332 100644 --- a/UncomplicatedCustomRoles/API/Features/CustomInfo.cs +++ b/UncomplicatedCustomRoles/API/Features/CustomInfo.cs @@ -21,9 +21,8 @@ namespace UncomplicatedCustomRoles.API.Features; public class CustomInfo { - private Player _lastOwner; - private bool _detached; + private Player _lastOwner; public CustomInfo(string nickname, string role, string info) { @@ -84,7 +83,7 @@ public string Info } internal static bool SuppressExternalSync { get; set; } - + internal void Detach() { _detached = true; diff --git a/UncomplicatedCustomRoles/API/Features/SummonedCustomRole.cs b/UncomplicatedCustomRoles/API/Features/SummonedCustomRole.cs index 8b846da..e2cbd2b 100644 --- a/UncomplicatedCustomRoles/API/Features/SummonedCustomRole.cs +++ b/UncomplicatedCustomRoles/API/Features/SummonedCustomRole.cs @@ -19,6 +19,7 @@ using PlayerRoles.FirstPersonControl; using PlayerRoles.PlayableScps; using Respawning.Objectives; +using UncomplicatedCustomRoles.API.Events; using UncomplicatedCustomRoles.API.Features.Controllers; using UncomplicatedCustomRoles.API.Features.CustomModules; using UncomplicatedCustomRoles.API.Interfaces; @@ -408,7 +409,7 @@ public void Remove() _internalValid = false; if (wasValid) - API.Events.CustomRoleEvents.OnRemoved(new API.Events.CustomRoleRemovedEventArgs(this)); + CustomRoleEvents.OnRemoved(new CustomRoleRemovedEventArgs(this)); } /// diff --git a/UncomplicatedCustomRoles/Extensions/CustomRoleExtension.cs b/UncomplicatedCustomRoles/Extensions/CustomRoleExtension.cs index 12941f9..f8fee5e 100644 --- a/UncomplicatedCustomRoles/Extensions/CustomRoleExtension.cs +++ b/UncomplicatedCustomRoles/Extensions/CustomRoleExtension.cs @@ -100,4 +100,4 @@ public static bool Unregister(this ICustomRole role, bool removeFromPlayers = fa { return CustomRole.Unregister(role, removeFromPlayers); } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Extensions/MirrorExtension.cs b/UncomplicatedCustomRoles/Extensions/MirrorExtension.cs index 961451c..9a0ada5 100644 --- a/UncomplicatedCustomRoles/Extensions/MirrorExtension.cs +++ b/UncomplicatedCustomRoles/Extensions/MirrorExtension.cs @@ -273,7 +273,7 @@ public static void ChangeAppearance(this Player player, RoleTypeId type, IEnumer fpc = playerfpc; ushort value = 0; - fpc?.FpcModule.MouseLook.GetSyncValues(0, out value, out var _); + fpc?.FpcModule.MouseLook.GetSyncValues(0, out value, out _); writer.WriteRelativePosition(new RelativePosition(player.Position)); writer.WriteUShort(value); } diff --git a/UncomplicatedCustomRoles/Manager/RoleValidator.cs b/UncomplicatedCustomRoles/Manager/RoleValidator.cs index 2ef82fc..fd335c2 100644 --- a/UncomplicatedCustomRoles/Manager/RoleValidator.cs +++ b/UncomplicatedCustomRoles/Manager/RoleValidator.cs @@ -302,18 +302,8 @@ private static void ValidateMisc(ICustomRole role, List warnings) if (role.SpawnHintDuration < 0) warnings.Add($"'spawn_hint_duration' is negative ({role.SpawnHintDuration})."); - if (!string.IsNullOrEmpty(role.SpawnBroadcast) && role.SpawnBroadcastDuration == 0) - warnings.Add( - "'spawn_broadcast' is set but 'spawn_broadcast_duration' is 0; the broadcast would disappear instantly."); - - if (!string.IsNullOrEmpty(role.SpawnHint) && role.SpawnHintDuration == 0) - warnings.Add("'spawn_hint' is set but 'spawn_hint_duration' is 0; the hint would disappear instantly."); - - if (role.Scale.x == 0 && role.Scale.y == 0 && role.Scale.z == 0) + if (role.Scale is { x: 0, y: 0, z: 0 }) warnings.Add("'scale' is 0 on every axis; the player would be invisible. Use 1 for the normal size."); - else if (role.Scale.x < 0 || role.Scale.y < 0 || role.Scale.z < 0) - warnings.Add( - $"'scale' has a negative axis ({role.Scale.x}, {role.Scale.y}, {role.Scale.z}); the model would be mirrored/broken."); } private static void ValidateSpawnSettings(ICustomRole role, List errors, List warnings) diff --git a/UncomplicatedCustomRoles/Manager/SpawnManager.cs b/UncomplicatedCustomRoles/Manager/SpawnManager.cs index bf97447..36516a4 100644 --- a/UncomplicatedCustomRoles/Manager/SpawnManager.cs +++ b/UncomplicatedCustomRoles/Manager/SpawnManager.cs @@ -25,6 +25,7 @@ using PlayerStatsSystem; using Subtitles; using UncomplicatedCustomRoles.API.Enums; +using UncomplicatedCustomRoles.API.Events; using UncomplicatedCustomRoles.API.Features; using UncomplicatedCustomRoles.API.Features.Controllers; using UncomplicatedCustomRoles.API.Features.CustomModules; @@ -134,8 +135,8 @@ public static void SummonCustomSubclass(Player player, int id, bool doBypassRole return; } - var spawningArgs = new API.Events.CustomRoleSpawningEventArgs(player, Role); - API.Events.CustomRoleEvents.OnSpawning(spawningArgs); + var spawningArgs = new CustomRoleSpawningEventArgs(player, Role); + CustomRoleEvents.OnSpawning(spawningArgs); if (!spawningArgs.IsAllowed) { LogManager.Debug( @@ -279,8 +280,8 @@ internal static void SummonSubclassApplier(Player Player, ICustomRole Role, bool { if (!spawningEventAlreadyFired) { - var spawningArgs = new API.Events.CustomRoleSpawningEventArgs(Player, Role); - API.Events.CustomRoleEvents.OnSpawning(spawningArgs); + var spawningArgs = new CustomRoleSpawningEventArgs(Player, Role); + CustomRoleEvents.OnSpawning(spawningArgs); if (!spawningArgs.IsAllowed) { LogManager.Debug( @@ -462,7 +463,7 @@ internal static void SummonSubclassApplier(Player Player, ICustomRole Role, bool if (API.Features.Escape.Bucket.Contains(Player.PlayerId)) API.Features.Escape.Bucket.Remove(Player.PlayerId); - API.Events.CustomRoleEvents.OnSpawned(new API.Events.CustomRoleSpawnedEventArgs(roleInstance)); + CustomRoleEvents.OnSpawned(new CustomRoleSpawnedEventArgs(roleInstance)); LogManager.Debug($"{Player} successfully spawned as {Role.Name} ({Role.Id})! [2VDS]"); } @@ -627,7 +628,7 @@ static IEnumerable ExtractPermissions(object obj) } } } - + for (var a = 0; a < Role.SpawnSettings.SpawnChance; a++) candidates.Add(Role); } diff --git a/UncomplicatedCustomRoles/Patches/PlayerInfoSyncPatch.cs b/UncomplicatedCustomRoles/Patches/PlayerInfoSyncPatch.cs index 3465414..d702bb6 100644 --- a/UncomplicatedCustomRoles/Patches/PlayerInfoSyncPatch.cs +++ b/UncomplicatedCustomRoles/Patches/PlayerInfoSyncPatch.cs @@ -43,7 +43,7 @@ private static void Prefix(NicknameSync __instance, ref PlayerInfoArea value) if (CustomInfo.SuppressExternalSync) return; - if (__instance._hub is not null && __instance._hub.TryGetSummonedInstance(out var _)) + if (__instance._hub is not null && __instance._hub.TryGetSummonedInstance(out _)) { value |= PlayerInfoArea.CustomInfo; value &= ~PlayerInfoArea.Role; From 9f97077b797a49930a80de291c82ae6588748aa6 Mon Sep 17 00:00:00 2001 From: MedveMarci Date: Wed, 15 Jul 2026 13:16:33 +0200 Subject: [PATCH 20/47] Fixed OverrideRpNames function; the coroutine stops if the player's role gets removed; fixed CustomKeycard Placeholders with RpNames; --- .../API/Features/SummonedCustomRole.cs | 16 +++- .../Manager/PlaceholderManager.cs | 11 ++- .../Manager/RoleValidator.cs | 5 -- .../Manager/SpawnManager.cs | 78 +++++++++---------- 4 files changed, 62 insertions(+), 48 deletions(-) diff --git a/UncomplicatedCustomRoles/API/Features/SummonedCustomRole.cs b/UncomplicatedCustomRoles/API/Features/SummonedCustomRole.cs index e2cbd2b..24881e3 100644 --- a/UncomplicatedCustomRoles/API/Features/SummonedCustomRole.cs +++ b/UncomplicatedCustomRoles/API/Features/SummonedCustomRole.cs @@ -145,10 +145,21 @@ internal SummonedCustomRole(Player player, ICustomRole role, Triplet InfiniteEffects { get; } /// - /// Gets the current nickname of the player - if null the role didn't changed it! + /// Gets whether the player has a custom nickname applied by UCR /// public bool IsCustomNickname { get; } + /// + /// Gets the nickname UCR applied to the player for this role (already resolved from placeholders), + /// or if the role didn't change the nickname.

+ ///
+ public string AppliedNickname { get; internal set; } + + /// + /// Gets the of the delayed override_rpnames nickname re-apply. + /// + internal CoroutineHandle NicknameReapplyCoroutine { get; set; } + /// /// Gets the instance of the current instance /// @@ -380,6 +391,9 @@ public void Remove() if (IsDefaultCoroutineRole && GenericCoroutine.IsRunning) Timing.KillCoroutines(GenericCoroutine); + if (NicknameReapplyCoroutine.IsRunning) + Timing.KillCoroutines(NicknameReapplyCoroutine); + // Remove effects Player.DisableAllEffects(); InfiniteEffects.Clear(); diff --git a/UncomplicatedCustomRoles/Manager/PlaceholderManager.cs b/UncomplicatedCustomRoles/Manager/PlaceholderManager.cs index 7da2957..6970f5c 100644 --- a/UncomplicatedCustomRoles/Manager/PlaceholderManager.cs +++ b/UncomplicatedCustomRoles/Manager/PlaceholderManager.cs @@ -11,6 +11,7 @@ using System.Collections.Generic; using LabApi.Features.Wrappers; using Respawning.NamingRules; +using UncomplicatedCustomRoles.API.Features; using UncomplicatedCustomRoles.API.Interfaces; using UncomplicatedCustomRoles.Extensions; using UnityEngine; @@ -25,7 +26,7 @@ public static string ApplyPlaceholders(string? origin, Player player, ICustomRol return (origin ?? string.Empty).BulkReplace(new Dictionary { { "nick", player.Nickname }, - { "displayname", player.DisplayName }, + { "displayname", ResolveDisplayName(player) }, { "rand", Random.Range(0, 10) }, { "dnumber", Random.Range(1000, 10000) }, { "unitid", player.UnitId }, @@ -47,4 +48,12 @@ public static string ApplyPlaceholders(string? origin, Player player, ICustomRol { "max_hume", player.MaxHumeShield } }, "%%"); } + + private static string ResolveDisplayName(Player player) + { + if (SummonedCustomRole.TryGet(player, out var summoned) && !string.IsNullOrEmpty(summoned.AppliedNickname)) + return summoned.AppliedNickname!; + + return player.DisplayName; + } } \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Manager/RoleValidator.cs b/UncomplicatedCustomRoles/Manager/RoleValidator.cs index fd335c2..6c1eb87 100644 --- a/UncomplicatedCustomRoles/Manager/RoleValidator.cs +++ b/UncomplicatedCustomRoles/Manager/RoleValidator.cs @@ -9,7 +9,6 @@ */ using System; -using System.Collections; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; @@ -358,10 +357,6 @@ private static void ValidateSpawnSettings(ICustomRole role, List errors, $"'spawn_settings.can_replace_roles' lists '{duplicate.Key}' {duplicate.Count()} times, which multiplies the spawn chance for that role - remove the duplicates unless that is intended."); } - if (role.SpawnSettings.RequiredPermission is IDictionary) - warnings.Add( - "'spawn_settings.required_permission' is a mapping; it must be a single permission string or a list of permission strings."); - if (role.SpawnSettings.SpawnZones is not null) foreach (var zone in role.SpawnSettings.SpawnZones.Where(z => z is FacilityZone.None)) warnings.Add( diff --git a/UncomplicatedCustomRoles/Manager/SpawnManager.cs b/UncomplicatedCustomRoles/Manager/SpawnManager.cs index 36516a4..f6c1f0a 100644 --- a/UncomplicatedCustomRoles/Manager/SpawnManager.cs +++ b/UncomplicatedCustomRoles/Manager/SpawnManager.cs @@ -274,7 +274,12 @@ public static void SummonCustomSubclass(Player player, int id, bool doBypassRole } } - internal static void SummonSubclassApplier(Player Player, ICustomRole Role, bool spawningEventAlreadyFired = false) + public static void SummonSubclassApplier(Player Player, ICustomRole Role) + { + SummonSubclassApplier(Player, Role, false); + } + + internal static void SummonSubclassApplier(Player Player, ICustomRole Role, bool spawningEventAlreadyFired) { try { @@ -329,39 +334,27 @@ internal static void SummonSubclassApplier(Player Player, ICustomRole Role, bool Player.AddAmmo(Ammo.Key, Ammo.Value); } - // Reset the inventory if we need to add the old one - if (PlayerEventHandler.RespawnInventoryQueue.TryGetValue(Player.PlayerId, out var oldInventory)) + if (PlayerEventHandler.RespawnInventoryQueue.TryRemove(Player.PlayerId, out var oldInventory)) { - Player.ClearInventory(); - Player.ClearAmmo(); + if (!oldInventory.Item3) + { + Player.ClearInventory(); + Player.ClearAmmo(); - foreach (var item in oldInventory.Item1) - if (!oldInventory.Item3) - { + foreach (var item in oldInventory.Item1) Player.AddItem(item); - } - else - { - var pickup = Pickup.Create(item, Player.Position); - if (pickup is null) - continue; - pickup.Spawn(); - } - foreach (var item in oldInventory.Item2) - if (!oldInventory.Item3) - { - Player.Inventory.ServerAddAmmo(item.Key, item.Value); - } - else - { - var pickup = Pickup.Create(item.Key, Player.Position); - if (pickup is null) - continue; - pickup.Spawn(); - } + foreach (var ammo in oldInventory.Item2) + Player.Inventory.ServerAddAmmo(ammo.Key, ammo.Value); + } + else + { + foreach (var item in oldInventory.Item1) + Pickup.Create(item, Player.Position)?.Spawn(); - PlayerEventHandler.RespawnInventoryQueue.TryRemove(Player.PlayerId, out _); + foreach (var ammo in oldInventory.Item2) + Pickup.Create(ammo.Key, Player.Position)?.Spawn(); + } } var InfoArea = Player.ReferenceHub.nicknameSync.Network_playerInfoToShow; @@ -424,22 +417,13 @@ internal static void SummonSubclassApplier(Player Player, ICustomRole Role, bool // Changing nickname if needed var ChangedNick = false; + string appliedNick = null; if (Plugin.Instance.Config.AllowNicknameEdit && !string.IsNullOrEmpty(Role.Nickname)) { var Nick = PlaceholderManager.ApplyPlaceholders(Role.Nickname, Player, Role); - if (Role.Nickname.Contains(",")) - Player.DisplayName = Nick.Split(',').RandomItem(); - else - Player.DisplayName = Nick; - if (Plugin.Instance.Config.OverrideRpNames) - Timing.CallDelayed(3f, () => // Override RPNames shit (sowwy andrew) - { - if (Role.Nickname.Contains(",")) - Player.DisplayName = Nick.Split(',').RandomItem(); - else - Player.DisplayName = Nick; - }); + appliedNick = Role.Nickname.Contains(",") ? Nick.Split(',').RandomItem().Trim() : Nick; + Player.DisplayName = appliedNick; ChangedNick = true; } @@ -452,8 +436,20 @@ internal static void SummonSubclassApplier(Player Player, ICustomRole Role, bool SummonedCustomRole roleInstance = new(Player, Role, Badge, PermanentEffects, InfoArea, customInfo, ChangedNick); + roleInstance.AppliedNickname = appliedNick; + + if (appliedNick is not null) + customInfo.Nickname = appliedNick; + customInfo.UpdateInfo(Player); + if (appliedNick is not null && Plugin.Instance.Config.OverrideRpNames) + roleInstance.NicknameReapplyCoroutine = Timing.CallDelayed(3f, () => + { + if (roleInstance.IsValid && SummonedCustomRole.Get(roleInstance.Player) == roleInstance) + roleInstance.Player.DisplayName = appliedNick; + }); + var escapeController = Player.GameObject.AddComponent(); escapeController.Init(roleInstance); From 8c3a4565e66b7553b4c0de69756eaa16e99f217b Mon Sep 17 00:00:00 2001 From: MedveMarci Date: Mon, 20 Jul 2026 19:12:23 +0200 Subject: [PATCH 21/47] Refactor CustomInfo and add FlagMigrator for deprecated flag handling; implement InfoTag module for custom info display --- .../API/Features/CustomInfo.cs | 72 ++++++-- .../API/Features/CustomModules/InfoTag.cs | 126 ++++++++++++++ .../API/Features/CustomRole.cs | 2 + UncomplicatedCustomRoles/Commands/Reload.cs | 1 + UncomplicatedCustomRoles/Commands/Update.cs | 61 +++++-- .../Manager/FlagMigrator.cs | 154 ++++++++++++++++++ .../Manager/InfoColors.cs | 73 +++++++++ UncomplicatedCustomRoles/Plugin.cs | 1 + 8 files changed, 460 insertions(+), 30 deletions(-) create mode 100644 UncomplicatedCustomRoles/API/Features/CustomModules/InfoTag.cs create mode 100644 UncomplicatedCustomRoles/Manager/FlagMigrator.cs create mode 100644 UncomplicatedCustomRoles/Manager/InfoColors.cs diff --git a/UncomplicatedCustomRoles/API/Features/CustomInfo.cs b/UncomplicatedCustomRoles/API/Features/CustomInfo.cs index 380d332..5693be0 100644 --- a/UncomplicatedCustomRoles/API/Features/CustomInfo.cs +++ b/UncomplicatedCustomRoles/API/Features/CustomInfo.cs @@ -21,9 +21,10 @@ namespace UncomplicatedCustomRoles.API.Features; public class CustomInfo { - private bool _detached; private Player _lastOwner; + private bool _detached; + public CustomInfo(string nickname, string role, string info) { Nickname = nickname; @@ -131,6 +132,37 @@ public void UpdateInfo(Player player) { rawInfo = PlaceholderManager.ApplyPlaceholders(rawInfo, player, summonedCustomRole.Role); + var infoTeam = summonedCustomRole.Role.Role.GetTeam(); + if (DisguiseTeam.List.TryGetValue(player.PlayerId, out var infoFakeTeam)) + infoTeam = infoFakeTeam; + + var rawUnit = string.Empty; + var showUnit = false; + if (!string.IsNullOrEmpty(rawRole) && !summonedCustomRole.HasModule() + && infoTeam is Team.FoundationForces + && NamingRulesManager.TryGetNamingRule(infoTeam, out var infoUnitRule) + && !string.IsNullOrEmpty(infoUnitRule.LastGeneratedName)) + { + showUnit = true; + rawUnit = infoUnitRule.LastGeneratedName; + } + + if (summonedCustomRole.TryGetModule(out InfoTag infoTag)) + { + if (infoTag.ShowBadge) + player.InfoArea |= PlayerInfoArea.Badge; + else + player.InfoArea &= ~PlayerInfoArea.Badge; + + if (infoTag.ShowPowerStatus) + player.InfoArea |= PlayerInfoArea.PowerStatus; + else + player.InfoArea &= ~PlayerInfoArea.PowerStatus; + + ApplyCustomInfo(player, infoTag.Compose(player, rawInfo, rawNickname, rawRole, rawUnit, showUnit)); + return; + } + if (summonedCustomRole.TryGetModule(out CustomInfoOrder customInfoOrderModule)) rawCustomInfo = $"{customInfoOrderModule.Order}"; @@ -160,16 +192,8 @@ public void UpdateInfo(Player player) } } - var roleTeam = summonedCustomRole.Role.Role.GetTeam(); - if (DisguiseTeam.List.TryGetValue(player.PlayerId, out var fakeTeam)) - roleTeam = fakeTeam; - - if (!string.IsNullOrEmpty(rawRole) && !summonedCustomRole.HasModule() - && roleTeam is Team.FoundationForces - && NamingRulesManager.TryGetNamingRule(roleTeam, - out var unitNamingRule) - && !string.IsNullOrEmpty(unitNamingRule.LastGeneratedName)) - rawRole = $"{rawRole} ({unitNamingRule.LastGeneratedName})"; + if (showUnit) + rawRole = $"{rawRole} ({rawUnit})"; } else { @@ -189,7 +213,7 @@ public void UpdateInfo(Player player) return; } - player.CustomInfo = rawCustomInfo.Replace("%%", "%\n%").BulkReplace(new Dictionary + ApplyCustomInfo(player, rawCustomInfo.Replace("%%", "%\n%").BulkReplace(new Dictionary { { "custominfo", @@ -203,11 +227,33 @@ public void UpdateInfo(Player player) "rolename", rawRole } - }, "%%"); + }, "%%")); } finally { SuppressExternalSync = previousSuppress; } } + + private static void ApplyCustomInfo(Player player, string composed) + { + if (!string.IsNullOrEmpty(composed) && !NicknameSync.ValidateCustomInfo(composed, out var error)) + { + LogManager.Error( + $"The name tag of player {player.PlayerId} would be rejected by the game and won't be shown: {error}\n" + + $"Composed tag: {composed}\n" + + "Likely causes: a colour that isn't on the allowed list written inside 'custom_info', a '[' or ']' coming from a nickname, or a tag longer than 400 characters."); + composed = string.Empty; + } + + if (string.IsNullOrEmpty(composed)) + { + player.InfoArea |= PlayerInfoArea.Nickname | PlayerInfoArea.Role | PlayerInfoArea.UnitName; + player.CustomInfo = string.Empty; + } + else + { + player.CustomInfo = composed; + } + } } \ No newline at end of file diff --git a/UncomplicatedCustomRoles/API/Features/CustomModules/InfoTag.cs b/UncomplicatedCustomRoles/API/Features/CustomModules/InfoTag.cs new file mode 100644 index 0000000..130e3aa --- /dev/null +++ b/UncomplicatedCustomRoles/API/Features/CustomModules/InfoTag.cs @@ -0,0 +1,126 @@ +/* + * This file is a part of the UncomplicatedCustomRoles project. + * + * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) + * + * This file is licensed under the GNU Affero General Public License v3.0. + * You should have received a copy of the AGPL license along with this file. + * If not, see . + */ + +using System.Linq; +using System.Text.RegularExpressions; +using LabApi.Features.Wrappers; +using MEC; +using UncomplicatedCustomRoles.Manager; + +namespace UncomplicatedCustomRoles.API.Features.CustomModules; +#nullable enable + +public class InfoTag : CustomModule +{ + internal const string DefaultOrder = "%custominfo%%nickname%%rolename% %unitname%"; + + private static readonly string[] KnownTokens = ["custominfo", "nickname", "rolename", "unitname"]; + + private static readonly Regex TokenRegex = new("%([a-zA-Z_]+)%", RegexOptions.Compiled); + + private static readonly Regex MultiNewline = new("\n{2,}", RegexOptions.Compiled); + + internal string Order => TryGetStringValue("order", DefaultOrder); + + internal string UnitFormat => TryGetStringValue("unit_format", "({unit})"); + + internal bool ShowUnitName => TryGetCastedValue("show_unitname", true); + + internal bool ShowBadge => TryGetCastedValue("show_badge", true); + + internal bool ShowPowerStatus => TryGetCastedValue("show_powerstatus", true); + + private (string Token, string Color, bool Bold)[] Parts => + [ + ("custominfo", TryGetStringValue("custominfo_color"), TryGetCastedValue("custominfo_bold", false)), + ("nickname", TryGetStringValue("nickname_color"), TryGetCastedValue("nickname_bold", false)), + ("rolename", TryGetStringValue("rolename_color"), TryGetCastedValue("rolename_bold", false)), + ("unitname", TryGetStringValue("unitname_color"), TryGetCastedValue("unitname_bold", false)) + ]; + + public override bool Validate(out string error) + { + foreach (var (token, color, _) in Parts) + if (!string.IsNullOrWhiteSpace(color) && !InfoColors.TryResolve(color, out _)) + { + error = + $"'{token}_color' '{color}' is not a colour the game allows on the name tag. Allowed names: {string.Join(", ", InfoColors.Names)} (or an accepted hex code)."; + return false; + } + + if (UnitFormat.Contains("[") || UnitFormat.Contains("]")) + { + error = $"'unit_format' cannot contain square brackets ('[' or ']'), got '{UnitFormat}'. Use () instead."; + return false; + } + + var tokens = TokenRegex.Matches(Order).Cast().Select(m => m.Groups[1].Value).ToList(); + + var unknown = tokens.Where(t => !KnownTokens.Contains(t, System.StringComparer.OrdinalIgnoreCase)).Distinct() + .ToList(); + if (unknown.Count > 0) + LogManager.Warn( + $"[CustomModule] InfoTag 'order' contains unknown token(s): {string.Join(", ", unknown.Select(t => $"%{t}%"))}; they will be shown as-is. Valid tokens: %custominfo%, %nickname%, %rolename%, %unitname%."); + + if (!tokens.Any(t => KnownTokens.Contains(t, System.StringComparer.OrdinalIgnoreCase))) + { + error = + "'order' must contain at least one of %custominfo%, %nickname%, %rolename% or %unitname%; otherwise the name tag would show static text only."; + return false; + } + + error = null!; + return true; + } + + internal string Compose(Player player, string customInfoText, string nickname, string roleName, string unitName, + bool showUnit) + { + var parts = Parts.ToDictionary(p => p.Token, p => (p.Color, p.Bold)); + + var template = Order.Replace("%%", "%\n%"); + var result = TokenRegex.Replace(template, m => Render(m.Groups[1].Value.ToLowerInvariant())); + + result = MultiNewline.Replace(result, "\n").Trim('\n', ' '); + + return string.IsNullOrEmpty(result) ? string.Empty : $"{result}"; + + string Render(string token) + { + var content = token switch + { + "custominfo" => customInfoText, + "nickname" => string.IsNullOrEmpty(nickname) ? player.Nickname : nickname, + "rolename" => roleName, + "unitname" => showUnit && ShowUnitName && !string.IsNullOrEmpty(unitName) + ? UnitFormat.Replace("{unit}", unitName) + : string.Empty, + _ => $"%{token}%" + }; + + if (string.IsNullOrEmpty(content) || !parts.TryGetValue(token, out var style)) + return content; + + if (style.Bold) + content = $"{content}"; + + if (!string.IsNullOrWhiteSpace(style.Color) && InfoColors.TryResolve(style.Color, out var hex)) + content = $"{content}"; + + return content; + } + } + + public override void OnAdded() + { + Timing.CallDelayed(Timing.WaitForOneFrame, () => { CustomRole.CustomInfo.UpdateInfo(CustomRole.Player); }); + base.OnAdded(); + } +} diff --git a/UncomplicatedCustomRoles/API/Features/CustomRole.cs b/UncomplicatedCustomRoles/API/Features/CustomRole.cs index 639cba5..89ecd64 100644 --- a/UncomplicatedCustomRoles/API/Features/CustomRole.cs +++ b/UncomplicatedCustomRoles/API/Features/CustomRole.cs @@ -423,6 +423,8 @@ internal static bool Validate(ICustomRole role, out string error) internal static LoadStatusType InternalRegister(ICustomRole customRole) { + FlagMigrator.Migrate(customRole); + if (Plugin.Instance.Config.EnableValidator) { RoleValidator.Validate(customRole, out var errors, out var warnings); diff --git a/UncomplicatedCustomRoles/Commands/Reload.cs b/UncomplicatedCustomRoles/Commands/Reload.cs index 6393b83..e6c192e 100644 --- a/UncomplicatedCustomRoles/Commands/Reload.cs +++ b/UncomplicatedCustomRoles/Commands/Reload.cs @@ -36,6 +36,7 @@ public bool Executor(List arguments, ICommandSender sender, out string r CustomRole.CustomRoles = new ConcurrentDictionary(); CustomRole.NotLoadedRoles.Clear(); CustomRole.OutdatedRoles.Clear(); + FlagMigrator.Migrated.Clear(); ImportManager.Unload(); FileConfigs.LoadAll(); diff --git a/UncomplicatedCustomRoles/Commands/Update.cs b/UncomplicatedCustomRoles/Commands/Update.cs index 4dec3e2..150b575 100644 --- a/UncomplicatedCustomRoles/Commands/Update.cs +++ b/UncomplicatedCustomRoles/Commands/Update.cs @@ -16,6 +16,7 @@ using UncomplicatedCustomRoles.API.Features; using UncomplicatedCustomRoles.API.Interfaces; using UncomplicatedCustomRoles.Compatibility; +using UncomplicatedCustomRoles.Manager; namespace UncomplicatedCustomRoles.Commands; @@ -23,7 +24,8 @@ public class Update : IUCRCommand { public string Name { get; } = "update"; - public string Description { get; } = "Update one or more outdated (but loaded) CustomRole(s)"; + public string Description { get; } = + "Rewrite one or more loaded CustomRole config files to the latest format (outdated roles and deprecated flags)"; public string RequiredPermission { get; } = "ucr.update"; @@ -36,33 +38,58 @@ public bool Executor(List arguments, ICommandSender sender, out string r return false; } + var updated = 0; + if (arguments[0].ToLower() is "all") { - foreach (var role in CustomRole.OutdatedRoles) - UpdateRole(role); + foreach (var role in CustomRole.OutdatedRoles.ToList()) + if (UpdateRole(role)) + updated++; + + foreach (var role in FlagMigrator.Migrated.ToList()) + if (PersistMigrated(role)) + updated++; + } + else if (int.TryParse(arguments[0], out var id)) + { + var outdated = CustomRole.OutdatedRoles.FirstOrDefault(r => r.CustomRole.Id == id); + var migrated = FlagMigrator.Migrated.FirstOrDefault(r => r.Id == id); + + if (outdated is not null && UpdateRole(outdated)) + updated++; + if (migrated is not null && PersistMigrated(migrated)) + updated++; + + if (outdated is null && migrated is null) + response = $"CustomRole {arguments[0]} is not outdated / doesn't need a config update!"; } else { - if (int.TryParse(arguments[0], out var id)) - { - var role = CustomRole.OutdatedRoles.FirstOrDefault(r => r.CustomRole.Id == id); - if (role is not null) - UpdateRole(role); - else - response = $"CustomRole {arguments[0]} not found!"; - } - else - { - response = $"CustomRole {arguments[0]} not found!"; - } + response = $"CustomRole {arguments[0]} not found!"; } - response ??= "Successfully updated CustomRole(s)!"; + response ??= updated > 0 + ? $"Successfully updated {updated} CustomRole config file(s)!" + : "Nothing to update."; return true; } - private static void UpdateRole(OutdatedCustomRole role) + private static bool UpdateRole(OutdatedCustomRole role) { + if (string.IsNullOrEmpty(role.Path)) + return false; + File.WriteAllText(role.Path, YamlConfigParser.Serializer.Serialize(role.CustomRole)); + return true; + } + + private static bool PersistMigrated(ICustomRole role) + { + if (!CompatibilityManager.RolePaths.TryGetValue(role, out var path) || string.IsNullOrEmpty(path)) + return false; + + File.WriteAllText(path, YamlConfigParser.Serializer.Serialize(role)); + FlagMigrator.Migrated.Remove(role); + return true; } } \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Manager/FlagMigrator.cs b/UncomplicatedCustomRoles/Manager/FlagMigrator.cs new file mode 100644 index 0000000..86d72ad --- /dev/null +++ b/UncomplicatedCustomRoles/Manager/FlagMigrator.cs @@ -0,0 +1,154 @@ +/* + * This file is a part of the UncomplicatedCustomRoles project. + * + * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) + * + * This file is licensed under the GNU Affero General Public License v3.0. + * You should have received a copy of the AGPL license along with this file. + * If not, see . + */ + +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using UncomplicatedCustomRoles.API.Interfaces; + +namespace UncomplicatedCustomRoles.Manager; +#nullable enable + +internal static class FlagMigrator +{ + private static readonly Regex RoleNameToken = new("%rolename%", RegexOptions.Compiled | RegexOptions.IgnoreCase); + internal static List Migrated { get; } = []; + + internal static void Migrate(ICustomRole role) + { + if (role.CustomFlags is not { Count: > 0 } flags) + return; + + string? order = null; + string? nickColor = null; + var hasOrder = false; + var hasColor = false; + var hasNoUnit = false; + var hasInfoTag = false; + List deprecated = []; + + foreach (var flag in flags) + { + var (name, args) = Parse(flag); + switch (name?.ToLowerInvariant()) + { + case "infotag": + hasInfoTag = true; + break; + case "custominfoorder": + hasOrder = true; + deprecated.Add(flag); + if (args is not null && args.TryGetValue("order", out var o)) + order = o?.ToString(); + break; + case "colorfulnickname": + hasColor = true; + deprecated.Add(flag); + if (args is not null && args.TryGetValue("color", out var c)) + nickColor = c?.ToString(); + break; + case "nounitname": + hasNoUnit = true; + deprecated.Add(flag); + break; + } + } + + if (deprecated.Count == 0) + return; + + var used = DeprecatedList(hasOrder, hasColor, hasNoUnit); + + if (hasInfoTag) + { + foreach (var flag in deprecated) + flags.Remove(flag); + + LogManager.Warn( + $"[Flag Migrator] Role {role} uses both the new 'InfoTag' flag and the deprecated name-tag flag(s) {used}. " + + "The deprecated one(s) were ignored; please remove them from your config."); + return; + } + + var infoOrder = string.IsNullOrEmpty(order) ? InfoTagDefaultOrder : order!; + var infoArgs = new Dictionary(); + + if (hasNoUnit) + infoArgs["show_unitname"] = false; + else if (RoleNameToken.IsMatch(infoOrder)) + infoOrder = RoleNameToken.Replace(infoOrder, "%rolename% %unitname%"); + + + infoArgs["order"] = infoOrder; + if (hasColor && !string.IsNullOrEmpty(nickColor)) + infoArgs["nickname_color"] = nickColor!; + + foreach (var flag in deprecated) + flags.Remove(flag); + + flags.Add(new Dictionary { ["InfoTag"] = infoArgs }); + + if (!Migrated.Contains(role)) + Migrated.Add(role); + + LogManager.Warn( + $"[Flag Migrator] Role {role} still uses the deprecated name-tag flag(s) {used}. " + + "They were automatically migrated to the 'InfoTag' flag.\n" + + $"To persist this to the config file automatically run 'ucr update {role.Id}' (or 'ucr update all'), " + + "or replace those flags manually in your custom_flags with:\n" + + RenderYaml(infoArgs)); + } + + private const string InfoTagDefaultOrder = "%custominfo%%nickname%%rolename%"; + + private static string DeprecatedList(bool order, bool color, bool noUnit) + { + List names = []; + if (order) names.Add("CustomInfoOrder"); + if (color) names.Add("ColorfulNickname"); + if (noUnit) names.Add("NoUnitName"); + return string.Join(", ", names); + } + + private static (string? name, Dictionary? args) Parse(object flag) + { + switch (flag) + { + case string s: + return (s, null); + case Dictionary d: + foreach (var kv in d) + { + var args = kv.Value as Dictionary; + return (kv.Key?.ToString(), + args?.ToDictionary(x => x.Key.ToString(), x => x.Value)); + } + + return (null, null); + default: + return (null, null); + } + } + + private static string RenderYaml(Dictionary infoArgs) + { + var sb = new StringBuilder(); + sb.AppendLine("custom_flags:"); + sb.AppendLine("- InfoTag:"); + foreach (var kv in infoArgs) + { + var value = kv.Value is bool b ? b ? "true" : "false" : $"\"{kv.Value}\""; + sb.AppendLine($" {kv.Key}: {value}"); + } + + return sb.ToString().TrimEnd(); + } +} diff --git a/UncomplicatedCustomRoles/Manager/InfoColors.cs b/UncomplicatedCustomRoles/Manager/InfoColors.cs new file mode 100644 index 0000000..61bfc16 --- /dev/null +++ b/UncomplicatedCustomRoles/Manager/InfoColors.cs @@ -0,0 +1,73 @@ +/* + * This file is a part of the UncomplicatedCustomRoles project. + * + * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) + * + * This file is licensed under the GNU Affero General Public License v3.0. + * You should have received a copy of the AGPL license along with this file. + * If not, see . + */ + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace UncomplicatedCustomRoles.Manager; +#nullable enable + +internal static class InfoColors +{ + private static readonly Dictionary NameToHex = new(StringComparer.OrdinalIgnoreCase) + { + { "pink", "FF96DE" }, + { "red", "C50000" }, + { "brown", "944710" }, + { "silver", "A0A0A0" }, + { "lightgreen", "32CD32" }, + { "crimson", "DC143C" }, + { "cyan", "00B7EB" }, + { "aqua", "00FFFF" }, + { "deeppink", "FF1493" }, + { "tomato", "FF6448" }, + { "yellow", "FAFF86" }, + { "magenta", "FF0090" }, + { "bluegreen", "4DFFB8" }, + { "orange", "FF9966" }, + { "lime", "BFFF00" }, + { "green", "228B22" }, + { "emerald", "50C878" }, + { "carmine", "960018" }, + { "nickel", "727472" }, + { "mint", "98FB98" }, + { "armygreen", "4B5320" }, + { "pumpkin", "EE7600" }, + { "white", "FFFFFF" }, + { "black", "000000" } + }; + + internal static IEnumerable Names => NameToHex.Keys; + + internal static bool TryResolve(string? input, out string hex) + { + hex = string.Empty; + + if (string.IsNullOrWhiteSpace(input)) + return false; + + var raw = input!.Trim().TrimStart('#').Replace("_", string.Empty).Replace("-", string.Empty).Replace(" ", string.Empty); + + if (NameToHex.TryGetValue(raw, out var mapped)) + { + hex = mapped; + return true; + } + + if (raw.Length == 6 && NameToHex.Values.Any(h => string.Equals(h, raw, StringComparison.OrdinalIgnoreCase))) + { + hex = raw.ToUpperInvariant(); + return true; + } + + return false; + } +} diff --git a/UncomplicatedCustomRoles/Plugin.cs b/UncomplicatedCustomRoles/Plugin.cs index c3e2092..fbf2750 100644 --- a/UncomplicatedCustomRoles/Plugin.cs +++ b/UncomplicatedCustomRoles/Plugin.cs @@ -63,6 +63,7 @@ public override void Enable() CustomRole.CustomRoles.Clear(); CustomRole.NotLoadedRoles.Clear(); + FlagMigrator.Migrated.Clear(); EventHandlerBase.Register(new List { From 341b97de99a2eb34668ac3ae3f18618c62f9b85a Mon Sep 17 00:00:00 2001 From: MedveMarci Date: Mon, 20 Jul 2026 19:13:36 +0200 Subject: [PATCH 22/47] Fixed custom category limits --- .../Events/ServerEventHandler.cs | 1 + .../Extensions/PlayerExtension.cs | 51 +++++++++--------- .../Manager/InventoryLimitOverride.cs | 45 ++++++++++++++++ .../Manager/SpawnManager.cs | 1 - .../Patches/CategoryLimitPatch.cs | 53 +++++++++++++++++++ 5 files changed, 123 insertions(+), 28 deletions(-) create mode 100644 UncomplicatedCustomRoles/Manager/InventoryLimitOverride.cs create mode 100644 UncomplicatedCustomRoles/Patches/CategoryLimitPatch.cs diff --git a/UncomplicatedCustomRoles/Events/ServerEventHandler.cs b/UncomplicatedCustomRoles/Events/ServerEventHandler.cs index 2d1b5c8..2c992a1 100644 --- a/UncomplicatedCustomRoles/Events/ServerEventHandler.cs +++ b/UncomplicatedCustomRoles/Events/ServerEventHandler.cs @@ -72,6 +72,7 @@ public void OnRoundRestarted() Spawn.SpawnQueue.Clear(); Spawn.Spawning.Clear(); API.Features.Escape.Bucket.Clear(); + InventoryLimitOverride.ClearAll(); } public void OnWaveRespawning(WaveRespawningEventArgs ev) diff --git a/UncomplicatedCustomRoles/Extensions/PlayerExtension.cs b/UncomplicatedCustomRoles/Extensions/PlayerExtension.cs index 598bb4d..8425951 100644 --- a/UncomplicatedCustomRoles/Extensions/PlayerExtension.cs +++ b/UncomplicatedCustomRoles/Extensions/PlayerExtension.cs @@ -233,41 +233,38 @@ private static string ProcessCustomInfo(string customInfo) // REF https://gitlab.com/exmod-team/EXILED/-/blob/master/EXILED/Exiled.API/Features/Player.cs?ref_type=heads#L2558 internal static void SetCategoryLimit(this Player player, ItemCategory category, sbyte limit) { - var index = InventoryLimits.StandardCategoryLimits.Where(x => x.Value >= 0).OrderBy(x => x.Key).ToList() - .FindIndex(x => x.Key == category); - - if (index is -1) - return; - - MirrorExtensions.SendFakeSyncObject(player, ServerConfigSynchronizer.Singleton.netIdentity, - typeof(ServerConfigSynchronizer), writer => - { - writer.WriteULong(1ul); - writer.WriteUInt(1); - writer.WriteByte((byte)SyncList.Operation.OP_SET); - writer.WriteInt(index); - writer.WriteSByte(limit); - }); + InventoryLimitOverride.Set(player.PlayerId, category, limit); + SendCategoryLimit(player, category, limit); } // REF https://gitlab.com/exmod-team/EXILED/-/blob/master/EXILED/Exiled.API/Features/Player.cs?ref_type=heads#L2584 internal static void ResetCategoryLimit(this Player player, ItemCategory category) { - var index = InventoryLimits.StandardCategoryLimits.Where(x => x.Value >= 0).OrderBy(x => x.Key).ToList() - .FindIndex(x => x.Key == category); + InventoryLimitOverride.Clear(player.PlayerId, category); - if (index is -1) + var config = ServerConfigSynchronizer.Singleton; + var index = (int)category; + if (config is null || index < 0 || index >= config.CategoryLimits.Count) return; - MirrorExtensions.SendFakeSyncObject(player, ServerConfigSynchronizer.Singleton.netIdentity, - typeof(ServerConfigSynchronizer), writer => - { - writer.WriteULong(1ul); - writer.WriteUInt(1); - writer.WriteByte((byte)SyncList.Operation.OP_SET); - writer.WriteInt(index); - writer.WriteSByte(ServerConfigSynchronizer.Singleton.CategoryLimits[index]); - }); + SendCategoryLimit(player, category, config.CategoryLimits[index]); + } + + private static void SendCategoryLimit(Player player, ItemCategory category, sbyte limit) + { + var config = ServerConfigSynchronizer.Singleton; + var index = (int)category; + if (config is null || index < 0 || index >= config.CategoryLimits.Count) + return; + + MirrorExtensions.SendFakeSyncObject(player, config.netIdentity, typeof(ServerConfigSynchronizer), writer => + { + writer.WriteULong(1ul); + writer.WriteUInt(1); + writer.WriteByte((byte)SyncList.Operation.OP_SET); + writer.WriteUInt((uint)index); + writer.WriteSByte(limit); + }); } internal static void ResetInventory(this Player player, IEnumerable items) diff --git a/UncomplicatedCustomRoles/Manager/InventoryLimitOverride.cs b/UncomplicatedCustomRoles/Manager/InventoryLimitOverride.cs new file mode 100644 index 0000000..c643cd8 --- /dev/null +++ b/UncomplicatedCustomRoles/Manager/InventoryLimitOverride.cs @@ -0,0 +1,45 @@ +/* + * This file is a part of the UncomplicatedCustomRoles project. + * + * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) + * + * This file is licensed under the GNU Affero General Public License v3.0. + * You should have received a copy of the AGPL license along with this file. + * If not, see . + */ + +using System.Collections.Concurrent; +using System.Collections.Generic; + +namespace UncomplicatedCustomRoles.Manager; + +internal static class InventoryLimitOverride +{ + private static readonly ConcurrentDictionary> Categories = new(); + + internal static void Set(int playerId, ItemCategory category, sbyte limit) + { + Categories.GetOrAdd(playerId, _ => new Dictionary())[category] = limit; + } + + internal static void Clear(int playerId, ItemCategory category) + { + if (!Categories.TryGetValue(playerId, out var map)) + return; + + map.Remove(category); + if (map.Count == 0) + Categories.TryRemove(playerId, out _); + } + + internal static void ClearAll() + { + Categories.Clear(); + } + + internal static bool TryGet(int playerId, ItemCategory category, out sbyte limit) + { + limit = 0; + return Categories.TryGetValue(playerId, out var map) && map.TryGetValue(category, out limit); + } +} diff --git a/UncomplicatedCustomRoles/Manager/SpawnManager.cs b/UncomplicatedCustomRoles/Manager/SpawnManager.cs index f6c1f0a..fbbd651 100644 --- a/UncomplicatedCustomRoles/Manager/SpawnManager.cs +++ b/UncomplicatedCustomRoles/Manager/SpawnManager.cs @@ -47,7 +47,6 @@ internal class SpawnManager { public static readonly IReadOnlyDictionary ColorMap = new Dictionary { - { "white", "#FFFFFF" }, { "pink", "#FF96DE" }, { "red", "#C50000" }, { "brown", "#944710" }, diff --git a/UncomplicatedCustomRoles/Patches/CategoryLimitPatch.cs b/UncomplicatedCustomRoles/Patches/CategoryLimitPatch.cs new file mode 100644 index 0000000..1fce972 --- /dev/null +++ b/UncomplicatedCustomRoles/Patches/CategoryLimitPatch.cs @@ -0,0 +1,53 @@ +/* + * This file is a part of the UncomplicatedCustomRoles project. + * + * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) + * + * This file is licensed under the GNU Affero General Public License v3.0. + * You should have received a copy of the AGPL license along with this file. + * If not, see . + */ + +using HarmonyLib; +using InventorySystem.Configs; +using InventorySystem.Items.Armor; +using UncomplicatedCustomRoles.Extensions; +using UncomplicatedCustomRoles.Manager; + +namespace UncomplicatedCustomRoles.Patches; + +[HarmonyPatch(typeof(InventoryLimits), nameof(InventoryLimits.GetCategoryLimit), typeof(ItemCategory), + typeof(ReferenceHub))] +internal static class CategoryLimitByHubPatch +{ + private static void Postfix(ItemCategory category, ReferenceHub player, ref sbyte __result) + { + if (TryGetCustomLimit(player, category, out var limit)) + __result = limit; + } + + internal static bool TryGetCustomLimit(ReferenceHub player, ItemCategory category, out sbyte limit) + { + limit = 0; + + if (player is null) + return false; + + if (player.TryGetSummonedInstance(out var role) && + role.Role.CustomInventoryLimits is { Count: > 0 } limits && limits.TryGetValue(category, out limit)) + return true; + + return InventoryLimitOverride.TryGet(player.PlayerId, category, out limit); + } +} + +[HarmonyPatch(typeof(InventoryLimits), nameof(InventoryLimits.GetCategoryLimit), typeof(BodyArmor), + typeof(ItemCategory))] +internal static class CategoryLimitByArmorPatch +{ + private static void Postfix(BodyArmor armor, ItemCategory category, ref sbyte __result) + { + if (armor is not null && CategoryLimitByHubPatch.TryGetCustomLimit(armor.Owner, category, out var limit)) + __result = limit; + } +} \ No newline at end of file From 563afaed04843c97e87c0ac4fb191125bac58216 Mon Sep 17 00:00:00 2001 From: MedveMarci Date: Mon, 20 Jul 2026 19:14:59 +0200 Subject: [PATCH 23/47] Fixed Fake SCP's door permission --- UncomplicatedCustomRoles/Patches/TeamPatch.cs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/UncomplicatedCustomRoles/Patches/TeamPatch.cs b/UncomplicatedCustomRoles/Patches/TeamPatch.cs index 72af53b..157dffb 100644 --- a/UncomplicatedCustomRoles/Patches/TeamPatch.cs +++ b/UncomplicatedCustomRoles/Patches/TeamPatch.cs @@ -282,8 +282,15 @@ private static bool Prefix(DoorPermissionsPolicy __instance, ReferenceHub hub, I return false; } - if (hub.roleManager.CurrentRole is IDoorPermissionProvider currentRole && - (!DisguiseTeam.List.TryGetValue(hub.PlayerId, out var team) || team != Team.SCPs)) + var isFakedScp = DisguiseTeam.List.TryGetValue(hub.PlayerId, out var team) && team == Team.SCPs; + + if (isFakedScp && __instance.CheckPermissions(DoorPermissionFlags.ScpOverride)) + { + __result = true; + return false; + } + + if (hub.roleManager.CurrentRole is IDoorPermissionProvider currentRole && !isFakedScp) { __result = __instance.CheckPermissions(currentRole, requester, out callback); return false; @@ -314,16 +321,19 @@ private static bool Prefix(ReferenceHub hub, IDoorPermissionRequester requester, return false; } + var isFakedScp = DisguiseTeam.List.TryGetValue(hub.PlayerId, out var team) && team == Team.SCPs; var combinedPermissions = DoorPermissionFlags.None; - if (hub.roleManager.CurrentRole is IDoorPermissionProvider currentRole && - (!DisguiseTeam.List.TryGetValue(hub.PlayerId, out var team) || team != Team.SCPs)) + if (hub.roleManager.CurrentRole is IDoorPermissionProvider currentRole && !isFakedScp) combinedPermissions |= currentRole.GetPermissions(requester); var curInstance = hub.inventory.CurInstance; if (curInstance != null && curInstance is IDoorPermissionProvider permissionProvider) combinedPermissions |= permissionProvider.GetPermissions(requester); + if (isFakedScp) + combinedPermissions |= DoorPermissionFlags.ScpOverride; + __result = combinedPermissions; return false; } From 5ac5c76cbedd4a6369acb3e1c6d3b557b040daf6 Mon Sep 17 00:00:00 2001 From: MedveMarci Date: Mon, 20 Jul 2026 19:39:23 +0200 Subject: [PATCH 24/47] Mark ColorfulNickname, CustomInfoOrder, and NoUnitName modules as deprecated --- .../API/Features/CustomModules/ColorfulNickname.cs | 1 + .../API/Features/CustomModules/CustomInfoOrder.cs | 1 + .../API/Features/CustomModules/NoUnitName.cs | 3 +++ 3 files changed, 5 insertions(+) diff --git a/UncomplicatedCustomRoles/API/Features/CustomModules/ColorfulNickname.cs b/UncomplicatedCustomRoles/API/Features/CustomModules/ColorfulNickname.cs index 1adb469..2308af0 100644 --- a/UncomplicatedCustomRoles/API/Features/CustomModules/ColorfulNickname.cs +++ b/UncomplicatedCustomRoles/API/Features/CustomModules/ColorfulNickname.cs @@ -15,6 +15,7 @@ namespace UncomplicatedCustomRoles.API.Features.CustomModules; +[Obsolete("This module is deprecated and will be removed in a future version. Use InfoTag instead.")] public class ColorfulNickname : CustomModule { public override List RequiredArgs => ["color"]; diff --git a/UncomplicatedCustomRoles/API/Features/CustomModules/CustomInfoOrder.cs b/UncomplicatedCustomRoles/API/Features/CustomModules/CustomInfoOrder.cs index ce591ac..9791b14 100644 --- a/UncomplicatedCustomRoles/API/Features/CustomModules/CustomInfoOrder.cs +++ b/UncomplicatedCustomRoles/API/Features/CustomModules/CustomInfoOrder.cs @@ -16,6 +16,7 @@ namespace UncomplicatedCustomRoles.API.Features.CustomModules; +[Obsolete("This module is deprecated and will be removed in a future version. Use InfoTag instead.")] public class CustomInfoOrder : CustomModule { private static readonly string[] KnownTokens = ["custominfo", "nickname", "rolename"]; diff --git a/UncomplicatedCustomRoles/API/Features/CustomModules/NoUnitName.cs b/UncomplicatedCustomRoles/API/Features/CustomModules/NoUnitName.cs index cd833f0..af0dd04 100644 --- a/UncomplicatedCustomRoles/API/Features/CustomModules/NoUnitName.cs +++ b/UncomplicatedCustomRoles/API/Features/CustomModules/NoUnitName.cs @@ -8,8 +8,11 @@ * If not, see . */ +using System; + namespace UncomplicatedCustomRoles.API.Features.CustomModules; +[Obsolete("This module is deprecated and will be removed in a future version. Use InfoTag instead.")] public class NoUnitName : CustomModule { } \ No newline at end of file From f6aae82b975e6690e95595dfd77c66a21190b882 Mon Sep 17 00:00:00 2001 From: MedveMarci Date: Thu, 23 Jul 2026 10:55:18 +0200 Subject: [PATCH 25/47] Added CustomTeam flag --- .../API/Features/CustomModules/CustomTeam.cs | 47 +++++++++++++++++++ .../Events/PlayerEventHandler.cs | 6 +++ UncomplicatedCustomRoles/Patches/TeamPatch.cs | 10 ++++ 3 files changed, 63 insertions(+) create mode 100644 UncomplicatedCustomRoles/API/Features/CustomModules/CustomTeam.cs diff --git a/UncomplicatedCustomRoles/API/Features/CustomModules/CustomTeam.cs b/UncomplicatedCustomRoles/API/Features/CustomModules/CustomTeam.cs new file mode 100644 index 0000000..22d670d --- /dev/null +++ b/UncomplicatedCustomRoles/API/Features/CustomModules/CustomTeam.cs @@ -0,0 +1,47 @@ +/* + * This file is a part of the UncomplicatedCustomRoles project. + * + * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) + * + * This file is licensed under the GNU Affero General Public License v3.0. + * You should have received a copy of the AGPL license along with this file. + * If not, see . + */ + +using System; +using System.Collections.Generic; + +namespace UncomplicatedCustomRoles.API.Features.CustomModules; + +public class CustomTeam : CustomModule +{ + public override List RequiredArgs => ["team"]; + internal string Team => TryGetStringValue("team", string.Empty); + + internal bool IsSameTeam(CustomTeam other) + { + return other is not null && !string.IsNullOrWhiteSpace(Team) && + string.Equals(Team, other.Team, StringComparison.OrdinalIgnoreCase); + } + + internal static bool SameTeam(ReferenceHub first, ReferenceHub second) + { + return first is not null && second is not null && first != second && + SummonedCustomRole.TryGet(first, out var firstRole) && firstRole.TryGetModule(out CustomTeam firstTeam) && + SummonedCustomRole.TryGet(second, out var secondRole) && + secondRole.TryGetModule(out CustomTeam secondTeam) && + firstTeam.IsSameTeam(secondTeam); + } + + public override bool Validate(out string error) + { + if (string.IsNullOrWhiteSpace(Team)) + { + error = "'team' must be a non-empty team name (e.g. 'SerpentsHand')."; + return false; + } + + error = null; + return true; + } +} diff --git a/UncomplicatedCustomRoles/Events/PlayerEventHandler.cs b/UncomplicatedCustomRoles/Events/PlayerEventHandler.cs index b1a173e..24cf93a 100644 --- a/UncomplicatedCustomRoles/Events/PlayerEventHandler.cs +++ b/UncomplicatedCustomRoles/Events/PlayerEventHandler.cs @@ -279,6 +279,12 @@ public void OnHurting(PlayerHurtingEventArgs Hurting) if (Hurting.Player is not null && Hurting.Attacker is not null && Hurting.Player.IsAlive && Hurting.Attacker.IsAlive) { + if (CustomTeam.SameTeam(Hurting.Attacker.ReferenceHub, Hurting.Player.ReferenceHub)) + { + Hurting.IsAllowed = false; + return; + } + if (Hurting.Attacker.TryGetSummonedInstance(out var attackerCustomRole)) { if (attackerCustomRole.Role.IsFriendOf is not null && diff --git a/UncomplicatedCustomRoles/Patches/TeamPatch.cs b/UncomplicatedCustomRoles/Patches/TeamPatch.cs index 157dffb..45e77aa 100644 --- a/UncomplicatedCustomRoles/Patches/TeamPatch.cs +++ b/UncomplicatedCustomRoles/Patches/TeamPatch.cs @@ -29,6 +29,7 @@ using PlayerRoles.PlayableScps.Scp939.Mimicry; using PlayerStatsSystem; using UncomplicatedCustomRoles.API.Features; +using UncomplicatedCustomRoles.API.Features.CustomModules; using UncomplicatedCustomRoles.Manager; using static HarmonyLib.AccessTools; @@ -402,4 +403,13 @@ private static bool IsScpButNot079(RoleTypeId roleTypeId, Team team) { return team == Team.SCPs && roleTypeId != RoleTypeId.Scp079; } +} + +[HarmonyPatch(typeof(FlashbangGrenade), nameof(FlashbangGrenade.ProcessPlayer))] +internal static class FlashbangCustomTeamPatch +{ + private static bool Prefix(FlashbangGrenade __instance, ReferenceHub hub) + { + return !CustomTeam.SameTeam(__instance.PreviousOwner.Hub, hub); + } } \ No newline at end of file From e8f1a48218ab1418cca2750b6ed71409547a185d Mon Sep 17 00:00:00 2001 From: MedveMarci Date: Wed, 5 Aug 2026 17:02:19 +0200 Subject: [PATCH 26/47] Added Timed spawns; More detailed Percentages command; Fixed handling left player; Fixed CustomModule decoder --- .../API/Features/Behaviour/SpawnBehaviour.cs | 5 + .../Features/CustomModules/CustomKeycard.cs | 12 +- .../Features/CustomModules/CustomModule.cs | 2 +- .../API/Features/SummonedCustomRole.cs | 127 ++++++++++++------ UncomplicatedCustomRoles/Commands/Info.cs | 8 +- .../Commands/Percentages.cs | 62 ++++++--- .../Events/PlayerEventHandler.cs | 27 ++++ .../Events/ServerEventHandler.cs | 6 +- .../Manager/DelayedSpawnManager.cs | 123 +++++++++++++++++ .../Manager/InventoryLimitOverride.cs | 5 + .../Manager/RoleValidator.cs | 32 +++-- .../Manager/SpawnManager.cs | 97 ++++++------- .../Manager/YamlFlagsHandler.cs | 14 +- UncomplicatedCustomRoles/Plugin.cs | 2 +- .../Properties/AssemblyInfo.cs | 4 +- 15 files changed, 401 insertions(+), 125 deletions(-) create mode 100644 UncomplicatedCustomRoles/Manager/DelayedSpawnManager.cs diff --git a/UncomplicatedCustomRoles/API/Features/Behaviour/SpawnBehaviour.cs b/UncomplicatedCustomRoles/API/Features/Behaviour/SpawnBehaviour.cs index bb2eb18..37f8476 100644 --- a/UncomplicatedCustomRoles/API/Features/Behaviour/SpawnBehaviour.cs +++ b/UncomplicatedCustomRoles/API/Features/Behaviour/SpawnBehaviour.cs @@ -39,6 +39,11 @@ public class SpawnBehaviour /// public float SpawnChance { get; set; } = 60; + /// + /// Gets or sets how many seconds after the round starts the role is spawned. + /// + public float SpawnDelay { get; set; } = 0; + /// /// Gets or sets the of the role /// diff --git a/UncomplicatedCustomRoles/API/Features/CustomModules/CustomKeycard.cs b/UncomplicatedCustomRoles/API/Features/CustomModules/CustomKeycard.cs index 328037b..677725a 100644 --- a/UncomplicatedCustomRoles/API/Features/CustomModules/CustomKeycard.cs +++ b/UncomplicatedCustomRoles/API/Features/CustomModules/CustomKeycard.cs @@ -155,6 +155,16 @@ public override void OnAdded() { Timing.CallDelayed(Timing.WaitForOneFrame, () => { + if (Player is null || !Player.IsAlive) + return; + + if (Player.IsInventoryFull) + { + LogManager.Warn( + $"[CustomKeycard] Can't give the '{KeycardType}' keycard to {Player.Nickname}: their inventory is already full. Free a slot in 'inventory' or remove one of the role's CustomKeycard flags."); + return; + } + _keycardItem = KeycardType switch { ItemType.KeycardCustomManagement => KeycardItem.CreateCustomKeycardManagement( @@ -176,7 +186,7 @@ public override void OnAdded() if (_keycardItem is null) LogManager.Error( - $"[CustomKeycard] Failed to create keycard of type '{KeycardType}' for player {Player?.Nickname}. This is likely a bug, please report it."); + $"[CustomKeycard] Failed to create keycard of type '{KeycardType}' for player {Player?.Nickname}. If the type is a valid customizable keycard this is a bug, please report it."); }); base.OnAdded(); } diff --git a/UncomplicatedCustomRoles/API/Features/CustomModules/CustomModule.cs b/UncomplicatedCustomRoles/API/Features/CustomModules/CustomModule.cs index ff484ea..5519624 100644 --- a/UncomplicatedCustomRoles/API/Features/CustomModules/CustomModule.cs +++ b/UncomplicatedCustomRoles/API/Features/CustomModules/CustomModule.cs @@ -263,7 +263,7 @@ internal static List Load(List modules, SummonedCustomRole LogManager.Silent( $"[CM Loader] Initialize loading for {summonedCustomRole}\nPreloaded {YamlFlagsHandler.Modules.Length} modules..."); - var data = YamlFlagsHandler.Decode(modules) ?? new Dictionary?>(); + var data = YamlFlagsHandler.Decode(modules) ?? []; List mods = []; diff --git a/UncomplicatedCustomRoles/API/Features/SummonedCustomRole.cs b/UncomplicatedCustomRoles/API/Features/SummonedCustomRole.cs index 24881e3..7fd1bdf 100644 --- a/UncomplicatedCustomRoles/API/Features/SummonedCustomRole.cs +++ b/UncomplicatedCustomRoles/API/Features/SummonedCustomRole.cs @@ -46,6 +46,8 @@ public class SummonedCustomRole private static readonly ConcurrentDictionary _cachedCountByRoleId = new(); internal static int EventTriggeredModuleTotal; + + private readonly int _playerId; private int _eventModuleCount; @@ -54,6 +56,7 @@ internal SummonedCustomRole(Player player, ICustomRole role, Triplet 0) { count--; @@ -336,6 +355,11 @@ public void Destroy() /// Remove the current CustomRole from the player without destroying the instance /// public void Remove() + { + RemoveInternal(false); + } + + private void RemoveInternal(bool detached) { try { @@ -345,47 +369,55 @@ public void Remove() _customModules.Remove(module); } - if (Badge is { } badge) + if (!detached) { - Player.ReferenceHub.serverRoles.SetText(badge.First); - Player.ReferenceHub.serverRoles.SetColor(badge.Second); - Player.ReferenceHub.serverRoles.RefreshLocalTag(); - - LogManager.Debug("Badge detected, fixed"); - } - - CustomInfo?.Detach(); + if (Badge is { } badge) + { + Player.ReferenceHub.serverRoles.SetText(badge.First); + Player.ReferenceHub.serverRoles.SetColor(badge.Second); + Player.ReferenceHub.serverRoles.RefreshLocalTag(); - if (IsCustomNickname) - Player.DisplayName = null!; + LogManager.Debug("Badge detected, fixed"); + } - LogManager.Debug("Scale reset to 1, 1, 1"); - Player.Scale = new Vector3(1, 1, 1); + CustomInfo?.Detach(); - Player.IsDisarmed = false; + if (IsCustomNickname) + Player.DisplayName = null!; - DisguiseTeam.Remove(Player.PlayerId); + LogManager.Debug("Scale reset to 1, 1, 1"); + Player.Scale = new Vector3(1, 1, 1); - // Reset ammo limit - if (Role.Ammo is { Count: > 0 }) - foreach (var ammo in Role.Ammo.Keys) - Player.ResetAmmoLimit(ammo); + Player.IsDisarmed = false; + } - // Reset category limit - if (Role.CustomInventoryLimits is { Count: > 0 }) - foreach (var category in Role.CustomInventoryLimits.Keys) - Player.ResetCategoryLimit(category); + DisguiseTeam.Remove(_playerId); - // Clear the custom info last so nothing re-applies it afterwards - CustomInfo.SuppressExternalSync = true; - try - { - Player.ReferenceHub.nicknameSync.Network_playerInfoToShow = PlayerInfoArea; - Player.ReferenceHub.nicknameSync.Network_customPlayerInfoString = string.Empty; - } - finally + if (detached) + InventoryLimitOverride.ClearAll(_playerId); + else { - CustomInfo.SuppressExternalSync = false; + // Reset ammo limit + if (Role.Ammo is { Count: > 0 }) + foreach (var ammo in Role.Ammo.Keys) + Player.ResetAmmoLimit(ammo); + + // Reset category limit + if (Role.CustomInventoryLimits is { Count: > 0 }) + foreach (var category in Role.CustomInventoryLimits.Keys) + Player.ResetCategoryLimit(category); + + // Clear the custom info last so nothing re-applies it afterwards + CustomInfo.SuppressExternalSync = true; + try + { + Player.ReferenceHub.nicknameSync.Network_playerInfoToShow = PlayerInfoArea; + Player.ReferenceHub.nicknameSync.Network_customPlayerInfoString = string.Empty; + } + finally + { + CustomInfo.SuppressExternalSync = false; + } } if (IsDefaultCoroutineRole && GenericCoroutine.IsRunning) @@ -394,12 +426,16 @@ public void Remove() if (NicknameReapplyCoroutine.IsRunning) Timing.KillCoroutines(NicknameReapplyCoroutine); - // Remove effects - Player.DisableAllEffects(); - InfiniteEffects.Clear(); + if (!detached) + { + // Remove effects + Player.DisableAllEffects(); - if (Appearance != RoleTypeId.None && LabApiExtensions.IsAvailable) - LabApiExtensions.RemoveFakeRole(Player); + if (Appearance != RoleTypeId.None && LabApiExtensions.IsAvailable) + LabApiExtensions.RemoveFakeRole(Player); + } + + InfiniteEffects.Clear(); if (Role is CustomRole customRole) customRole.OnRemoved(this); @@ -407,7 +443,7 @@ public void Remove() catch (Exception e) { LogManager.Error( - $"Failed to act SummonedCustomRole::Remove() - {e.GetType().FullName}: {e.Message}\n{e.StackTrace}"); + $"Failed to act SummonedCustomRole::Remove(detached: {detached}) - {e.GetType().FullName}: {e.Message}\n{e.StackTrace}"); } EventHandler?.Unload(); @@ -779,6 +815,17 @@ public static void TryParseRemoteAdmin(ReferenceHub player, StringBuilder builde builder.AppendLine(Info.BuildInfo(role.Role)); } } + + internal static void ClearAll() + { + foreach (var role in List.Values.ToArray()) + role.DestroyDetached(); + + List.Clear(); + _cachedListByPlayerId.Clear(); + _cachedCountByRoleId.Clear(); + EventTriggeredModuleTotal = 0; + } public static void RemoveSpecificRole(int id) { diff --git a/UncomplicatedCustomRoles/Commands/Info.cs b/UncomplicatedCustomRoles/Commands/Info.cs index 4e95452..3a7a373 100644 --- a/UncomplicatedCustomRoles/Commands/Info.cs +++ b/UncomplicatedCustomRoles/Commands/Info.cs @@ -9,6 +9,7 @@ */ using System.Collections.Generic; +using System.Linq; using CommandSystem; using UncomplicatedCustomRoles.API.Enums; using UncomplicatedCustomRoles.API.Features; @@ -73,6 +74,10 @@ public static string BuildInfo(ICustomRole role) if (role.SpawnSettings != null) { + if (role.SpawnSettings.SpawnDelay > 0) + data.Add("⏱️ Spawn delay:", + $"{role.SpawnSettings.SpawnDelay}s after the round starts"); + if (role.SpawnSettings.Spawn is SpawnType.RoomsSpawn) data.Add("🚪 Spawn rooms:", string.Join(", ", role.SpawnSettings?.SpawnRooms ?? [])); @@ -88,7 +93,8 @@ public static string BuildInfo(ICustomRole role) { var decodedFlags = YamlFlagsHandler.Decode(role.CustomFlags); if (decodedFlags != null) - data.Add("🧩 Custom flags:", string.Join(", ", decodedFlags.Keys)); + data.Add("🧩 Custom flags:", + string.Join(", ", decodedFlags.Select(f => f.Key))); } foreach (var kvp in data) diff --git a/UncomplicatedCustomRoles/Commands/Percentages.cs b/UncomplicatedCustomRoles/Commands/Percentages.cs index 87b10da..c053fc4 100644 --- a/UncomplicatedCustomRoles/Commands/Percentages.cs +++ b/UncomplicatedCustomRoles/Commands/Percentages.cs @@ -29,38 +29,64 @@ public class Percentages : IUCRCommand public bool Executor(List args, ICommandSender sender, out string response) { - var detailed = args.Any() && args[0] is "details"; response = "Spawn percentages for each base Role:"; foreach (RoleTypeId role in Enum.GetValues(typeof(RoleTypeId))) { - var roles = CustomRole.List.Where(r => - r.SpawnSettings?.CanReplaceRoles != null && r.SpawnSettings.CanReplaceRoles.Contains(role)); - var customRoles = roles.ToList(); - if (customRoles.Any()) - { - var total = customRoles.Sum(r => r.SpawnSettings.SpawnChance); - response += - $"\n\n{(total >= 100 ? "❗" : "✔️")} {role.GetFullName()} ({customRoles.Count()})"; + var customRoles = CustomRole.List.Where(r => + r.SpawnSettings?.CanReplaceRoles != null && r.SpawnSettings.CanReplaceRoles.Contains(role) && + !r.IgnoreSpawnSystem && r.SpawnSettings.SpawnDelay <= 0).ToList(); + + if (!customRoles.Any()) + continue; + + var total = customRoles.Sum(r => r.SpawnSettings.SpawnChance); + + var effective = Math.Min(total, 100); + response += + $"\n\n{(total >= 100 ? "❗" : "✔️")} {role.GetFullName()} ({customRoles.Count})"; + response += + $"\nChance of spawning as a CustomRole: {effective}%\nChance of spawning as a regular role: {100 - effective}%"; + + if (total > 100) response += - $"\nChance of spawning as a CustomRole: {total}%\nChance of spawning as a regular role: {100 - total}%"; + $"\nThe configured chances add up to {total}%, so this role is always replaced and the chances below only weight which CustomRole wins."; - if (detailed) - foreach (var customRole in customRoles.Where(r => r.SpawnSettings.SpawnChance > 0)) - response += $"\n ∟ {customRole} - {customRole.SpawnSettings.SpawnChance}%"; + foreach (var customRole in customRoles.OrderByDescending(r => r.SpawnSettings.SpawnChance)) + { + var chance = customRole.SpawnSettings.SpawnChance; + + var actual = total <= 0 ? 0 : chance / Math.Max(total, 100) * 100; + + response += chance <= 0 + ? $"\n ∟ {customRole} - never spawns (spawn_chance is {chance})" + : $"\n ∟ {customRole} - {actual:0.##}%{(Math.Abs(actual - chance) > 0.01f ? $" (configured: {chance}%)" : string.Empty)}"; } } + var delayedRoles = CustomRole.List.Where(r => + !r.IgnoreSpawnSystem && r.SpawnSettings is { SpawnDelay: > 0 } && + r.SpawnSettings.CanReplaceRoles is { Count: > 0 }).ToList(); + if (delayedRoles.Any()) + { + response += + $"\n\n⏱️ Roles spawned on a timer ({delayedRoles.Count}) - handed out after the round started, not at spawn:"; + foreach (var customRole in delayedRoles) + response += + $"\n ∟ {customRole} - after {customRole.SpawnSettings.SpawnDelay}s, {customRole.SpawnSettings.SpawnChance}% for each {string.Join("/", customRole.SpawnSettings.CanReplaceRoles)}, up to {customRole.SpawnSettings.MaxPlayers} player(s)"; + } + var manualRoles = CustomRole.List.Where(r => - r.SpawnSettings?.CanReplaceRoles == null || !r.SpawnSettings.CanReplaceRoles.Any()); + r.IgnoreSpawnSystem || r.SpawnSettings?.CanReplaceRoles == null || + !r.SpawnSettings.CanReplaceRoles.Any()).ToList(); if (manualRoles.Any()) { response += - $"\n\nℹ️ Roles without a linked vanilla role ({manualRoles.Count()}) - spawned manually or by another plugin:"; + $"\n\nℹ️ Roles that never spawn on their own ({manualRoles.Count}) - spawned manually or by another plugin:"; foreach (var customRole in manualRoles) - response += customRole.SpawnSettings is not null && customRole.SpawnSettings.SpawnChance > 0 - ? $"\n ∟ {customRole} - {customRole.SpawnSettings.SpawnChance}%" - : $"\n ∟ {customRole}"; + response += customRole.IgnoreSpawnSystem + ? $"\n ∟ {customRole} - ignore_spawn_system is enabled" + : $"\n ∟ {customRole} - no can_replace_roles"; } response += "\nOwO"; // We want to render everything diff --git a/UncomplicatedCustomRoles/Events/PlayerEventHandler.cs b/UncomplicatedCustomRoles/Events/PlayerEventHandler.cs index 24cf93a..0f1c503 100644 --- a/UncomplicatedCustomRoles/Events/PlayerEventHandler.cs +++ b/UncomplicatedCustomRoles/Events/PlayerEventHandler.cs @@ -48,6 +48,7 @@ internal override void OnRegistered() PlayerEvents.Hurt += OnHurt; PlayerEvents.PickingUpItem += OnPickingUpItem; PlayerEvents.Joined += OnJoined; + PlayerEvents.Left += OnLeft; PlayerEvents.DamagingWindow += OnDamagingWindow; PlayerEvents.UnlockingWarheadButton += OnUnlockingWarheadButton; PlayerEvents.RequestedRaPlayerInfo += OnPlayerRequestedRaPlayerInfo; @@ -76,6 +77,7 @@ internal override void OnUnregistered() PlayerEvents.Hurt -= OnHurt; PlayerEvents.PickingUpItem -= OnPickingUpItem; PlayerEvents.Joined -= OnJoined; + PlayerEvents.Left -= OnLeft; PlayerEvents.DamagingWindow -= OnDamagingWindow; PlayerEvents.UnlockingWarheadButton -= OnUnlockingWarheadButton; PlayerEvents.RequestedRaPlayerInfo -= OnPlayerRequestedRaPlayerInfo; @@ -99,6 +101,31 @@ public void OnJoined(PlayerJoinedEventArgs ev) role.Player.Scale = role.Scale; } + public void OnLeft(PlayerLeftEventArgs ev) + { + if (ev.Player is null) + return; + + var playerId = ev.Player.PlayerId; + + if (SummonedCustomRole.TryGet(ev.Player, out var customRole)) + { + LogManager.Debug( + $"Player {ev.Player.Nickname} ({playerId}) left as CustomRole {customRole.Role.Name} ({customRole.Role.Id}), releasing the instance"); + customRole.DestroyDetached(); + } + + FirstRoundPlayers.Remove(playerId); + RagdollAppearanceQueue.Remove(playerId); + TerminationQueue.TryRemove(playerId, out _); + RespawnInventoryQueue.TryRemove(playerId, out _); + Spawn.SpawnQueue.Remove(playerId); + Spawn.Spawning.Remove(playerId); + API.Features.Escape.Bucket.Remove(playerId); + InventoryLimitOverride.ClearAll(playerId); + DisguiseTeam.Remove(playerId); + } + public void OnUpdatingEffect(PlayerEffectUpdatingEventArgs ev) { if (ev.Player is null) diff --git a/UncomplicatedCustomRoles/Events/ServerEventHandler.cs b/UncomplicatedCustomRoles/Events/ServerEventHandler.cs index 2c992a1..89ac242 100644 --- a/UncomplicatedCustomRoles/Events/ServerEventHandler.cs +++ b/UncomplicatedCustomRoles/Events/ServerEventHandler.cs @@ -48,7 +48,7 @@ public void OnPlayersSpawned() Started = true; FirstRoundPlayers.Clear(); - // Starts the infinite effect thing + DelayedSpawnManager.ScheduleAll(); InfiniteEffect.Stop(); InfiniteEffect.EffectAssociationAllowed = true; InfiniteEffect.Start(); @@ -57,6 +57,7 @@ public void OnPlayersSpawned() public void OnRoundEnded(RoundEndedEventArgs _) { Started = false; + DelayedSpawnManager.Cancel(); InfiniteEffect.Terminate(); } @@ -65,6 +66,9 @@ public void OnRoundRestarted() Announcer.SavedCustomAnnouncements.Clear(); // Round-scoped state must not leak into the next round + DelayedSpawnManager.Cancel(); + SummonedCustomRole.ClearAll(); + DisguiseTeam.Clear(); RespawnInventoryQueue.Clear(); RagdollAppearanceQueue.Clear(); TerminationQueue.Clear(); diff --git a/UncomplicatedCustomRoles/Manager/DelayedSpawnManager.cs b/UncomplicatedCustomRoles/Manager/DelayedSpawnManager.cs new file mode 100644 index 0000000..4b0c89e --- /dev/null +++ b/UncomplicatedCustomRoles/Manager/DelayedSpawnManager.cs @@ -0,0 +1,123 @@ +/* + * This file is a part of the UncomplicatedCustomRoles project. + * + * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) + * + * This file is licensed under the GNU Affero General Public License v3.0. + * You should have received a copy of the AGPL license along with this file. + * If not, see . + */ + +using System.Collections.Generic; +using System.Linq; +using LabApi.Features.Wrappers; +using MEC; +using UncomplicatedCustomRoles.API.Features; +using UncomplicatedCustomRoles.API.Interfaces; +using UncomplicatedCustomRoles.Extensions; +using Random = UnityEngine.Random; + +namespace UncomplicatedCustomRoles.Manager; + +internal static class DelayedSpawnManager +{ + private static readonly List Scheduled = []; + + internal static void ScheduleAll() + { + Cancel(); + + foreach (var role in CustomRole.CustomRoles.Values) + { + if (role?.SpawnSettings is null || role.IgnoreSpawnSystem || role.SpawnSettings.SpawnDelay <= 0) + continue; + + var id = role.Id; + var delay = role.SpawnSettings.SpawnDelay; + + LogManager.Debug($"Scheduling the delayed spawn of {role.Name} ({id}) in {delay} second(s)"); + Scheduled.Add(Timing.CallDelayed(delay, () => Execute(id))); + } + } + + internal static void Cancel() + { + foreach (var handle in Scheduled.Where(handle => handle.IsRunning)) + Timing.KillCoroutines(handle); + + Scheduled.Clear(); + } + + private static void Execute(int id) + { + if (!CustomRole.CustomRoles.TryGetValue(id, out var role) || role?.SpawnSettings is null) + { + LogManager.Debug($"The delayed spawn of the role {id} fired but the role is no longer registered"); + return; + } + + if (!Round.IsRoundStarted || Round.IsRoundEnded) + { + LogManager.Debug($"Skipping the delayed spawn of {role.Name} ({id}): the round is not running anymore"); + return; + } + + var settings = role.SpawnSettings; + + var readyPlayers = Player.ReadyList.Count(); + if (readyPlayers < settings.MinPlayers) + { + LogManager.Debug( + $"Skipping the delayed spawn of {role.Name} ({id}): min_players is {settings.MinPlayers} but only {readyPlayers} player(s) are on the server"); + return; + } + + var slots = settings.MaxPlayers - SummonedCustomRole.Count(role); + if (slots < 1) + { + LogManager.Debug( + $"Skipping the delayed spawn of {role.Name} ({id}): max_players ({settings.MaxPlayers}) is already reached"); + return; + } + + var candidates = Player.ReadyList.Where(player => IsEligible(player, role)).ToList(); + if (candidates.Count == 0) + { + LogManager.Debug( + $"Skipping the delayed spawn of {role.Name} ({id}): nobody currently holds one of its can_replace_roles ({string.Join(", ", settings.CanReplaceRoles ?? [])})"); + return; + } + + candidates.ShuffleList(); + + var spawned = 0; + foreach (var player in candidates) + { + if (spawned >= slots) + break; + + if (settings.SpawnChance < 100 && Random.Range(0f, 100f) >= settings.SpawnChance) + continue; + + SpawnManager.SummonCustomSubclass(player, id); + spawned++; + } + + LogManager.Debug( + $"The delayed spawn of {role.Name} ({id}) spawned {spawned} player(s) out of {candidates.Count} candidate(s)"); + } + + private static bool IsEligible(Player player, ICustomRole role) + { + if (player is null || player.HasCustomRole()) + return false; + + if (Plugin.Instance.Config.IgnoreNpcs && player.IsNpc) + return false; + + if (role.SpawnSettings.CanReplaceRoles is not { } canReplaceRoles || !canReplaceRoles.Contains(player.Role)) + return false; + + return SpawnManager.HasRequiredPermission(player, role); + } +} diff --git a/UncomplicatedCustomRoles/Manager/InventoryLimitOverride.cs b/UncomplicatedCustomRoles/Manager/InventoryLimitOverride.cs index c643cd8..7e7a2eb 100644 --- a/UncomplicatedCustomRoles/Manager/InventoryLimitOverride.cs +++ b/UncomplicatedCustomRoles/Manager/InventoryLimitOverride.cs @@ -37,6 +37,11 @@ internal static void ClearAll() Categories.Clear(); } + internal static void ClearAll(int playerId) + { + Categories.TryRemove(playerId, out _); + } + internal static bool TryGet(int playerId, ItemCategory category, out sbyte limit) { limit = 0; diff --git a/UncomplicatedCustomRoles/Manager/RoleValidator.cs b/UncomplicatedCustomRoles/Manager/RoleValidator.cs index 6c1eb87..1e6c063 100644 --- a/UncomplicatedCustomRoles/Manager/RoleValidator.cs +++ b/UncomplicatedCustomRoles/Manager/RoleValidator.cs @@ -331,9 +331,9 @@ private static void ValidateSpawnSettings(ICustomRole role, List errors, break; } - if (role.SpawnSettings.SpawnChance is < 0 or > 100) + if (role.SpawnSettings.SpawnChance < 0) warnings.Add( - $"'spawn_settings.spawn_chance' should be between 0 and 100, got {role.SpawnSettings.SpawnChance}."); + $"'spawn_settings.spawn_chance' should be more than 0, got {role.SpawnSettings.SpawnChance}."); if (role.SpawnSettings.MinPlayers < 1) warnings.Add($"'spawn_settings.min_players' should be at least 1, got {role.SpawnSettings.MinPlayers}."); @@ -345,16 +345,28 @@ private static void ValidateSpawnSettings(ICustomRole role, List errors, warnings.Add( $"'spawn_settings.max_players' ({role.SpawnSettings.MaxPlayers}) is below 'min_players' ({role.SpawnSettings.MinPlayers}); the role will never spawn."); + var delayed = role.SpawnSettings.SpawnDelay > 0; + + if (role.SpawnSettings.SpawnDelay < 0) + warnings.Add( + $"'spawn_settings.spawn_delay' is negative ({role.SpawnSettings.SpawnDelay}); use 0 to spawn the role together with the vanilla role it replaces."); + + if (delayed && (role.SpawnSettings.CanReplaceRoles is null || !role.SpawnSettings.CanReplaceRoles.Any())) + warnings.Add( + "'spawn_settings.spawn_delay' is set but 'can_replace_roles' is empty; the delayed spawn has nobody to convert. List the roles the players should be taken from, e.g. 'Spectator'."); + if (role.SpawnSettings.CanReplaceRoles is not null) { - foreach (var replace in role.SpawnSettings.CanReplaceRoles.Where(r => - !SpawnManager.SpawnEvaluatedRoles.Contains(r))) - warnings.Add( - $"'spawn_settings.can_replace_roles' contains '{replace}', which the spawn system never evaluates - it will never trigger a replacement. Usable roles: {string.Join(", ", SpawnManager.SpawnEvaluatedRoles.OrderBy(r => r.ToString()))}."); + if (!delayed) + foreach (var replace in role.SpawnSettings.CanReplaceRoles.Where(r => + !SpawnManager.SpawnEvaluatedRoles.Contains(r))) + warnings.Add( + $"'spawn_settings.can_replace_roles' contains '{replace}', which the spawn system never evaluates - it will never trigger a replacement. Usable roles: {string.Join(", ", SpawnManager.SpawnEvaluatedRoles.OrderBy(r => r.ToString()))}. Set 'spawn_delay' if you want the role to be handed out mid-round instead."); foreach (var duplicate in role.SpawnSettings.CanReplaceRoles.GroupBy(r => r).Where(g => g.Count() > 1)) - warnings.Add( - $"'spawn_settings.can_replace_roles' lists '{duplicate.Key}' {duplicate.Count()} times, which multiplies the spawn chance for that role - remove the duplicates unless that is intended."); + warnings.Add(delayed + ? $"'spawn_settings.can_replace_roles' lists '{duplicate.Key}' {duplicate.Count()} times; remove the duplicates." + : $"'spawn_settings.can_replace_roles' lists '{duplicate.Key}' {duplicate.Count()} times, which multiplies the spawn chance for that role - remove the duplicates unless that is intended."); } if (role.SpawnSettings.SpawnZones is not null) @@ -465,10 +477,10 @@ private static void ValidateCustomFlags(ICustomRole role, string label) if (role.CustomFlags is null || role.CustomFlags.Count == 0) return; - Dictionary> flags; + List>> flags; try { - flags = YamlFlagsHandler.Decode(role.CustomFlags) ?? new Dictionary>(); + flags = YamlFlagsHandler.Decode(role.CustomFlags) ?? []; } catch (Exception e) { diff --git a/UncomplicatedCustomRoles/Manager/SpawnManager.cs b/UncomplicatedCustomRoles/Manager/SpawnManager.cs index fbbd651..45ddf56 100644 --- a/UncomplicatedCustomRoles/Manager/SpawnManager.cs +++ b/UncomplicatedCustomRoles/Manager/SpawnManager.cs @@ -573,56 +573,13 @@ internal static void SummonSubclassApplier(Player Player, ICustomRole Role, bool List candidates = []; foreach (var Role in CustomRole.CustomRoles.Values) - if (Role.SpawnSettings is not null && !Role.IgnoreSpawnSystem && + if (Role.SpawnSettings is not null && !Role.IgnoreSpawnSystem && Role.SpawnSettings.SpawnDelay <= 0 && Role.SpawnSettings.CanReplaceRoles is { } canReplaceRoles && canReplaceRoles.Contains(NewRole) && readyPlayers >= Role.SpawnSettings.MinPlayers && SummonedCustomRole.Count(Role) < Role.SpawnSettings.MaxPlayers) { - if (Role.SpawnSettings.RequiredPermission is not null) - { - static bool CheckPermission(Player player, string permission) - { - if (Enum.TryParse(permission, out PlayerPermissions playerPermissions)) - return player.HasPermission(playerPermissions); - - return player.HasAnyPermission(permission); - } - - static IEnumerable ExtractPermissions(object obj) - { - switch (obj) - { - case string s when !string.IsNullOrWhiteSpace(s): - return [s]; - case IEnumerable enumerable: - { - var list = new List(); - foreach (var item in enumerable) - { - if (item is null) continue; - var s = item.ToString(); - if (!string.IsNullOrWhiteSpace(s)) list.Add(s); - } - - return list; - } - default: - return []; - } - } - - var permsList = ExtractPermissions(Role.SpawnSettings.RequiredPermission).ToList(); - if (permsList.Any()) - { - var hasAll = permsList.All(p => CheckPermission(player, p)); - if (!hasAll) - { - LogManager.Debug( - $"Player {player.PlayerId} doesn't have the required permission(s) to spawn as role {Role.Name} ({Role.Id}), skipping... Player Permissions: {string.Join(", ", player.GetPermissions())}, Required permission(s): {string.Join(", ", permsList)}"); - continue; - } - } - } + if (!HasRequiredPermission(player, Role)) + continue; for (var a = 0; a < Role.SpawnSettings.SpawnChance; a++) candidates.Add(Role); @@ -634,6 +591,54 @@ static IEnumerable ExtractPermissions(object obj) return null; } + internal static bool HasRequiredPermission(Player player, ICustomRole role) + { + if (role.SpawnSettings?.RequiredPermission is null) + return true; + + static bool CheckPermission(Player player, string permission) + { + if (Enum.TryParse(permission, out PlayerPermissions playerPermissions)) + return player.HasPermission(playerPermissions); + + return player.HasAnyPermission(permission); + } + + static IEnumerable ExtractPermissions(object obj) + { + switch (obj) + { + case string s when !string.IsNullOrWhiteSpace(s): + return [s]; + case IEnumerable enumerable: + { + var list = new List(); + foreach (var item in enumerable) + { + if (item is null) continue; + var s = item.ToString(); + if (!string.IsNullOrWhiteSpace(s)) list.Add(s); + } + + return list; + } + default: + return []; + } + } + + var permsList = ExtractPermissions(role.SpawnSettings.RequiredPermission).ToList(); + if (!permsList.Any()) + return true; + + if (permsList.All(p => CheckPermission(player, p))) + return true; + + LogManager.Debug( + $"Player {player.PlayerId} doesn't have the required permission(s) to spawn as role {role.Name} ({role.Id}), skipping... Player Permissions: {string.Join(", ", player.GetPermissions())}, Required permission(s): {string.Join(", ", permsList)}"); + return false; + } + public static void AnnounceScpTermination(ReferenceHub scp, DamageHandlerBase hit) { var announcement1 = hit.CassieDeathAnnouncement.Announcement; diff --git a/UncomplicatedCustomRoles/Manager/YamlFlagsHandler.cs b/UncomplicatedCustomRoles/Manager/YamlFlagsHandler.cs index a17b44a..a0b170e 100644 --- a/UncomplicatedCustomRoles/Manager/YamlFlagsHandler.cs +++ b/UncomplicatedCustomRoles/Manager/YamlFlagsHandler.cs @@ -35,23 +35,29 @@ internal static void InvalidateCache() _modules = null; } - public static Dictionary?>? Decode(List flags) + public static List?>>? Decode(List flags) { if (flags is null) return null; - Dictionary?> result = new(); + List?>> result = []; foreach (var flag in flags) if (flag is Dictionary str) { foreach (var res in str) if (res.Value is Dictionary dict) - result[res.Key.ToString()] = dict.ConvertKeyToString(); + result.Add(new KeyValuePair?>(res.Key.ToString(), + dict.ConvertKeyToString())); + else if (res.Value is null) + result.Add(new KeyValuePair?>(res.Key.ToString(), null)); + else + LogManager.Warn( + $"[CM Loader] The custom flag '{res.Key}' has its settings written as '{res.Value}' instead of a list of 'setting: value' lines, so it can't be read and will be ignored."); } else { - result[flag.ToString()] = null; + result.Add(new KeyValuePair?>(flag.ToString(), null)); } return result; diff --git a/UncomplicatedCustomRoles/Plugin.cs b/UncomplicatedCustomRoles/Plugin.cs index fbf2750..82047bb 100644 --- a/UncomplicatedCustomRoles/Plugin.cs +++ b/UncomplicatedCustomRoles/Plugin.cs @@ -43,7 +43,7 @@ internal class Plugin : Plugin public override string Author => "FoxWorn3365, Dr.Agenda, MedveMarci"; - public override Version Version { get; } = new(9, 5, 1, 0); + public override Version Version { get; } = new(9, 6, 0, 0); public override Version RequiredApiVersion => new(LabApiProperties.CompiledVersion); diff --git a/UncomplicatedCustomRoles/Properties/AssemblyInfo.cs b/UncomplicatedCustomRoles/Properties/AssemblyInfo.cs index 6d334b3..2b56237 100644 --- a/UncomplicatedCustomRoles/Properties/AssemblyInfo.cs +++ b/UncomplicatedCustomRoles/Properties/AssemblyInfo.cs @@ -31,5 +31,5 @@ // È possibile specificare tutti i valori oppure impostare valori predefiniti per i numeri relativi alla revisione e alla build // usando l'asterisco '*' come illustrato di seguito: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("9.5.1.0")] -[assembly: AssemblyFileVersion("9.5.1.0")] \ No newline at end of file +[assembly: AssemblyVersion("9.6.0.0")] +[assembly: AssemblyFileVersion("9.6.0.0")] \ No newline at end of file From dc0372c9e1b3530e3870baf33d2b1ad051ff97ca Mon Sep 17 00:00:00 2001 From: MedveMarci Date: Wed, 5 Aug 2026 17:19:10 +0200 Subject: [PATCH 27/47] Version bump --- UncomplicatedCustomRoles/Plugin.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UncomplicatedCustomRoles/Plugin.cs b/UncomplicatedCustomRoles/Plugin.cs index 82047bb..460a236 100644 --- a/UncomplicatedCustomRoles/Plugin.cs +++ b/UncomplicatedCustomRoles/Plugin.cs @@ -43,7 +43,7 @@ internal class Plugin : Plugin public override string Author => "FoxWorn3365, Dr.Agenda, MedveMarci"; - public override Version Version { get; } = new(9, 6, 0, 0); + public override Version Version { get; } = new(9, 6, 0, 1); public override Version RequiredApiVersion => new(LabApiProperties.CompiledVersion); From 25cc6455fdd8faa83830cd10a9ec79a88016455a Mon Sep 17 00:00:00 2001 From: MedveMarci Date: Wed, 5 Aug 2026 17:19:29 +0200 Subject: [PATCH 28/47] Version bump2 --- UncomplicatedCustomRoles/Properties/AssemblyInfo.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/UncomplicatedCustomRoles/Properties/AssemblyInfo.cs b/UncomplicatedCustomRoles/Properties/AssemblyInfo.cs index 2b56237..04e18ce 100644 --- a/UncomplicatedCustomRoles/Properties/AssemblyInfo.cs +++ b/UncomplicatedCustomRoles/Properties/AssemblyInfo.cs @@ -31,5 +31,5 @@ // È possibile specificare tutti i valori oppure impostare valori predefiniti per i numeri relativi alla revisione e alla build // usando l'asterisco '*' come illustrato di seguito: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("9.6.0.0")] -[assembly: AssemblyFileVersion("9.6.0.0")] \ No newline at end of file +[assembly: AssemblyVersion("9.6.0.1")] +[assembly: AssemblyFileVersion("9.6.0.1")] \ No newline at end of file From 4bd60a18f35dd69e96b5def30594c296d59c55ba Mon Sep 17 00:00:00 2001 From: MedveMarci Date: Wed, 5 Aug 2026 18:06:03 +0200 Subject: [PATCH 29/47] Quick version manager update --- UncomplicatedCustomRoles/Commands/Version.cs | 3 +- .../Manager/NET/HttpManager.cs | 134 +++++++++++++++--- .../Manager/VersionManager.cs | 20 ++- UncomplicatedCustomRoles/Plugin.cs | 5 +- 4 files changed, 139 insertions(+), 23 deletions(-) diff --git a/UncomplicatedCustomRoles/Commands/Version.cs b/UncomplicatedCustomRoles/Commands/Version.cs index dfd3cb0..90ddee8 100644 --- a/UncomplicatedCustomRoles/Commands/Version.cs +++ b/UncomplicatedCustomRoles/Commands/Version.cs @@ -27,7 +27,8 @@ public bool Executor(List arguments, ICommandSender sender, out string r { if (VersionManager.VersionInfo is null) { - response = "Can't load VersionManager.VersionInfo: Failed to GET HTTPS"; + response = + $"UncomplicatedCustomRoles\nAuthors: {Plugin.Instance.Author}\nVersion: {Plugin.Instance.Version}\n\nThe UCS cloud has no informations about this version, so it can't be verified.\nThis is expected on an unreleased build, otherwise check the server console for the reason."; return false; } diff --git a/UncomplicatedCustomRoles/Manager/NET/HttpManager.cs b/UncomplicatedCustomRoles/Manager/NET/HttpManager.cs index f955247..aba17ca 100644 --- a/UncomplicatedCustomRoles/Manager/NET/HttpManager.cs +++ b/UncomplicatedCustomRoles/Manager/NET/HttpManager.cs @@ -76,20 +76,57 @@ public HttpManager(string prefix) public List IsJobRole { get; } = []; /// - /// Gets the latest of the plugin, loaded by the UCS cloud + /// Gets every version of the plugin known by the UCS cloud + /// + public List Versions + { + get + { + if (_versions is null) + LoadVersions(); + return _versions; + } + } + + /// + /// Gets the latest of the plugin, pre-releases included, loaded by the UCS cloud /// public Version LatestVersion { get { if (_latestVersion is null) - LoadLatestVersion(); + LoadVersions(); return _latestVersion; } } + /// + /// Gets the latest stable (non pre-release) of the plugin, loaded by the UCS cloud. + /// + public Version LatestStableVersion + { + get + { + if (_latestStableVersion is null) + LoadVersions(); + return _latestStableVersion; + } + } + + /// + /// Gets whether the running build is a pre-release + /// + public bool IsPreRelease => TryGetVersionInfo(Plugin.Instance.Version, out var info) + ? info.PreRelease != 0 + : Plugin.Instance.Version.Revision != 0; + + private List _versions { get; set; } + private Version _latestVersion { get; set; } + private Version _latestStableVersion { get; set; } + internal void RegisterEvents() { PlayerEvents.Joined += OnVerified; @@ -111,24 +148,91 @@ public string AddServerOwner(Player player, string discordId) JsonSerializer.Serialize(new OwnerMessage(player, discordId)), "application/json"); } - public void LoadLatestVersion() + public void LoadVersions() + { + _versions = []; + _latestVersion = new Version(); + _latestStableVersion = new Version(); + + string answer = null; + + try + { + answer = HttpQuery.Get($"{Endpoint}/{Prefix}/versions"); + _versions = JsonSerializer.Deserialize>(answer) ?? []; + } + catch + { + LogManager.Debug($"Failed to load the version list from the UCS cloud: '{answer}'"); + } + + foreach (var version in _versions) + { + if (!Version.TryParse(version.Name, out var parsed)) + continue; + + if (parsed > _latestVersion) + _latestVersion = parsed; + + if (version.PreRelease == 0 && parsed > _latestStableVersion) + _latestStableVersion = parsed; + } + + if (_versions.Count is 0) + LoadLatestVersionFallback(); + } + + /// + /// Loads the latest version from the single-value endpoint, used when the version list is unavailable. + /// + private void LoadLatestVersionFallback() { - var Version = HttpQuery.Get($"{Endpoint}/{Prefix}/versions/latest@text/plain"); + string answer = null; try { - if (!string.IsNullOrEmpty(Version) && Version.Contains(".")) - _latestVersion = new Version(Version.Trim()); - else - _latestVersion = new Version(); + answer = HttpQuery.Get($"{Endpoint}/{Prefix}/versions/latest@text/plain"); + + if (string.IsNullOrEmpty(answer) || !answer.Contains(".")) + return; + + _latestVersion = new Version(answer.Trim()); + + // That endpoint doesn't tell us whether it's a pre-release, and only pre-releases ship with a non-zero + // revision, so anything else can safely be treated as the latest stable one. + if (_latestVersion.Revision is 0) + _latestStableVersion = _latestVersion; } catch { - LogManager.Debug($"Failed to parse the latest version received from the UCS cloud: '{Version}'"); + LogManager.Debug($"Failed to parse the latest version received from the UCS cloud: '{answer}'"); _latestVersion = new Version(); } } + /// + /// Tries to get the cloud informations about the given version of the plugin + /// + public bool TryGetVersionInfo(Version version, out VersionInfo info) + { + info = Versions.FirstOrDefault(v => Version.TryParse(v.Name, out var parsed) && parsed == version); + return info is not null; + } + + /// + /// Gets the release the current installation should be updated to, or if there's + /// nothing newer to install. + /// + public Version GetUpdateTarget() + { + var current = Plugin.Instance.Version; + + if (IsPreRelease) + current = new Version(current.Major, current.Minor, Math.Max(current.Build, 0)); + + return LatestStableVersion.CompareTo(current) > 0 ? LatestStableVersion : null; + } + public void LoadCreditTags() { Credits = new Dictionary>(); @@ -205,19 +309,13 @@ public void ApplyCreditTag(Player player) public bool IsLatestVersion(out Version latest) { - latest = LatestVersion; - if (latest.CompareTo(Plugin.Instance.Version) > 0) - return false; - - return true; + latest = LatestStableVersion; + return GetUpdateTarget() is null; } public bool IsLatestVersion() { - if (LatestVersion.CompareTo(Plugin.Instance.Version) > 0) - return false; - - return true; + return GetUpdateTarget() is null; } internal HttpStatusCode ShareLogs(string data, out string content) diff --git a/UncomplicatedCustomRoles/Manager/VersionManager.cs b/UncomplicatedCustomRoles/Manager/VersionManager.cs index 0a1a72e..f8f8275 100644 --- a/UncomplicatedCustomRoles/Manager/VersionManager.cs +++ b/UncomplicatedCustomRoles/Manager/VersionManager.cs @@ -10,6 +10,7 @@ using System; using System.IO; +using System.Net; using System.Security.Cryptography; using System.Text.Json; using MEC; @@ -30,7 +31,21 @@ public static void Init() try { var data = Plugin.HttpManager.VersionInfo(); - data.GetStatusCode(out var msg); + + if (string.IsNullOrWhiteSpace(data)) + { + LogManager.Silent("The UCS cloud gave us an empty answer while asking for the version info."); + return; + } + + var status = data.GetStatusCode(out var msg); + if (status is not HttpStatusCode.Unused) + { + LogManager.Silent( + $"The UCS cloud has no info about v{Plugin.Instance.Version} - HTTP {(int)status}: {msg ?? "Message is null"}"); + return; + } + VersionInfo = JsonSerializer.Deserialize(data); if (VersionInfo is null) { @@ -41,8 +56,9 @@ public static void Init() if (VersionInfo.PreRelease != 0) { + var latestStable = Plugin.HttpManager.LatestStableVersion; LogManager.Info( - $"\nNOTICE!\nYou are currently using the version v{Plugin.Instance.Version}, who's a PRE-RELEASE or an EXPERIMENTAL RELESE of UncomplicatedCustomRoles!\nLatest stable release: {Plugin.HttpManager.LatestVersion}\nNOTE: This is NOT a stable version, so there can be bugs and malfunctions, for this reason we do not recommend use in production."); + $"\nNOTICE!\nYou are currently using the version v{Plugin.Instance.Version}, who's a PRE-RELEASE or an EXPERIMENTAL RELESE of UncomplicatedCustomRoles!\nLatest stable release: {(latestStable > new Version() ? $"v{latestStable}" : "unknown")}\nNOTE: This is NOT a stable version, so there can be bugs and malfunctions, for this reason we do not recommend use in production."); if (VersionInfo.ForceDebug != 0 && !(Plugin.Instance.Config?.Debug ?? true)) { LogManager.Info("Debug logs have been activated!"); diff --git a/UncomplicatedCustomRoles/Plugin.cs b/UncomplicatedCustomRoles/Plugin.cs index 460a236..5468753 100644 --- a/UncomplicatedCustomRoles/Plugin.cs +++ b/UncomplicatedCustomRoles/Plugin.cs @@ -74,9 +74,10 @@ public override void Enable() Task.Run(delegate { - if (HttpManager.LatestVersion.CompareTo(Version) > 0) + var updateTarget = HttpManager.GetUpdateTarget(); + if (updateTarget is not null) LogManager.Warn( - $"You are NOT using the latest version of UncomplicatedCustomRoles!\nCurrent: v{Version} | Latest available: v{HttpManager.LatestVersion}\nDownload it from GitHub: https://github.com/FoxWorn3365/UncomplicatedCustomRoles/releases/latest"); + $"You are NOT using the latest version of UncomplicatedCustomRoles!\nCurrent: v{Version} | Latest available: v{updateTarget}\nDownload it from GitHub: https://github.com/FoxWorn3365/UncomplicatedCustomRoles/releases/latest"); VersionManager.Init(); }); From 3b5a1e57181d5ef385d95d70d5da79318719764a Mon Sep 17 00:00:00 2001 From: MedveMarci Date: Mon, 10 Aug 2026 14:50:17 +0200 Subject: [PATCH 30/47] Downgraded System.Text.Json to 9.0.4 and removed System.ComponentModel --- UncomplicatedCustomRoles/UncomplicatedCustomRoles.csproj | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/UncomplicatedCustomRoles/UncomplicatedCustomRoles.csproj b/UncomplicatedCustomRoles/UncomplicatedCustomRoles.csproj index 0c1d251..967b8f1 100644 --- a/UncomplicatedCustomRoles/UncomplicatedCustomRoles.csproj +++ b/UncomplicatedCustomRoles/UncomplicatedCustomRoles.csproj @@ -32,8 +32,7 @@ - - + From 0fc0e374a4961ef7d58f126978676000656bc0b7 Mon Sep 17 00:00:00 2001 From: MedveMarci Date: Mon, 10 Aug 2026 14:50:57 +0200 Subject: [PATCH 31/47] Removed UCS Spawnpoint API; only local spawnpoint supported --- .../API/Features/SpawnPoint.cs | 10 +- .../Commands/SpawnPoint.cs | 120 ++--------- UncomplicatedCustomRoles/Config.cs | 4 - .../Manager/NET/SpawnPointApiCommunicator.cs | 197 ------------------ .../Manager/SpawnPointManager.cs | 126 +++++++++++ UncomplicatedCustomRoles/Plugin.cs | 4 +- 6 files changed, 150 insertions(+), 311 deletions(-) delete mode 100644 UncomplicatedCustomRoles/Manager/NET/SpawnPointApiCommunicator.cs create mode 100644 UncomplicatedCustomRoles/Manager/SpawnPointManager.cs diff --git a/UncomplicatedCustomRoles/API/Features/SpawnPoint.cs b/UncomplicatedCustomRoles/API/Features/SpawnPoint.cs index 529a810..c76c7f8 100644 --- a/UncomplicatedCustomRoles/API/Features/SpawnPoint.cs +++ b/UncomplicatedCustomRoles/API/Features/SpawnPoint.cs @@ -48,12 +48,12 @@ internal SpawnPoint(string name, Player player) : this(name, player.Room?.GameOb } /// - /// Gets the list of every synced in the server + /// Gets the list of every stored in the server /// public static HashSet List { get; } = []; /// - /// Gets the list of every unsynced in the server + /// Gets the list of every in the server that is not written to the local storage file /// public static HashSet UnsyncedList { get; } = []; @@ -83,7 +83,7 @@ internal SpawnPoint(string name, Player player) : this(name, player.Room?.GameOb public Triplet RoomRotationBase { get; } /// - /// Gets whether the is synced with the UCS cloud (or local file) or not + /// Gets whether the is stored inside the local SpawnPoint file or not /// [JsonIgnore] public bool Sync { get; set; } @@ -185,14 +185,14 @@ public override string ToString() } /// - /// Creates a new instance that is not synchronized with the network. + /// Creates a new instance that is not written to the local storage file. /// /// The unique name to assign to the spawn point. Cannot be null or empty. /// The identifier of the room to which the spawn point belongs. Cannot be null or empty. /// The base position of the spawn point, specified as a triplet of coordinates. /// The base rotation of the spawn point, specified as a quadruple representing rotation values. /// The base rotation of the room, specified as a triplet of rotation values. - /// A instance that is not registered for network synchronization. + /// A instance that is not stored on the disk. public static SpawnPoint CreateNotSync(string name, string roomId, Triplet positionBase, Quadruple rotationBase, Triplet roomRotationBase) { diff --git a/UncomplicatedCustomRoles/Commands/SpawnPoint.cs b/UncomplicatedCustomRoles/Commands/SpawnPoint.cs index dbb7ff4..b941114 100644 --- a/UncomplicatedCustomRoles/Commands/SpawnPoint.cs +++ b/UncomplicatedCustomRoles/Commands/SpawnPoint.cs @@ -9,14 +9,10 @@ */ using System.Collections.Generic; -using System.Net; -using System.Threading.Tasks; using CommandSystem; using LabApi.Features.Wrappers; using UncomplicatedCustomRoles.API.Interfaces; -using UncomplicatedCustomRoles.Extensions; using UncomplicatedCustomRoles.Manager; -using UncomplicatedCustomRoles.Manager.NET; using SpawnPointInstance = UncomplicatedCustomRoles.API.Features.SpawnPoint; namespace UncomplicatedCustomRoles.Commands; @@ -25,10 +21,7 @@ internal class SpawnPoint : IUCRCommand { public const string CommandHeader = "UncomplicatedCustomRoles - SpawnPoint Feature\n"; - public const string LocalError = - "Sorry but you can't perform that action while having your spawnpoints hosted in your local folder!"; - - public Dictionary> SubCommands = new() + public readonly Dictionary> SubCommands = new() { { "list", @@ -47,22 +40,13 @@ internal class SpawnPoint : IUCRCommand new KeyValuePair("(Name) ", "Teleport yourself to a SpawnPoint") }, { - "sync", - new KeyValuePair("", - "Update your local SpawnPoint list by downloading it from the UCS cloud") - }, - { - "migrate", - new KeyValuePair("(NewPort) ", "Migrate current SpawnPoints to another port (but same IP)") - }, - { - "download", + "reload", new KeyValuePair("", - "Get a link to download the current SpawnPoint list from the UCS cloud") + "Reload the SpawnPoint list from the local file, discarding every unsaved change") }, { - "ip", - new KeyValuePair("", "Get your current IPv4/IPv6") + "path", + new KeyValuePair("", "Show where the SpawnPoints of this server are stored") } }; @@ -96,7 +80,7 @@ public bool Executor(List arguments, ICommandSender sender, out string r { case "list": response = - $"{CommandHeader}Currently registered SpawnPoints ({SpawnPointInstance.List.Count}/{SpawnPointApiCommunicator.MaxSpawnPoints}):\n"; + $"{CommandHeader}Currently registered SpawnPoints ({SpawnPointInstance.List.Count}):\n"; foreach (var SpawnPoint in SpawnPointInstance.List) response += $"- {SpawnPoint}\n"; @@ -115,17 +99,11 @@ public bool Executor(List arguments, ICommandSender sender, out string r return false; } - if (SpawnPointInstance.List.Count >= SpawnPointApiCommunicator.MaxSpawnPoints) - { - response = - $"You've reached the maximum number of SpawnPoints for this port!\nMaximum: {SpawnPointApiCommunicator.MaxSpawnPoints}"; - return false; - } - new SpawnPointInstance(arguments[1], player); - SpawnPointApiCommunicator.AsyncPushSpawnPoints(); - response = $"SpawnPoint {arguments[1]} successfully created!"; + response = SpawnPointManager.Save() + ? $"SpawnPoint {arguments[1]} successfully created!" + : $"SpawnPoint {arguments[1]} created!\nThe SpawnPoint list has been updated but it could NOT be saved on the disk: check the server console!"; break; case "delete": if (arguments.Count != 2) @@ -137,67 +115,15 @@ public bool Executor(List arguments, ICommandSender sender, out string r if (SpawnPointInstance.TryGet(arguments[1], out var spawnPoint)) { spawnPoint.Destroy(); - response = "SpawnPoint successfully removed!"; - SpawnPointApiCommunicator.AsyncPushSpawnPoints(); + response = SpawnPointManager.Save() + ? "SpawnPoint successfully removed!" + : $"SpawnPoint removed!\nThe SpawnPoint list has been updated but it could NOT be saved on the disk: check the server console!"; } else { response = $"SpawnPoint '{arguments[1]}' not found!"; } - break; - case "migrate": - if (SpawnPointApiCommunicator.Local) - { - response = LocalError; - return false; - } - - if (arguments.Count < 2) - { - response = "Wrong usage!\nucr spawnpoint migrate (NewPort)"; - return false; - } - - if (!int.TryParse(arguments[1], out var newPort)) - { - response = $"'{arguments[1]}' is not a valid port number!"; - return false; - } - - if (arguments.Count == 2) - { - response = - $"Are you sure to migrate every SpawnPoint from port {Server.Port} to port {newPort}?\nIf yes do again the command:\nucr spawnpoint migrate {arguments[1]} yes"; - return true; - } - - if (arguments.Count == 3) - { - var Status = SpawnPointApiCommunicator.PushMigrationRequest(newPort).GetStatusCode(out _); - - if (Status is HttpStatusCode.OK) - { - response = "Migration completed!\nRefreshing the local database..."; - SpawnPointInstance.List.Clear(); - } - else - { - response = $"Migration failed!\nUCS cloud says: {Status}"; - } - } - - break; - case "download": - if (SpawnPointApiCommunicator.Local) - { - response = LocalError; - return false; - } - - var url = SpawnPointApiCommunicator.AskDownloadUrl(); - LogManager.Info($"Download your SpawnPoint settings with this URL:\n{url}"); - response = $"Download URL:\n{url}"; break; case "goto": if (arguments.Count != 2) @@ -223,24 +149,12 @@ public bool Executor(List arguments, ICommandSender sender, out string r } break; - case "ip": - if (SpawnPointApiCommunicator.Local) - { - response = LocalError; - return false; - } - - response = $"Your IPv4/IPv6 is: {SpawnPointApiCommunicator.AskIp()}"; - break; + case "reload": case "sync": - if (SpawnPointApiCommunicator.Local) - { - response = LocalError; - return false; - } - - response = "Sync started! The SpawnPoints are being downloaded in the background..."; - Task.Run(SpawnPointApiCommunicator.LoadFromCloud); + response = $"Reloaded {SpawnPointManager.Load()} SpawnPoints from the local storage!"; + break; + case "path": + response = $"Your SpawnPoints are stored in:\n{SpawnPointManager.FilePath}"; break; default: response = $"SubCommand '{arguments[0]}' not found!"; diff --git a/UncomplicatedCustomRoles/Config.cs b/UncomplicatedCustomRoles/Config.cs index a7eb65e..241c189 100644 --- a/UncomplicatedCustomRoles/Config.cs +++ b/UncomplicatedCustomRoles/Config.cs @@ -42,10 +42,6 @@ internal class Config [Description("Whether the NPCs can naturally spawn custom roles")] public bool IgnoreNpcs { get; set; } = true; - [Description( - "Whether you want your spawnpoints to be hosted inside our central server or locally in the configs folder")] - public bool LocalSpawnPoints { get; set; } = false; - [Description("Auto load the Custom Role ID from the file, bypassing YAML")] public bool UseIdFixer { get; set; } = false; diff --git a/UncomplicatedCustomRoles/Manager/NET/SpawnPointApiCommunicator.cs b/UncomplicatedCustomRoles/Manager/NET/SpawnPointApiCommunicator.cs deleted file mode 100644 index f2d8a41..0000000 --- a/UncomplicatedCustomRoles/Manager/NET/SpawnPointApiCommunicator.cs +++ /dev/null @@ -1,197 +0,0 @@ -/* - * This file is a part of the UncomplicatedCustomRoles project. - * - * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) - * - * This file is licensed under the GNU Affero General Public License v3.0. - * You should have received a copy of the AGPL license along with this file. - * If not, see . - */ - -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text.Json; -using System.Threading.Tasks; -using LabApi.Features.Wrappers; -using LabApi.Loader.Features.Paths; -using UncomplicatedCustomRoles.API.Enums; -using UncomplicatedCustomRoles.API.Features; -using UncomplicatedCustomRoles.API.Interfaces; - -namespace UncomplicatedCustomRoles.Manager.NET; - -internal class SpawnPointApiCommunicator -{ - /// - /// Gets the maximum number of SpawnPoints per server - /// - public const int MaxSpawnPoints = 100; // Don't worry, the check is also in the APIs backend :wink: - - /// - /// Gets the API endpoint - /// - public static string Endpoint => "https://api.ucserver.it/spawnpoints"; - - /// - /// Gets the file path for the local spawnpoints of this server - /// - public static string FilePath => Path.Combine(PathManager.Configs.FullName, $".{Server.Port}-spawnpoints.json"); - - /// - /// Gets whether the spawnpoints should be local or "global" - /// - public static bool Local => Plugin.Instance.Config.LocalSpawnPoints; - - /// - /// Init the Communicator - /// - public static void Init() - { - if (!Plugin.HttpManager.IsAllowed) - return; - - if (!File.Exists(FilePath)) - File.WriteAllText(FilePath, JsonSerializer.Serialize(new SpawnPoint[] { })); - - Task.Run(LoadFromCloud); - } - - /// - /// Retrive s loaded on UCS cloud - /// - public static void LoadFromCloud() - { - // We need first to reset the list - SpawnPoint.List.Clear(); - - if (Local) - { - try - { - TryLoadSpawnPoints(File.ReadAllText(FilePath)); - } - catch (Exception e) - { - LogManager.Warn($"Failed to load the local SpawnPoints from {FilePath}: {e.Message}"); - LogManager.Debug($"SpawnPointApiCommunicator::LoadFromCloud() failed - {e}"); - } - - return; - } - - try - { - TryLoadSpawnPoints(HttpQuery.Get($"{Endpoint}/list?port={Server.Port}")); - } - catch (Exception e) - { - LogManager.Warn($"Failed to load SpawnPoints from the UCS cloud: {e.Message}"); - LogManager.Debug($"SpawnPointApiCommunicator::LoadFromCloud() failed - {e}"); - } - } - - /// - /// Push the s inside UCS cloud - useful if the list has been updated!

- /// Every server has a limit of 10 ports with 10 spawnpoints for each one - ///
- /// - public static void PushSpawnPoints() - { - if (Local) - { - File.WriteAllText(FilePath, - JsonSerializer.Serialize(SpawnPoint.List.Where(s => s.Sync), - new JsonSerializerOptions { WriteIndented = true })); - return; - } - - try - { - var answer = HttpQuery.Post($"{Endpoint}/update?port={Server.Port}", - JsonSerializer.Serialize(SpawnPoint.List), "application/json"); - if (answer is "FILE_TOO_BIG_OR_SMALL" or "LIMIT_EXCEEDED" || answer.StartsWith("QTA_TOO_MUCH_")) - LogManager.Warn( - $"UCS cloud has declined the request: you have reached the maximum number of SpawnPoints: the current limit is: {MaxSpawnPoints} SpawnPoints per Server port and 10 total Server port!\nPlease contact us through our Discord! -- Server says: {answer}"); - else if (answer is "UNKNOWN_LOGIC" or "") - LogManager.Warn( - $"Failed to update your SpawnPoints on the UCS cloud: it seems to be broken!\nContact us as fast as possible!\nServer says: {answer}"); - else if (Plugin.Instance.Config.EnableBasicLogs) - LogManager.Info($"Your list of SpawnPoints on UCS cloud has been updated!\nServer says: {answer}"); - else - LogManager.Silent($"Your list of SpawnPoints on UCS cloud has been updated!\nServer says: {answer}"); - } - catch (Exception e) - { - LogManager.Warn($"Failed to push SpawnPoints to the UCS cloud: {e.Message}"); - LogManager.Debug($"SpawnPointApiCommunicator::PushSpawnPoints() failed - {e}"); - } - } - - /// - /// Async call the function - /// - /// - public static Task AsyncPushSpawnPoints() - { - return Task.Run(PushSpawnPoints); - } - - /// - /// Send a migration request to our central servers - /// - /// - /// - public static string PushMigrationRequest(int newPort) - { - return HttpQuery.Get($"{Endpoint}/migrate?port={Server.Port}&to={newPort}"); - } - - /// - /// Send a downloadUrl request to our central request and share the answer - /// - /// - public static string AskDownloadUrl() - { - return HttpQuery.Get($"{Endpoint}/download?port={Server.Port}"); - } - - public static string AskIp() - { - return HttpQuery.Get($"{Endpoint}/ip"); - } - - /// - /// Check every in order to find if any of them are with an invalid (non-existing) - /// SpawnPoint - /// - private static void CustomRoleSpawnCompatibilityChecker() - { - foreach (var role in CustomRole.CustomRoles.Values.Where(role => - role.SpawnSettings is not null && role.SpawnSettings.SpawnPoints is not null && - role.SpawnSettings.Spawn is SpawnType.SpawnPointSpawn)) - foreach (var spawnPoint in role.SpawnSettings.SpawnPoints) - if (!SpawnPoint.Exists(spawnPoint)) - LogManager.Warn( - $"CustomRole {role.Name} ({role.Id}) has an invalid SpawnPoint '{spawnPoint}' inside its configuration: the selected SpawnPoint does not exist!"); - } - - private static void TryLoadSpawnPoints(string json) - { - var List = JsonSerializer.Deserialize>(json); - - if (List is null) - { - LogManager.Warn("Failed to load the SpawnPoints: the received content is not a valid SpawnPoint list!"); - return; - } - - foreach (var SpawnPoint in List) - SpawnPoint.List.Add(SpawnPoint); - - LogManager.Info($"Loaded {List.Count} SpawnPoints from our central servers!"); - - CustomRoleSpawnCompatibilityChecker(); - } -} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Manager/SpawnPointManager.cs b/UncomplicatedCustomRoles/Manager/SpawnPointManager.cs new file mode 100644 index 0000000..6414112 --- /dev/null +++ b/UncomplicatedCustomRoles/Manager/SpawnPointManager.cs @@ -0,0 +1,126 @@ +/* + * This file is a part of the UncomplicatedCustomRoles project. + * + * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) + * + * This file is licensed under the GNU Affero General Public License v3.0. + * You should have received a copy of the AGPL license along with this file. + * If not, see . + */ + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.Json; +using LabApi.Features.Wrappers; +using LabApi.Loader.Features.Paths; +using UncomplicatedCustomRoles.API.Enums; +using UncomplicatedCustomRoles.API.Features; +using UncomplicatedCustomRoles.API.Interfaces; + +namespace UncomplicatedCustomRoles.Manager; + +internal static class SpawnPointManager +{ + private static readonly JsonSerializerOptions SerializerOptions = new() { WriteIndented = true }; + + public static string FilePath => Path.Combine(PathManager.Configs.FullName, $".{Server.Port}-spawnpoints.json"); + + public static void Init() + { + if (!File.Exists(FilePath)) + try + { + File.WriteAllText(FilePath, JsonSerializer.Serialize(Array.Empty(), SerializerOptions)); + } + catch (Exception e) + { + LogManager.Warn($"Failed to create the SpawnPoint storage file {FilePath}: {e.Message}"); + LogManager.Debug($"SpawnPointManager::Init() failed - {e}"); + return; + } + + Load(); + } + + public static int Load() + { + SpawnPoint.List.Clear(); + + string content; + + try + { + content = File.ReadAllText(FilePath); + } + catch (FileNotFoundException) + { + return 0; + } + catch (Exception e) + { + LogManager.Warn($"Failed to read the SpawnPoints from {FilePath}: {e.Message}"); + LogManager.Debug($"SpawnPointManager::Load() failed - {e}"); + return 0; + } + + List loaded; + + try + { + loaded = JsonSerializer.Deserialize>(content); + } + catch (Exception e) + { + SpawnPoint.List.Clear(); + LogManager.Warn( + $"Failed to parse the SpawnPoints stored in {FilePath}: {e.Message}\nThe file is not a valid SpawnPoint list, fix it or delete it to start over."); + LogManager.Debug($"SpawnPointManager::Load() failed - {e}"); + return 0; + } + + if (loaded is null) + { + LogManager.Warn( + $"Failed to load the SpawnPoints: the content of {FilePath} is not a valid SpawnPoint list!"); + return 0; + } + + if (Plugin.Instance.Config.EnableBasicLogs) + LogManager.Info($"Loaded {loaded.Count} SpawnPoints from {FilePath}"); + else + LogManager.Silent($"Loaded {loaded.Count} SpawnPoints from {FilePath}"); + + CustomRoleSpawnCompatibilityChecker(); + + return loaded.Count; + } + + public static bool Save() + { + try + { + File.WriteAllText(FilePath, JsonSerializer.Serialize(SpawnPoint.List.Where(s => s.Sync), + SerializerOptions)); + return true; + } + catch (Exception e) + { + LogManager.Error($"Failed to store the SpawnPoints inside {FilePath}: {e.Message}"); + LogManager.Debug($"SpawnPointManager::Save() failed - {e}"); + return false; + } + } + + private static void CustomRoleSpawnCompatibilityChecker() + { + foreach (var role in CustomRole.CustomRoles.Values.Where(role => + role.SpawnSettings is not null && role.SpawnSettings.SpawnPoints is not null && + role.SpawnSettings.Spawn is SpawnType.SpawnPointSpawn)) + foreach (var spawnPoint in role.SpawnSettings.SpawnPoints) + if (!SpawnPoint.Exists(spawnPoint)) + LogManager.Warn( + $"CustomRole {role.Name} ({role.Id}) has an invalid SpawnPoint '{spawnPoint}' inside its configuration: the selected SpawnPoint does not exist!"); + } +} diff --git a/UncomplicatedCustomRoles/Plugin.cs b/UncomplicatedCustomRoles/Plugin.cs index 5468753..a1ede07 100644 --- a/UncomplicatedCustomRoles/Plugin.cs +++ b/UncomplicatedCustomRoles/Plugin.cs @@ -43,7 +43,7 @@ internal class Plugin : Plugin public override string Author => "FoxWorn3365, Dr.Agenda, MedveMarci"; - public override Version Version { get; } = new(9, 6, 0, 1); + public override Version Version { get; } = new(9, 6, 0, 2); public override Version RequiredApiVersion => new(LabApiProperties.CompiledVersion); @@ -89,7 +89,7 @@ public override void Enable() FileConfigs.LoadAll(); FileConfigs.LoadAll(Server.Port.ToString()); - SpawnPointApiCommunicator.Init(); + SpawnPointManager.Init(); DisguiseTeam.Clear(); From 209759415b6cee8fd4966158fc1a95f38e39eac8 Mon Sep 17 00:00:00 2001 From: MedveMarci Date: Mon, 10 Aug 2026 14:51:22 +0200 Subject: [PATCH 32/47] Little tweaks for RoleValidator --- .../Manager/MapSpawnValidator.cs | 5 +- .../Manager/RoleValidator.cs | 105 ++++++++++-------- 2 files changed, 64 insertions(+), 46 deletions(-) diff --git a/UncomplicatedCustomRoles/Manager/MapSpawnValidator.cs b/UncomplicatedCustomRoles/Manager/MapSpawnValidator.cs index 11f576c..0f5aeab 100644 --- a/UncomplicatedCustomRoles/Manager/MapSpawnValidator.cs +++ b/UncomplicatedCustomRoles/Manager/MapSpawnValidator.cs @@ -24,6 +24,9 @@ internal static class MapSpawnValidator { internal static void ValidateAll() { + foreach (var role in CustomRole.CustomRoles.Values) + RoleValidator.ValidatePostLoad(role); + var rooms = Room.List; if (rooms is null || rooms.Count == 0) return; @@ -38,8 +41,6 @@ internal static void ValidateAll() foreach (var role in CustomRole.CustomRoles.Values) { - RoleValidator.ValidatePostLoad(role); - var spawn = role.SpawnSettings; if (spawn is null) continue; diff --git a/UncomplicatedCustomRoles/Manager/RoleValidator.cs b/UncomplicatedCustomRoles/Manager/RoleValidator.cs index 1e6c063..9d4375d 100644 --- a/UncomplicatedCustomRoles/Manager/RoleValidator.cs +++ b/UncomplicatedCustomRoles/Manager/RoleValidator.cs @@ -166,9 +166,6 @@ private static void ValidateHealthLike(ICustomRole role, List errors, Li errors.Add($"'health.maximum' must be at least 1, got {role.Health.Maximum}."); if (role.Health.Amount < 1) warnings.Add($"'health.amount' is {role.Health.Amount}; the player would spawn (nearly) dead."); - if (role.Health.Maximum >= 1 && role.Health.Amount > role.Health.Maximum) - warnings.Add( - $"'health.amount' ({role.Health.Amount}) is above 'health.maximum' ({role.Health.Maximum}); it will be capped."); } if (role.Ahp is not null) @@ -197,7 +194,7 @@ private static void ValidateHealthLike(ICustomRole role, List errors, Li $"'hume_shield.maximum' ({role.HumeShield.Maximum}) is below 'hume_shield.amount' ({role.HumeShield.Amount})."); if (role.HumeShield.RegenerationAmount < 0) warnings.Add( - $"'hume_shield.regeneration_amount' is negative ({role.HumeShield.RegenerationAmount}); the shield would drain instead of regenerating."); + $"'hume_shield.regeneration_amount' is negative ({role.HumeShield.RegenerationAmount}); the regeneration only runs when it is above 0, so the shield will never regenerate."); if (role.HumeShield.RegenerationDelay < 0) warnings.Add( $"'hume_shield.regeneration_delay' is negative ({role.HumeShield.RegenerationDelay}); use 0 for no delay."); @@ -234,9 +231,16 @@ private static void ValidateEffects(ICustomRole role, List warnings) } if (string.IsNullOrWhiteSpace(effect.EffectType) || - !EffectNames.Any(n => n.StartsWith(effect.EffectType, StringComparison.InvariantCultureIgnoreCase))) + !EffectNames.Any(n => string.Equals(n, effect.EffectType, StringComparison.InvariantCultureIgnoreCase))) + { + var closest = string.IsNullOrWhiteSpace(effect.EffectType) + ? null + : EffectNames.FirstOrDefault(n => + n.StartsWith(effect.EffectType, StringComparison.InvariantCultureIgnoreCase)); + warnings.Add( - $"'effects' entry #{i + 1} has an unknown effect_type '{effect.EffectType}'; it will be skipped. Valid effects: {string.Join(", ", EffectNames)}."); + $"'effects' entry #{i + 1} has an unknown effect_type '{effect.EffectType}'; it will be skipped.{(closest is null ? string.Empty : $" Did you mean '{closest}'?")} Valid effects: {string.Join(", ", EffectNames)}."); + } if (effect.Intensity == 0) warnings.Add( @@ -280,8 +284,9 @@ private static void ValidateInventoryLimits(ICustomRole role, List warni .Select(kvp => kvp.Key)); foreach (var category in role.CustomInventoryLimits.Keys.Where(c => !configurable.Contains(c))) - warnings.Add( - $"'custom_inventory_limits' contains '{category}', whose limit cannot be overridden; the entry is ignored. Configurable categories: {string.Join(", ", configurable.OrderBy(c => c.ToString()))}."); + warnings.Add(category is ItemCategory.Ammo + ? "'custom_inventory_limits' contains 'Ammo', which the game does not count in inventory slots; the entry does nothing. Ammo is limited per ammo type, not per category." + : $"'custom_inventory_limits' contains '{category}', which the game does not limit by slot count. UCR still applies the limit server-side, but the client's inventory HUD will not show it. Categories the game limits on its own: {string.Join(", ", configurable.OrderBy(c => c.ToString()))}."); } catch (Exception e) { @@ -301,8 +306,16 @@ private static void ValidateMisc(ICustomRole role, List warnings) if (role.SpawnHintDuration < 0) warnings.Add($"'spawn_hint_duration' is negative ({role.SpawnHintDuration})."); - if (role.Scale is { x: 0, y: 0, z: 0 }) - warnings.Add("'scale' is 0 on every axis; the player would be invisible. Use 1 for the normal size."); + var scale = role.Scale; + if (scale.x != 0 || scale.y != 0 || scale.z != 0) + { + if (scale.x < 0 || scale.y < 0 || scale.z < 0) + warnings.Add( + $"'scale' has a negative axis ({scale.x}, {scale.y}, {scale.z}); the model will be turned inside out. Use 1 for the normal size."); + else if (scale.x == 0 || scale.y == 0 || scale.z == 0) + warnings.Add( + $"'scale' has an axis set to 0 ({scale.x}, {scale.y}, {scale.z}); the model will be flattened on it. Use 1 for the normal size, or 0 on every axis to keep the vanilla one."); + } } private static void ValidateSpawnSettings(ICustomRole role, List errors, List warnings) @@ -331,19 +344,32 @@ private static void ValidateSpawnSettings(ICustomRole role, List errors, break; } - if (role.SpawnSettings.SpawnChance < 0) - warnings.Add( - $"'spawn_settings.spawn_chance' should be more than 0, got {role.SpawnSettings.SpawnChance}."); + if (role.SpawnSettings.SpawnZones is not null) + foreach (var zone in role.SpawnSettings.SpawnZones.Where(z => z is FacilityZone.None)) + warnings.Add( + $"'spawn_settings.spawn_zones' contains '{zone}', which is not a real facility zone. Valid zones: LightContainment, HeavyContainment, Entrance, Surface."); - if (role.SpawnSettings.MinPlayers < 1) - warnings.Add($"'spawn_settings.min_players' should be at least 1, got {role.SpawnSettings.MinPlayers}."); + if (role.SpawnSettings.SpawnRoles is not null) + foreach (var spawnRole in role.SpawnSettings.SpawnRoles.Where(r => + r is RoleTypeId.None || r.GetTeam() is Team.Dead)) + warnings.Add( + $"'spawn_settings.spawn_roles' contains '{spawnRole}', which is not a spawnable role to take a spawn position from."); - if (role.SpawnSettings.MaxPlayers < 1) + if (role.IgnoreSpawnSystem) + return; + + ValidateSpawnEligibility(role, warnings); + } + + private static void ValidateSpawnEligibility(ICustomRole role, List warnings) + { + if (role.SpawnSettings.SpawnChance <= 0) warnings.Add( - $"'spawn_settings.max_players' is {role.SpawnSettings.MaxPlayers}; the role will never spawn naturally."); - else if (role.SpawnSettings.MaxPlayers < role.SpawnSettings.MinPlayers) + $"'spawn_settings.spawn_chance' is {role.SpawnSettings.SpawnChance}; it has to be above 0 or the role will never spawn on its own (only 'ucr spawn' and the API can still hand it out)."); + + if (role.SpawnSettings.MaxPlayers < 1) warnings.Add( - $"'spawn_settings.max_players' ({role.SpawnSettings.MaxPlayers}) is below 'min_players' ({role.SpawnSettings.MinPlayers}); the role will never spawn."); + $"'spawn_settings.max_players' is {role.SpawnSettings.MaxPlayers}; it is the number of players that can hold this role at the same time, so the role will never spawn on its own."); var delayed = role.SpawnSettings.SpawnDelay > 0; @@ -351,34 +377,18 @@ private static void ValidateSpawnSettings(ICustomRole role, List errors, warnings.Add( $"'spawn_settings.spawn_delay' is negative ({role.SpawnSettings.SpawnDelay}); use 0 to spawn the role together with the vanilla role it replaces."); - if (delayed && (role.SpawnSettings.CanReplaceRoles is null || !role.SpawnSettings.CanReplaceRoles.Any())) - warnings.Add( - "'spawn_settings.spawn_delay' is set but 'can_replace_roles' is empty; the delayed spawn has nobody to convert. List the roles the players should be taken from, e.g. 'Spectator'."); - - if (role.SpawnSettings.CanReplaceRoles is not null) + if (role.SpawnSettings.CanReplaceRoles is not { } canReplaceRoles || !canReplaceRoles.Any()) { - if (!delayed) - foreach (var replace in role.SpawnSettings.CanReplaceRoles.Where(r => - !SpawnManager.SpawnEvaluatedRoles.Contains(r))) - warnings.Add( - $"'spawn_settings.can_replace_roles' contains '{replace}', which the spawn system never evaluates - it will never trigger a replacement. Usable roles: {string.Join(", ", SpawnManager.SpawnEvaluatedRoles.OrderBy(r => r.ToString()))}. Set 'spawn_delay' if you want the role to be handed out mid-round instead."); - - foreach (var duplicate in role.SpawnSettings.CanReplaceRoles.GroupBy(r => r).Where(g => g.Count() > 1)) - warnings.Add(delayed - ? $"'spawn_settings.can_replace_roles' lists '{duplicate.Key}' {duplicate.Count()} times; remove the duplicates." - : $"'spawn_settings.can_replace_roles' lists '{duplicate.Key}' {duplicate.Count()} times, which multiplies the spawn chance for that role - remove the duplicates unless that is intended."); + warnings.Add(delayed + ? "'spawn_settings.spawn_delay' is set but 'can_replace_roles' is empty; the delayed spawn has nobody to convert. List the roles the players should be taken from, e.g. 'Spectator'." + : "'spawn_settings.can_replace_roles' is empty; with no delay the role is handed out by replacing one of these roles at spawn, so an empty list means it never spawns on its own. List the vanilla roles it should replace, e.g. 'ClassD'."); + return; } - if (role.SpawnSettings.SpawnZones is not null) - foreach (var zone in role.SpawnSettings.SpawnZones.Where(z => z is FacilityZone.None)) + if (!delayed) + foreach (var replace in canReplaceRoles.Where(r => !SpawnManager.SpawnEvaluatedRoles.Contains(r))) warnings.Add( - $"'spawn_settings.spawn_zones' contains '{zone}', which is not a real facility zone. Valid zones: LightContainment, HeavyContainment, Entrance, Surface."); - - if (role.SpawnSettings.SpawnRoles is not null) - foreach (var spawnRole in role.SpawnSettings.SpawnRoles.Where(r => - r is RoleTypeId.None || r.GetTeam() is Team.Dead)) - warnings.Add( - $"'spawn_settings.spawn_roles' contains '{spawnRole}', which is not a spawnable role to take a spawn position from."); + $"'spawn_settings.can_replace_roles' contains '{replace}', which the spawn system never evaluates - it will never trigger a replacement. Usable roles: {string.Join(", ", SpawnManager.SpawnEvaluatedRoles.OrderBy(r => r.ToString()))}. Set 'spawn_delay' if you want the role to be handed out mid-round instead."); } private static void ValidateRoleAfterEscape(ICustomRole role, List warnings) @@ -416,9 +426,16 @@ private static void ValidateRoleAfterEscape(ICustomRole role, List warni } } - if (kvp.Value is "Deny" or "deny" or "DENY" || string.IsNullOrEmpty(kvp.Value)) + if (kvp.Value is "Deny" or "deny" or "DENY") continue; + if (string.IsNullOrWhiteSpace(kvp.Value)) + { + warnings.Add( + $"'role_after_escape' value for '{kvp.Key}' is empty; the escaping player would end up as a Spectator. Use 'Deny' to block the escape, or 'InternalRole ' / 'CustomRole '."); + continue; + } + var value = kvp.Value.Split(' '); if (value.Length != 2) warnings.Add( From f1e08304211d88683d658e1a240ae24d95e4a6fc Mon Sep 17 00:00:00 2001 From: MedveMarci Date: Mon, 10 Aug 2026 15:30:30 +0200 Subject: [PATCH 33/47] Updated VersionManager --- UncomplicatedCustomRoles/Commands/Version.cs | 9 +- .../Manager/NET/HttpManager.cs | 100 +++++++++++++++--- .../Manager/VersionManager.cs | 6 +- UncomplicatedCustomRoles/Plugin.cs | 7 +- 4 files changed, 103 insertions(+), 19 deletions(-) diff --git a/UncomplicatedCustomRoles/Commands/Version.cs b/UncomplicatedCustomRoles/Commands/Version.cs index 90ddee8..9c91401 100644 --- a/UncomplicatedCustomRoles/Commands/Version.cs +++ b/UncomplicatedCustomRoles/Commands/Version.cs @@ -32,8 +32,15 @@ public bool Executor(List arguments, ICommandSender sender, out string r return false; } + var source = string.IsNullOrWhiteSpace(VersionManager.VersionInfo.Source) + ? "unknown" + : VersionManager.VersionInfo.Source; + + if (!string.IsNullOrWhiteSpace(VersionManager.VersionInfo.SourceLink)) + source += $" - {VersionManager.VersionInfo.SourceLink}"; + response = - $"UncomplicatedCustomRoles\nAuthors: {Plugin.Instance.Author}\nVersion: {VersionManager.VersionInfo.Name}{(VersionManager.VersionInfo.CustomName is not null ? $" '{VersionManager.VersionInfo.CustomName}'" : string.Empty)} ({Plugin.Instance.Version})\nSource: {VersionManager.VersionInfo.Source} - {VersionManager.VersionInfo.SourceLink ?? string.Empty}\nPre release: {(VersionManager.VersionInfo.PreRelease != 0 ? "TRUE" : "FALSE")}\nForced debug: {(VersionManager.VersionInfo.ForceDebug != 0 ? "TRUE" : "FALSE")}\nHash: {(!VersionManager.CorrectHash ? "NOT MATCHING!" : "Matching")}"; + $"UncomplicatedCustomRoles\nAuthors: {Plugin.Instance.Author}\nVersion: {VersionManager.VersionInfo.Name}{(VersionManager.VersionInfo.CustomName is not null ? $" '{VersionManager.VersionInfo.CustomName}'" : string.Empty)} ({Plugin.Instance.Version})\nSource: {source}\nPre release: {(VersionManager.VersionInfo.PreRelease != 0 ? "TRUE" : "FALSE")}\nForced debug: {(VersionManager.VersionInfo.ForceDebug != 0 ? "TRUE" : "FALSE")}\nHash: {(!VersionManager.CorrectHash ? "NOT MATCHING!" : "Matching")}"; if (!VersionManager.CorrectHash) response += diff --git a/UncomplicatedCustomRoles/Manager/NET/HttpManager.cs b/UncomplicatedCustomRoles/Manager/NET/HttpManager.cs index aba17ca..655ce4f 100644 --- a/UncomplicatedCustomRoles/Manager/NET/HttpManager.cs +++ b/UncomplicatedCustomRoles/Manager/NET/HttpManager.cs @@ -28,6 +28,12 @@ namespace UncomplicatedCustomRoles.Manager.NET; internal class HttpManager { + private const string GitHubReleases = "https://github.com/UncomplicatedCustomServer/UncomplicatedCustomRoles/releases"; + + private const string GitHubLatestRelease = GitHubReleases + "/latest"; + + private const string DiscordInvite = "https://discord.gg/5StRGu8EJV"; + /// /// Create a new istance of the HttpManager /// @@ -114,12 +120,23 @@ public Version LatestStableVersion } } + /// + /// Gets the latest pre-release of the plugin, loaded by the UCS cloud. + /// + public Version LatestPreRelease + { + get + { + if (_latestPreRelease is null) + LoadVersions(); + return _latestPreRelease; + } + } + /// /// Gets whether the running build is a pre-release /// - public bool IsPreRelease => TryGetVersionInfo(Plugin.Instance.Version, out var info) - ? info.PreRelease != 0 - : Plugin.Instance.Version.Revision != 0; + public bool IsPreRelease => IsPreReleaseVersion(Plugin.Instance.Version); private List _versions { get; set; } @@ -127,6 +144,8 @@ public Version LatestStableVersion private Version _latestStableVersion { get; set; } + private Version _latestPreRelease { get; set; } + internal void RegisterEvents() { PlayerEvents.Joined += OnVerified; @@ -148,11 +167,37 @@ public string AddServerOwner(Player player, string discordId) JsonSerializer.Serialize(new OwnerMessage(player, discordId)), "application/json"); } + internal static int CompareReleases(Version left, Version right) + { + var release = new Version(left.Major, left.Minor, Math.Max(left.Build, 0)) + .CompareTo(new Version(right.Major, right.Minor, Math.Max(right.Build, 0))); + + if (release != 0) + return release; + + var leftPreRelease = Math.Max(left.Revision, 0); + var rightPreRelease = Math.Max(right.Revision, 0); + + if (leftPreRelease == rightPreRelease) + return 0; + + if (leftPreRelease is 0) + return 1; + + return rightPreRelease is 0 ? -1 : leftPreRelease.CompareTo(rightPreRelease); + } + + public bool IsPreReleaseVersion(Version version) + { + return TryGetVersionInfo(version, out var info) ? info.PreRelease != 0 : version.Revision > 0; + } + public void LoadVersions() { _versions = []; _latestVersion = new Version(); _latestStableVersion = new Version(); + _latestPreRelease = new Version(); string answer = null; @@ -171,11 +216,18 @@ public void LoadVersions() if (!Version.TryParse(version.Name, out var parsed)) continue; - if (parsed > _latestVersion) + if (CompareReleases(parsed, _latestVersion) > 0) _latestVersion = parsed; - if (version.PreRelease == 0 && parsed > _latestStableVersion) - _latestStableVersion = parsed; + if (version.PreRelease == 0) + { + if (CompareReleases(parsed, _latestStableVersion) > 0) + _latestStableVersion = parsed; + } + else if (CompareReleases(parsed, _latestPreRelease) > 0) + { + _latestPreRelease = parsed; + } } if (_versions.Count is 0) @@ -198,10 +250,10 @@ private void LoadLatestVersionFallback() _latestVersion = new Version(answer.Trim()); - // That endpoint doesn't tell us whether it's a pre-release, and only pre-releases ship with a non-zero - // revision, so anything else can safely be treated as the latest stable one. - if (_latestVersion.Revision is 0) + if (_latestVersion.Revision <= 0) _latestStableVersion = _latestVersion; + else + _latestPreRelease = _latestVersion; } catch { @@ -218,6 +270,16 @@ public bool TryGetVersionInfo(Version version, out VersionInfo info) info = Versions.FirstOrDefault(v => Version.TryParse(v.Name, out var parsed) && parsed == version); return info is not null; } + + private Version ResolveChannelTarget() + { + var target = LatestStableVersion; + + if (IsPreRelease && CompareReleases(LatestPreRelease, target) > 0) + target = LatestPreRelease; + + return target; + } /// /// Gets the release the current installation should be updated to, or if there's @@ -225,12 +287,22 @@ public bool TryGetVersionInfo(Version version, out VersionInfo info) /// public Version GetUpdateTarget() { - var current = Plugin.Instance.Version; + var target = ResolveChannelTarget(); + return CompareReleases(target, Plugin.Instance.Version) > 0 ? target : null; + } + + public string GetDownloadHint(Version version) + { + TryGetVersionInfo(version, out var info); - if (IsPreRelease) - current = new Version(current.Major, current.Minor, Math.Max(current.Build, 0)); + var link = string.IsNullOrWhiteSpace(info?.SourceLink) ? null : info.SourceLink.Trim(); - return LatestStableVersion.CompareTo(current) > 0 ? LatestStableVersion : null; + return info?.Source?.Trim().ToLowerInvariant() switch + { + "discord" => $"Download it from our Discord server: {link ?? DiscordInvite}", + "other" when link is not null => $"Download it from: {link}", + _ => $"Download it from GitHub: {link ?? (IsPreReleaseVersion(version) ? GitHubReleases : GitHubLatestRelease)}" + }; } public void LoadCreditTags() @@ -309,7 +381,7 @@ public void ApplyCreditTag(Player player) public bool IsLatestVersion(out Version latest) { - latest = LatestStableVersion; + latest = ResolveChannelTarget(); return GetUpdateTarget() is null; } diff --git a/UncomplicatedCustomRoles/Manager/VersionManager.cs b/UncomplicatedCustomRoles/Manager/VersionManager.cs index f8f8275..8790643 100644 --- a/UncomplicatedCustomRoles/Manager/VersionManager.cs +++ b/UncomplicatedCustomRoles/Manager/VersionManager.cs @@ -105,8 +105,12 @@ public static void HashNotMatchMessageSender(string hash) public static void RecallMessageSender() { + var download = Version.TryParse(VersionInfo.RecallTarget, out var target) + ? $"\n{Plugin.HttpManager.GetDownloadHint(target)}" + : string.Empty; + LogManager.Warn( - $"\n>>> IMPORTANT NOTICE <<<\nThe current version of the plugin ({VersionInfo.Name}) HAS BEEN RECALLED FOR THE FOLLOWING REASON:\n| {VersionInfo.RecallReason?.Replace(Environment.NewLine, $"{Environment.NewLine}| ")}\nFor that reason we are asking you to PLEASE update to the next stable version, who's the {VersionInfo.RecallTarget}!\nThis version CONTAINS IMPORTANT BUGS and for that reason SWITCHING TO THE NEWER ONE IS ESSENTIAL!"); + $"\n>>> IMPORTANT NOTICE <<<\nThe current version of the plugin ({VersionInfo.Name}) HAS BEEN RECALLED FOR THE FOLLOWING REASON:\n| {VersionInfo.RecallReason?.Replace(Environment.NewLine, $"{Environment.NewLine}| ")}\nFor that reason we are asking you to PLEASE update to the next stable version, who's the {VersionInfo.RecallTarget}!{download}\nThis version CONTAINS IMPORTANT BUGS and for that reason SWITCHING TO THE NEWER ONE IS ESSENTIAL!"); } public static string HashFile(string path) diff --git a/UncomplicatedCustomRoles/Plugin.cs b/UncomplicatedCustomRoles/Plugin.cs index a1ede07..5ec5c8d 100644 --- a/UncomplicatedCustomRoles/Plugin.cs +++ b/UncomplicatedCustomRoles/Plugin.cs @@ -76,8 +76,9 @@ public override void Enable() { var updateTarget = HttpManager.GetUpdateTarget(); if (updateTarget is not null) - LogManager.Warn( - $"You are NOT using the latest version of UncomplicatedCustomRoles!\nCurrent: v{Version} | Latest available: v{updateTarget}\nDownload it from GitHub: https://github.com/FoxWorn3365/UncomplicatedCustomRoles/releases/latest"); + LogManager.Warn(HttpManager.IsPreReleaseVersion(updateTarget) + ? $"A newer PRE-RELEASE of UncomplicatedCustomRoles is available!\nCurrent: v{Version} | Latest pre-release: v{updateTarget}\n{HttpManager.GetDownloadHint(updateTarget)}" + : $"You are NOT using the latest version of UncomplicatedCustomRoles!\nCurrent: v{Version} | Latest available: v{updateTarget}\n{HttpManager.GetDownloadHint(updateTarget)}"); VersionManager.Init(); }); @@ -141,7 +142,7 @@ public void OnFinishedLoadingPlugins() if (_welcomeShown || Config is not { EnableBasicLogs: true }) return; _welcomeShown = true; - LogManager.Info($"Thanks for using UncomplicatedCustomRoles v{Version.ToString(3)} by {Author}!", + LogManager.Info($"Thanks for using UncomplicatedCustomRoles v{Version} by {Author}!", ConsoleColor.Blue); LogManager.Info( "To receive support and to stay up-to-date, join our official Discord server: https://discord.gg/5StRGu8EJV", From aea9d2ed429dbc5ca683dbe093c71fed5906009d Mon Sep 17 00:00:00 2001 From: MedveMarci Date: Sat, 15 Aug 2026 18:42:29 +0200 Subject: [PATCH 34/47] Added CustomInfo formatter --- .../API/Features/CustomInfo.cs | 26 ++++++++++++++++--- .../Extensions/StringExtension.cs | 12 +++++++-- .../Manager/RoleValidator.cs | 15 ++++++++--- 3 files changed, 45 insertions(+), 8 deletions(-) diff --git a/UncomplicatedCustomRoles/API/Features/CustomInfo.cs b/UncomplicatedCustomRoles/API/Features/CustomInfo.cs index 5693be0..f33e372 100644 --- a/UncomplicatedCustomRoles/API/Features/CustomInfo.cs +++ b/UncomplicatedCustomRoles/API/Features/CustomInfo.cs @@ -112,7 +112,8 @@ public void UpdateInfo(Player player) var rawInfo = Info; var rawRole = Role; - if (!NicknameSync.ValidateCustomInfo(Info, out var customInfoError) && !string.IsNullOrEmpty(Info)) + if (!NicknameSync.ValidateCustomInfo(Info.SanitizeCustomInfo(), out var customInfoError) && + !string.IsNullOrEmpty(Info)) { LogManager.Error( $"CustomInfo is not correct, therefore the custom info part of player {player.PlayerId} won't be shown.\nCustomInfo: {Info}\nError: {customInfoError}"); @@ -120,7 +121,8 @@ public void UpdateInfo(Player player) rawInfo = string.Empty; } - if (!NicknameSync.ValidateCustomInfo(Role, out var roleNameError) && !string.IsNullOrEmpty(Role)) + if (!NicknameSync.ValidateCustomInfo(Role.SanitizeCustomInfo(), out var roleNameError) && + !string.IsNullOrEmpty(Role)) { LogManager.Error( $"RoleName is not correct, therefore the role name part of player {player.PlayerId} won't be shown.\nRoleName: {Role}\nError: {roleNameError}"); @@ -237,12 +239,30 @@ public void UpdateInfo(Player player) private static void ApplyCustomInfo(Player player, string composed) { + var cleaned = composed.SanitizeCustomInfo(); + + if (cleaned != composed) + { + LogManager.Debug( + $"Removed the characters the game does not accept in a name tag from the tag of player {player.PlayerId}.\nBefore: {composed}\nAfter: {cleaned}"); + composed = cleaned; + } + + if (!string.IsNullOrEmpty(composed) && composed.Length > 400) + { + LogManager.Error( + $"The name tag of player {player.PlayerId} is {composed.Length} characters long, but the game only accepts 400, so it won't be shown.\n" + + $"Composed tag: {composed}\n" + + "Shorten the 'custom_info' of the role, or the InfoTag layout building this tag."); + composed = string.Empty; + } + if (!string.IsNullOrEmpty(composed) && !NicknameSync.ValidateCustomInfo(composed, out var error)) { LogManager.Error( $"The name tag of player {player.PlayerId} would be rejected by the game and won't be shown: {error}\n" + $"Composed tag: {composed}\n" + - "Likely causes: a colour that isn't on the allowed list written inside 'custom_info', a '[' or ']' coming from a nickname, or a tag longer than 400 characters."); + "Likely causes: a colour that isn't on the allowed list written inside 'custom_info', or a rich text tag the game does not allow."); composed = string.Empty; } diff --git a/UncomplicatedCustomRoles/Extensions/StringExtension.cs b/UncomplicatedCustomRoles/Extensions/StringExtension.cs index c74d1aa..de29343 100644 --- a/UncomplicatedCustomRoles/Extensions/StringExtension.cs +++ b/UncomplicatedCustomRoles/Extensions/StringExtension.cs @@ -13,13 +13,14 @@ using System.Linq; using System.Net; using System.Text.Json; +using System.Text.RegularExpressions; using UncomplicatedCustomRoles.Manager; namespace UncomplicatedCustomRoles.Extensions; public static class StringExtension { - public static readonly HashSet _intChars = + public static readonly HashSet INTChars = [ '0', '1', @@ -32,13 +33,20 @@ public static class StringExtension '8', '9' ]; + + private static readonly Regex CustomInfoRejectedChars = new(@"[\[\]]|[^\p{L}\p{P}\p{Sc}\p{N} ^=+|~`<>\n]", RegexOptions.Compiled); + + public static string SanitizeCustomInfo(this string str) + { + return string.IsNullOrEmpty(str) ? str : CustomInfoRejectedChars.Replace(str, string.Empty); + } public static string ToInt(this string str, string separator = "") { List result = []; foreach (var ch in str) - if (_intChars.Contains(ch)) + if (INTChars.Contains(ch)) result.Add(ch); return string.Join(separator, result); diff --git a/UncomplicatedCustomRoles/Manager/RoleValidator.cs b/UncomplicatedCustomRoles/Manager/RoleValidator.cs index 9d4375d..50c2aac 100644 --- a/UncomplicatedCustomRoles/Manager/RoleValidator.cs +++ b/UncomplicatedCustomRoles/Manager/RoleValidator.cs @@ -20,6 +20,7 @@ using UncomplicatedCustomRoles.API.Features; using UncomplicatedCustomRoles.API.Features.CustomModules; using UncomplicatedCustomRoles.API.Interfaces; +using UncomplicatedCustomRoles.Extensions; using UncomplicatedCustomRoles.Integrations; namespace UncomplicatedCustomRoles.Manager; @@ -94,9 +95,17 @@ private static void ValidateIdentity(ICustomRole role, List errors, List if (string.IsNullOrWhiteSpace(role.Name)) warnings.Add("'name' is empty; it is used to identify the role in logs and commands."); - if (!string.IsNullOrEmpty(role.CustomInfo) - && !NicknameSync.ValidateCustomInfo(role.CustomInfo, out var customInfoError)) - warnings.Add($"'custom_info' will be rejected by the game: {customInfoError}"); + if (!string.IsNullOrEmpty(role.CustomInfo)) + { + var sanitized = role.CustomInfo.SanitizeCustomInfo(); + + if (sanitized != role.CustomInfo) + warnings.Add( + "'custom_info' contains characters the game does not accept in a name tag (square brackets, emoji, ...); they are removed automatically, so the text will be shown without them."); + + if (!NicknameSync.ValidateCustomInfo(sanitized, out var customInfoError)) + warnings.Add($"'custom_info' will be rejected by the game: {customInfoError}"); + } ValidatePlaceholders("nickname", role.Nickname, warnings); ValidatePlaceholders("custom_info", role.CustomInfo, warnings); From 2308fc930616e010187faecb271e1dd304bd019d Mon Sep 17 00:00:00 2001 From: MedveMarci Date: Wed, 26 Aug 2026 21:52:27 +0200 Subject: [PATCH 35/47] Fixed "Collection was modified" --- UncomplicatedCustomRoles/Manager/ImportManager.cs | 5 +---- .../Manager/PluginImportManager.cs | 3 ++- UncomplicatedCustomRoles/Manager/YamlFlagsHandler.cs | 11 +++++++++-- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/UncomplicatedCustomRoles/Manager/ImportManager.cs b/UncomplicatedCustomRoles/Manager/ImportManager.cs index 393bc6f..23f3f6e 100644 --- a/UncomplicatedCustomRoles/Manager/ImportManager.cs +++ b/UncomplicatedCustomRoles/Manager/ImportManager.cs @@ -12,7 +12,6 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; -using System.Threading.Tasks; using LabApi.Loader; using UncomplicatedCustomRoles.API.Attributes; using UncomplicatedCustomRoles.API.Features; @@ -23,7 +22,6 @@ namespace UncomplicatedCustomRoles.Manager; internal class ImportManager { - public const float WaitingTime = 5f; public static readonly List ActivePlugins = []; public static readonly List AvailableAssemblies = []; @@ -35,8 +33,7 @@ public static void Init() if (_alreadyLoaded) return; - // Call a delayed task - Task.Run(Actor); + Actor(); } public static void Reload() diff --git a/UncomplicatedCustomRoles/Manager/PluginImportManager.cs b/UncomplicatedCustomRoles/Manager/PluginImportManager.cs index 5c48c29..72001db 100644 --- a/UncomplicatedCustomRoles/Manager/PluginImportManager.cs +++ b/UncomplicatedCustomRoles/Manager/PluginImportManager.cs @@ -58,6 +58,7 @@ private static void ImportCustomRoles(Assembly assembly) private static void ImportCustomModules(Assembly assembly) { - ImportManager.AvailableAssemblies.Add(assembly); // Subscribe for the YamlFlagsHandler check-up + if (!ImportManager.AvailableAssemblies.Contains(assembly)) + ImportManager.AvailableAssemblies.Add(assembly); } } \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Manager/YamlFlagsHandler.cs b/UncomplicatedCustomRoles/Manager/YamlFlagsHandler.cs index a0b170e..64ea3fd 100644 --- a/UncomplicatedCustomRoles/Manager/YamlFlagsHandler.cs +++ b/UncomplicatedCustomRoles/Manager/YamlFlagsHandler.cs @@ -25,8 +25,15 @@ public static Type[] Modules { get { - _modules ??= GetModules(); - return _modules; + var cached = _modules; + + if (cached is not null) + return cached; + + cached = GetModules(); + _modules = cached; + + return cached; } } From b9accf9094dca44171fb94875096ba792ec852ef Mon Sep 17 00:00:00 2001 From: MedveMarci Date: Wed, 26 Aug 2026 21:58:22 +0200 Subject: [PATCH 36/47] Fixed RoleAppearance when invalid or empty --- UncomplicatedCustomRoles/API/Features/CustomRole.cs | 10 ++++++++-- .../PreviousVersionRoles/BonolisCustomRole.cs | 2 +- .../PreviousVersionRoles/FossuonCustomRole.cs | 2 +- .../PreviousVersionRoles/PreviousVersionRole.cs | 2 +- UncomplicatedCustomRoles/Manager/RoleValidator.cs | 4 ++-- 5 files changed, 13 insertions(+), 7 deletions(-) diff --git a/UncomplicatedCustomRoles/API/Features/CustomRole.cs b/UncomplicatedCustomRoles/API/Features/CustomRole.cs index 89ecd64..6168dc9 100644 --- a/UncomplicatedCustomRoles/API/Features/CustomRole.cs +++ b/UncomplicatedCustomRoles/API/Features/CustomRole.cs @@ -97,9 +97,11 @@ public class CustomRole : ICustomRole /// /// Gets or sets the the Role Appeareance for the player.

- /// If it's equal to then won't be applied + /// If it's equal to then won't be applied.

+ /// Leave it empty to keep the appearance of : anything that is not a usable alive role + /// falls back to it when the role is registered. ///
- public virtual RoleTypeId RoleAppearance { get; set; } = RoleTypeId.ClassD; + public virtual RoleTypeId RoleAppearance { get; set; } = RoleTypeId.None; /// /// Gets or sets the (s) that will be "friends" with this custom role @@ -425,6 +427,10 @@ internal static LoadStatusType InternalRegister(ICustomRole customRole) { FlagMigrator.Migrate(customRole); + if (customRole.RoleAppearance is RoleTypeId.None || + customRole.RoleAppearance.GetTeam() is PlayerRoles.Team.Dead) + customRole.RoleAppearance = customRole.Role; + if (Plugin.Instance.Config.EnableValidator) { RoleValidator.Validate(customRole, out var errors, out var warnings); diff --git a/UncomplicatedCustomRoles/Compatibility/PreviousVersionRoles/BonolisCustomRole.cs b/UncomplicatedCustomRoles/Compatibility/PreviousVersionRoles/BonolisCustomRole.cs index e4ef725..8ddf0f4 100644 --- a/UncomplicatedCustomRoles/Compatibility/PreviousVersionRoles/BonolisCustomRole.cs +++ b/UncomplicatedCustomRoles/Compatibility/PreviousVersionRoles/BonolisCustomRole.cs @@ -40,7 +40,7 @@ internal class BonolisCustomRole : IPreviousVersionRole public virtual Team? Team { get; set; } = null; - public virtual RoleTypeId RoleAppearance { get; set; } = RoleTypeId.ClassD; + public virtual RoleTypeId RoleAppearance { get; set; } = RoleTypeId.None; public virtual List IsFriendOf { get; set; } = []; diff --git a/UncomplicatedCustomRoles/Compatibility/PreviousVersionRoles/FossuonCustomRole.cs b/UncomplicatedCustomRoles/Compatibility/PreviousVersionRoles/FossuonCustomRole.cs index 26dc001..fa3b2f7 100644 --- a/UncomplicatedCustomRoles/Compatibility/PreviousVersionRoles/FossuonCustomRole.cs +++ b/UncomplicatedCustomRoles/Compatibility/PreviousVersionRoles/FossuonCustomRole.cs @@ -44,7 +44,7 @@ public class FossuonCustomRole : IPreviousVersionRole public virtual Team? Team { get; set; } = null; - public virtual RoleTypeId RoleAppearance { get; set; } = RoleTypeId.ClassD; + public virtual RoleTypeId RoleAppearance { get; set; } = RoleTypeId.None; public virtual List IsFriendOf { get; set; } = []; diff --git a/UncomplicatedCustomRoles/Compatibility/PreviousVersionRoles/PreviousVersionRole.cs b/UncomplicatedCustomRoles/Compatibility/PreviousVersionRoles/PreviousVersionRole.cs index 577eb22..38188dc 100644 --- a/UncomplicatedCustomRoles/Compatibility/PreviousVersionRoles/PreviousVersionRole.cs +++ b/UncomplicatedCustomRoles/Compatibility/PreviousVersionRoles/PreviousVersionRole.cs @@ -42,7 +42,7 @@ internal class PreviousVersionRole : IPreviousVersionRole public virtual Team? Team { get; set; } = null; - public virtual RoleTypeId RoleAppearance { get; set; } = RoleTypeId.ClassD; + public virtual RoleTypeId RoleAppearance { get; set; } = RoleTypeId.None; public virtual List IsFriendOf { get; set; } = []; diff --git a/UncomplicatedCustomRoles/Manager/RoleValidator.cs b/UncomplicatedCustomRoles/Manager/RoleValidator.cs index 50c2aac..f42ef6c 100644 --- a/UncomplicatedCustomRoles/Manager/RoleValidator.cs +++ b/UncomplicatedCustomRoles/Manager/RoleValidator.cs @@ -162,9 +162,9 @@ private static void ValidateRoles(ICustomRole role, List errors, List errors, List warnings) From 7673a98d3d185ed5bb81899eb791fba9ae6124d4 Mon Sep 17 00:00:00 2001 From: MedveMarci Date: Wed, 26 Aug 2026 22:44:32 +0200 Subject: [PATCH 37/47] fix: the custom info parts will be translated by the client if it's not modified by the plugin --- .../API/Features/CustomInfo.cs | 151 ++++++++++++++---- .../Patches/PlayerInfoSyncPatch.cs | 10 +- 2 files changed, 126 insertions(+), 35 deletions(-) diff --git a/UncomplicatedCustomRoles/API/Features/CustomInfo.cs b/UncomplicatedCustomRoles/API/Features/CustomInfo.cs index f33e372..b679998 100644 --- a/UncomplicatedCustomRoles/API/Features/CustomInfo.cs +++ b/UncomplicatedCustomRoles/API/Features/CustomInfo.cs @@ -8,6 +8,7 @@ * If not, see . */ +using System; using System.Collections.Generic; using LabApi.Features.Wrappers; using PlayerRoles; @@ -21,9 +22,12 @@ namespace UncomplicatedCustomRoles.API.Features; public class CustomInfo { + private const string ColorPrefix = ""; private Player _lastOwner; - private bool _detached; + private bool _nativeNickname = true; + private bool _nativeRole = true; + private bool _nativeUnit = true; public CustomInfo(string nickname, string role, string info) { @@ -91,6 +95,27 @@ internal void Detach() _lastOwner = null; } + internal PlayerInfoArea ApplyAreas(PlayerInfoArea value, PlayerInfoArea? original = null) + { + var restore = original ?? value; + + value |= PlayerInfoArea.CustomInfo; + + value = _nativeNickname + ? value | (restore & PlayerInfoArea.Nickname) + : value & ~PlayerInfoArea.Nickname; + + value = _nativeRole + ? value | (restore & PlayerInfoArea.Role) + : value & ~PlayerInfoArea.Role; + + value = _nativeUnit + ? value | (restore & PlayerInfoArea.UnitName) + : value & ~PlayerInfoArea.UnitName; + + return value; + } + public void UpdateInfo(Player player) { if (_detached) @@ -102,12 +127,33 @@ public void UpdateInfo(Player player) SuppressExternalSync = true; try { - player.InfoArea |= PlayerInfoArea.CustomInfo; - player.InfoArea &= ~PlayerInfoArea.Role; - player.InfoArea &= ~PlayerInfoArea.Nickname; - player.InfoArea &= ~PlayerInfoArea.UnitName; + var hasCustomRole = player.TryGetSummonedInstance(out var summonedCustomRole); + + InfoTag infoTag = null; + CustomInfoOrder customInfoOrderModule = null; + ColorfulNickname colorfulNickname = null; + + if (hasCustomRole) + { + summonedCustomRole.TryGetModule(out infoTag); + summonedCustomRole.TryGetModule(out customInfoOrderModule); + summonedCustomRole.TryGetModule(out colorfulNickname); + } + + var customLayout = infoTag is not null || customInfoOrderModule is not null; - var rawCustomInfo = "%custominfo%%nickname%%rolename%"; + _nativeRole = !customLayout && IsNativeRoleName(player, summonedCustomRole); + + _nativeNickname = _nativeRole && colorfulNickname is null && + (string.IsNullOrEmpty(Nickname) || Nickname == player.DisplayName); + + var hidesUnitName = hasCustomRole && summonedCustomRole.HasModule(); + + _nativeUnit = _nativeRole && !hidesUnitName; + + player.InfoArea = ApplyAreas(player.InfoArea, summonedCustomRole?.PlayerInfoArea); + + var rawCustomInfo = $"{ColorPrefix}%custominfo%%nickname%%rolename%"; var rawNickname = Nickname; var rawInfo = Info; var rawRole = Role; @@ -121,7 +167,7 @@ public void UpdateInfo(Player player) rawInfo = string.Empty; } - if (!NicknameSync.ValidateCustomInfo(Role.SanitizeCustomInfo(), out var roleNameError) && + if (!_nativeRole && !NicknameSync.ValidateCustomInfo(Role.SanitizeCustomInfo(), out var roleNameError) && !string.IsNullOrEmpty(Role)) { LogManager.Error( @@ -130,7 +176,7 @@ public void UpdateInfo(Player player) rawRole = string.Empty; } - if (player.TryGetSummonedInstance(out var summonedCustomRole)) + if (hasCustomRole) { rawInfo = PlaceholderManager.ApplyPlaceholders(rawInfo, player, summonedCustomRole.Role); @@ -140,16 +186,15 @@ public void UpdateInfo(Player player) var rawUnit = string.Empty; var showUnit = false; - if (!string.IsNullOrEmpty(rawRole) && !summonedCustomRole.HasModule() - && infoTeam is Team.FoundationForces - && NamingRulesManager.TryGetNamingRule(infoTeam, out var infoUnitRule) - && !string.IsNullOrEmpty(infoUnitRule.LastGeneratedName)) + + if (!_nativeUnit && !hidesUnitName && !string.IsNullOrEmpty(rawRole) && + TryGetUnitName(player, infoTeam, out var ownUnit)) { showUnit = true; - rawUnit = infoUnitRule.LastGeneratedName; + rawUnit = ownUnit; } - if (summonedCustomRole.TryGetModule(out InfoTag infoTag)) + if (infoTag is not null) { if (infoTag.ShowBadge) player.InfoArea |= PlayerInfoArea.Badge; @@ -165,10 +210,10 @@ public void UpdateInfo(Player player) return; } - if (summonedCustomRole.TryGetModule(out CustomInfoOrder customInfoOrderModule)) - rawCustomInfo = $"{customInfoOrderModule.Order}"; + if (customInfoOrderModule is not null) + rawCustomInfo = $"{ColorPrefix}{customInfoOrderModule.Order}"; - if (summonedCustomRole.TryGetModule(out ColorfulNickname colorfulNickname)) + if (colorfulNickname is not null) { LogManager.Debug( $"Applying ColorfulNickname module to player {player.PlayerId} with color {colorfulNickname.Color} and nickname {Nickname}"); @@ -202,20 +247,25 @@ public void UpdateInfo(Player player) rawInfo = PlaceholderManager.ApplyPlaceholders(rawInfo, player, null); } + if (_nativeNickname) + { + rawCustomInfo = rawCustomInfo.Replace("%nickname%", ""); + rawNickname = string.Empty; + } + + if (_nativeRole) + { + rawCustomInfo = rawCustomInfo.Replace("%rolename%", ""); + rawRole = string.Empty; + } + if (string.IsNullOrEmpty(rawInfo)) rawCustomInfo = rawCustomInfo.Replace("%custominfo%", ""); - if (string.IsNullOrEmpty(rawNickname)) + if (!_nativeNickname && string.IsNullOrEmpty(rawNickname)) rawNickname = player.Nickname; - if (string.IsNullOrEmpty(rawInfo) && string.IsNullOrEmpty(rawRole) && string.IsNullOrEmpty(player.Nickname)) - { - player.InfoArea |= PlayerInfoArea.Nickname | PlayerInfoArea.Role | PlayerInfoArea.UnitName; - player.CustomInfo = string.Empty; - return; - } - - ApplyCustomInfo(player, rawCustomInfo.Replace("%%", "%\n%").BulkReplace(new Dictionary + var composed = rawCustomInfo.Replace("%%", "%\n%").BulkReplace(new Dictionary { { "custominfo", @@ -229,7 +279,15 @@ public void UpdateInfo(Player player) "rolename", rawRole } - }, "%%")); + }, "%%"); + + if (string.IsNullOrWhiteSpace(composed.Replace(ColorPrefix, string.Empty))) + { + player.CustomInfo = string.Empty; + return; + } + + ApplyCustomInfo(player, composed); } finally { @@ -237,7 +295,40 @@ public void UpdateInfo(Player player) } } - private static void ApplyCustomInfo(Player player, string composed) + private bool IsNativeRoleName(Player player, SummonedCustomRole summonedCustomRole) + { + var shownRole = summonedCustomRole is null + ? player.Role + : summonedCustomRole.Appearance != RoleTypeId.None + ? summonedCustomRole.Appearance + : summonedCustomRole.Role.Role; + + return string.Equals(Role, shownRole.GetFullName(), StringComparison.Ordinal); + } + + private static bool TryGetUnitName(Player player, Team team, out string unitName) + { + unitName = string.Empty; + + if (!NamingRulesManager.TryGetNamingRule(team, out var namingRule)) + return false; + + if (!DisguiseTeam.RoleBaseList.ContainsKey(player.PlayerId) && player.RoleBase is HumanRole humanRole && + humanRole.Team == team) + { + var ownUnitName = NamingRulesManager.ClientFetchReceived(team, humanRole.UnitNameId); + if (!string.IsNullOrEmpty(ownUnitName)) + { + unitName = ownUnitName; + return true; + } + } + + unitName = namingRule.LastGeneratedName; + return !string.IsNullOrEmpty(unitName); + } + + private void ApplyCustomInfo(Player player, string composed) { var cleaned = composed.SanitizeCustomInfo(); @@ -268,6 +359,10 @@ private static void ApplyCustomInfo(Player player, string composed) if (string.IsNullOrEmpty(composed)) { + _nativeNickname = true; + _nativeRole = true; + _nativeUnit = true; + player.InfoArea |= PlayerInfoArea.Nickname | PlayerInfoArea.Role | PlayerInfoArea.UnitName; player.CustomInfo = string.Empty; } diff --git a/UncomplicatedCustomRoles/Patches/PlayerInfoSyncPatch.cs b/UncomplicatedCustomRoles/Patches/PlayerInfoSyncPatch.cs index d702bb6..98b3c50 100644 --- a/UncomplicatedCustomRoles/Patches/PlayerInfoSyncPatch.cs +++ b/UncomplicatedCustomRoles/Patches/PlayerInfoSyncPatch.cs @@ -43,12 +43,8 @@ private static void Prefix(NicknameSync __instance, ref PlayerInfoArea value) if (CustomInfo.SuppressExternalSync) return; - if (__instance._hub is not null && __instance._hub.TryGetSummonedInstance(out _)) - { - value |= PlayerInfoArea.CustomInfo; - value &= ~PlayerInfoArea.Role; - value &= ~PlayerInfoArea.Nickname; - value &= ~PlayerInfoArea.UnitName; - } + if (__instance._hub is not null && __instance._hub.TryGetSummonedInstance(out var role) && + role.CustomInfo is not null) + value = role.CustomInfo.ApplyAreas(value); } } \ No newline at end of file From aff2fe1bf79a001dbfeddf1ed1172e563a3e7976 Mon Sep 17 00:00:00 2001 From: MedveMarci Date: Wed, 26 Aug 2026 23:15:51 +0200 Subject: [PATCH 38/47] Updated README.md --- README.md | 63 ++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 42 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index c8034e2..f240a26 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -
+
Easy, fully configurable and customizable custom roles for your SCP:SL Server! @@ -9,8 +9,11 @@

-**LabAPI** >= `v1.x` -

+## Requirements +- **LabAPI** >= `v1.x` +- **Harmony** (`0Harmony.dll`) >= `v2.x` + +
## Localized READMEs - [🇫🇷 Français](https://github.com/UncomplicatedCustomServer/UncomplicatedCustomRoles/blob/main/Localization/README-FR.md) @@ -20,26 +23,42 @@ - [🇵🇱 Polski](https://github.com/UncomplicatedCustomServer/UncomplicatedCustomRoles/blob/main/Localization/README-PL.md) - [🇨🇳 简体中文](https://github.com/UncomplicatedCustomServer/UncomplicatedCustomRoles/blob/main/Localization/README-CN.md) +> [!NOTE] +> The localized READMEs are written and updated by volunteers from the community, so they can fall behind this one. +> If a translation is missing something, or says something different, **this English README is the one that is up to date**. + ## What's UncomplicatedCustomRoles -**UncomplicatedCustomRoles** or **UCR** is a plugin for **LabAPI** that allow you to create fully configurable and customizable custom role with YAML.\ +**UncomplicatedCustomRoles** or **UCR** is a plugin for **LabAPI** that lets you create fully configurable and customizable custom roles with YAML.\ +A custom role starts from a normal SCP:SL role and changes whatever you want about it: health, items, effects, spawn point, team, custom info and much more.\ With UCR, you can fully customize your Custom Roles by modifying almost every setting, allowing you to create whatever you can imagine: the only limit is your imagination ## Features ### 🖥️ Fully customizable Custom Roles -Let your imagination run wild with virtually complete customization of the basic roles.\ -Health, tags, role, name, special mechanics: make your SCP:SL server one-of-a-kind! -### 🫂 Active community on Discord -Join our Discord community to chat with other users and even the developers!\ -Interact, ask for help, share your knowledge and your roles: we’d love to have you! -### 📑 Exaustive documentation -Check out the [official UCR documentation](https://docs.ucr.ucserver.it/), where you'll find everything you need to use the plugin: from getting started to advanced settings! +Health, AHP, hume shield, stamina, scale, nickname, badge, role name, custom info, effects, damage multiplier, inventory, ammo, item limits and custom items: make your SCP:SL server one-of-a-kind! +### 🎯 Spawns exactly where and when you want +Pick how every role spawns: a random room, a whole zone, specific rooms, a spawn point you saved in-game, the position of another role, the Class-D cells, or simply wherever the player already is.\ +Then tune when it happens: spawn chance, spawn delay, how many can be alive at the same time, the minimum amount of players, which vanilla roles it may replace and the permission a player needs to get it. +### 🧩 More than 25 built-in modules +Add extra mechanics to a role with a single line of config: damage resistance, life stealing, custom keycard permissions, item bans, pacifism until the role takes damage, silent footsteps, tesla gates and SCP-096 that ignore the role, a fake team, a custom custom info layout, a schematic attached to the player, and many more. +### 🚪 Escape system +Decide whether a role can escape and which role it becomes afterwards — with a different outcome depending on whether it escaped free or cuffed, and by whom — and let it keep its inventory on the way out. +### 🔗 Plays well with your other plugins +UCR integrates out of the box with **UncomplicatedCustomItems**, **UncomplicatedCustomTeams**, **Exiled CustomItems**, **ScriptedEvents**, **SLWardrobe**, **RespawnTimer** and **LabApiExtensions**. +### 🧪 Built-in role validator +Every role is checked while it is being loaded, and UCR tells you in the console what is wrong with it, so a typo in a config does not turn into a broken round. +### ⌨️ In-game commands +Manage your custom roles using the many built-in commands provided by UCR:\ +`ucr list`, `ucr info`, `ucr role`, `ucr spawn`, `ucr cinfo`, `ucr reload`, `ucr spawnpoint`, `ucr percentages`, `ucr errors`, `ucr generate`, `ucr update`, `ucr version` and more. ### 🗂️ YAML based You don't need to know how to code to use UCR: custom roles are created using `yml` ([YAML](https://en.wikipedia.org/wiki/YAML)) files, an extremely easy and intuitive serialization language! +### 📑 Exhaustive documentation +Check out the [official UCR documentation](https://docs.ucr.ucserver.it/), where you'll find everything you need to use the plugin: from getting started to advanced settings! ### 🔌 Designed for developers UCR was also designed to make life easier for developers: integrating with the plugin is simple, intuitive, and well-documented!\ [Check it out!](https://docs.ucr.ucserver.it/developers/intro) -### ⌨️ In-game commands -Manage your custom roles using the many built-in commands provided by UCR. +### 🫂 Active community on Discord +Join our Discord community to chat with other users and even the developers!\ +Interact, ask for help, share your knowledge and your roles: we’d love to have you! ## Bugs and plan To track bugs and UCR planning, UCSC has made a FlySpray instance available.\ @@ -50,12 +69,14 @@ Check the [documentation](https://docs.ucr.ucserver.it/getting-started/installat ## If you use UCR, please consider making a donation UCR is a plugin made by **UCS Collective**.\ -Every plugin we create is **free** and **open-source**.\ -Please consider donating something to support our work through **OpenCollective**: +Every plugin we create is **free** and **open-source**, and it always will be.\ +What there is, is the time we spend writing the plugin, answering your questions on Discord and keeping everything working after every SCP:SL update.\ +If UCR is running on your server, **please consider donating something through OpenCollective** — every contribution, however small, goes straight back into the plugins you are using: +   Donate ## Contacts -### UncomplicatedCustomRoles +### UCS - UncomplicatedCustomServer **Discord:** [https://discord.gg/5StRGu8EJV](https://discord.gg/5StRGu8EJV) ### FoxWorn3365 @@ -67,9 +88,9 @@ Please consider donating something to support our work through **OpenCollective* **Discord:** `dr.agenda` ## Translation Credits -**French:** `@robocnop`\ -**Italian:** `@foxworn`\ -**Russian:** `@naxefir`\ +**Français:** `@robocnop`\ +**Italiano:** `@foxworn`\ +**Русский:** `@naxefir`\ **Deutsch:** `@seekedstroy`\ -**Polish:** `@.piwnica2137`\ -**Simplified Chinese** `@Raiden-Yayi` +**Polski:** `@.piwnica2137`\ +**简体中文:** `@Raiden-Yayi` \ No newline at end of file From f495a76fe4b249542dce899d93ab0621d13d0885 Mon Sep 17 00:00:00 2001 From: MedveMarci Date: Wed, 26 Aug 2026 23:16:35 +0200 Subject: [PATCH 39/47] Added OpenCollective funding --- .github/FUNDING.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..139c6a9 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,15 @@ +# These are supported funding model platforms + +github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +patreon: # Replace with a single Patreon username +open_collective: ucs +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry +polar: # Replace with a single Polar username +buy_me_a_coffee: # Replace with a single Buy Me a Coffee username +thanks_dev: # Replace with a single thanks.dev username +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] From cf70751db309171c704d2c3b66cb2be7e1f62a82 Mon Sep 17 00:00:00 2001 From: MedveMarci Date: Wed, 26 Aug 2026 23:31:47 +0200 Subject: [PATCH 40/47] 9.6.0-rev3 version bump --- UncomplicatedCustomRoles/Plugin.cs | 2 +- UncomplicatedCustomRoles/Properties/AssemblyInfo.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/UncomplicatedCustomRoles/Plugin.cs b/UncomplicatedCustomRoles/Plugin.cs index 5ec5c8d..1f251cc 100644 --- a/UncomplicatedCustomRoles/Plugin.cs +++ b/UncomplicatedCustomRoles/Plugin.cs @@ -43,7 +43,7 @@ internal class Plugin : Plugin public override string Author => "FoxWorn3365, Dr.Agenda, MedveMarci"; - public override Version Version { get; } = new(9, 6, 0, 2); + public override Version Version { get; } = new(9, 6, 0, 3); public override Version RequiredApiVersion => new(LabApiProperties.CompiledVersion); diff --git a/UncomplicatedCustomRoles/Properties/AssemblyInfo.cs b/UncomplicatedCustomRoles/Properties/AssemblyInfo.cs index 04e18ce..96a3760 100644 --- a/UncomplicatedCustomRoles/Properties/AssemblyInfo.cs +++ b/UncomplicatedCustomRoles/Properties/AssemblyInfo.cs @@ -31,5 +31,5 @@ // È possibile specificare tutti i valori oppure impostare valori predefiniti per i numeri relativi alla revisione e alla build // usando l'asterisco '*' come illustrato di seguito: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("9.6.0.1")] -[assembly: AssemblyFileVersion("9.6.0.1")] \ No newline at end of file +[assembly: AssemblyVersion("9.6.0.3")] +[assembly: AssemblyFileVersion("9.6.0.3")] \ No newline at end of file From a41f2824b5c1cd74d2cf483c4ed21ed7210b5b06 Mon Sep 17 00:00:00 2001 From: MedveMarci Date: Thu, 27 Aug 2026 11:52:36 +0200 Subject: [PATCH 41/47] 9.6.0 version bump --- UncomplicatedCustomRoles/Plugin.cs | 2 +- UncomplicatedCustomRoles/Properties/AssemblyInfo.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/UncomplicatedCustomRoles/Plugin.cs b/UncomplicatedCustomRoles/Plugin.cs index 1f251cc..dfc1279 100644 --- a/UncomplicatedCustomRoles/Plugin.cs +++ b/UncomplicatedCustomRoles/Plugin.cs @@ -43,7 +43,7 @@ internal class Plugin : Plugin public override string Author => "FoxWorn3365, Dr.Agenda, MedveMarci"; - public override Version Version { get; } = new(9, 6, 0, 3); + public override Version Version { get; } = new(9, 6, 0); public override Version RequiredApiVersion => new(LabApiProperties.CompiledVersion); diff --git a/UncomplicatedCustomRoles/Properties/AssemblyInfo.cs b/UncomplicatedCustomRoles/Properties/AssemblyInfo.cs index 96a3760..cf2b58b 100644 --- a/UncomplicatedCustomRoles/Properties/AssemblyInfo.cs +++ b/UncomplicatedCustomRoles/Properties/AssemblyInfo.cs @@ -31,5 +31,5 @@ // È possibile specificare tutti i valori oppure impostare valori predefiniti per i numeri relativi alla revisione e alla build // usando l'asterisco '*' come illustrato di seguito: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("9.6.0.3")] -[assembly: AssemblyFileVersion("9.6.0.3")] \ No newline at end of file +[assembly: AssemblyVersion("9.6.0")] +[assembly: AssemblyFileVersion("9.6.0")] \ No newline at end of file From f9bb3adbb297cfa9fb2adb1e9a589136fead475b Mon Sep 17 00:00:00 2001 From: MedveMarci Date: Fri, 28 Aug 2026 12:28:12 +0200 Subject: [PATCH 42/47] Fixed an xml doc --- .../API/Features/Behaviour/SpawnBehaviour.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UncomplicatedCustomRoles/API/Features/Behaviour/SpawnBehaviour.cs b/UncomplicatedCustomRoles/API/Features/Behaviour/SpawnBehaviour.cs index 37f8476..ca6343d 100644 --- a/UncomplicatedCustomRoles/API/Features/Behaviour/SpawnBehaviour.cs +++ b/UncomplicatedCustomRoles/API/Features/Behaviour/SpawnBehaviour.cs @@ -29,7 +29,7 @@ public class SpawnBehaviour public int MaxPlayers { get; set; } = 10; /// - /// Gets or sets the minimum number of players that are required by the given to spawn + /// Gets or sets the minimum number of players on the server that are required by the given to spawn /// public int MinPlayers { get; set; } = 1; From ba9ec9e8e68498c93ff68ca35611cf811220556c Mon Sep 17 00:00:00 2001 From: MedveMarci Date: Fri, 28 Aug 2026 12:31:19 +0200 Subject: [PATCH 43/47] Cache what a CustomModule reads from its args, and reject empty required ones --- .../Features/CustomModules/CustomModule.cs | 129 +++++++++++++++--- .../Manager/RoleValidator.cs | 4 +- 2 files changed, 111 insertions(+), 22 deletions(-) diff --git a/UncomplicatedCustomRoles/API/Features/CustomModules/CustomModule.cs b/UncomplicatedCustomRoles/API/Features/CustomModules/CustomModule.cs index 5519624..869372a 100644 --- a/UncomplicatedCustomRoles/API/Features/CustomModules/CustomModule.cs +++ b/UncomplicatedCustomRoles/API/Features/CustomModules/CustomModule.cs @@ -50,10 +50,16 @@ public Dictionary StringArgs { get { - Dictionary result = new(StringComparer.OrdinalIgnoreCase); - foreach (var kvp in Args) - result[kvp.Key] = kvp.Value?.ToString(); - return result; + if (_stringArgs is not null) + return _stringArgs; + + _stringArgs = new Dictionary(StringComparer.OrdinalIgnoreCase); + + if (Args is not null) + foreach (var kvp in Args) + _stringArgs[kvp.Key] = kvp.Value?.ToString(); + + return _stringArgs; } } @@ -69,12 +75,45 @@ public Dictionary StringArgs ///
public Player Player => CustomRole.Player; + private readonly Dictionary _castedValues = []; + + private readonly Dictionary _castedLists = []; + + private readonly HashSet _unconvertibleValues = []; + + private Dictionary _stringArgs; + internal void Initialize(SummonedCustomRole summonedCustomRole, Dictionary args) { CustomRole = summonedCustomRole; Args = args is null ? new Dictionary(StringComparer.OrdinalIgnoreCase) : new Dictionary(args, StringComparer.OrdinalIgnoreCase); + + InvalidateArgsCache(); + } + + public void InvalidateArgsCache() + { + _stringArgs = null; + _castedValues.Clear(); + _castedLists.Clear(); + _unconvertibleValues.Clear(); + } + + public List GetMissingArgs() + { + List missing = []; + + if (RequiredArgs is null) + return missing; + + foreach (var arg in RequiredArgs) + if (Args is null || !Args.TryGetValue(arg, out var value) || value is null || + (value is string text && string.IsNullOrWhiteSpace(text))) + missing.Add(arg); + + return missing; } /// @@ -129,7 +168,7 @@ public virtual void Execute() /// public object TryGetValue(string param, object def = null) { - return Args.TryGetValue(param, out var value) ? value : def; + return Args is not null && Args.TryGetValue(param, out var value) ? value : def; } /// @@ -153,15 +192,23 @@ public string TryGetStringValue(string param, string def = null) /// public T TryGetCastedValue(string param, T def = default) { - if (!Args.TryGetValue(param, out var value)) + ArgKey key = new(param, typeof(T)); + + if (_castedValues.TryGetValue(key, out var cached)) + return (T)cached; + + if (_unconvertibleValues.Contains(key) || Args is null || !Args.TryGetValue(param, out var value)) return def; try { - return (T)Convert.ChangeType(value, typeof(T)); + var converted = (T)Convert.ChangeType(value, typeof(T)); + _castedValues[key] = converted; + return converted; } catch { + _unconvertibleValues.Add(key); return def; } } @@ -175,7 +222,20 @@ public T TryGetCastedValue(string param, T def = default) /// public List TryGetCastedListValue(string param) { - if (!Args.TryGetValue(param, out var value) || value is null) + ArgKey key = new(param, typeof(T)); + + if (_castedLists.TryGetValue(key, out var cached)) + return (List)cached; + + var list = BuildCastedList(param); + _castedLists[key] = list; + + return list; + } + + private List BuildCastedList(string param) + { + if (Args is null || !Args.TryGetValue(param, out var value) || value is null) return []; switch (value) { @@ -268,8 +328,7 @@ internal static List Load(List modules, SummonedCustomRole List mods = []; foreach (var module in data) - if (InitializeCustomModule(module.Key, module.Value, YamlFlagsHandler.Modules, summonedCustomRole) is - CustomModule mod) + if (InitializeCustomModule(module.Key, module.Value, YamlFlagsHandler.Modules, summonedCustomRole) is { } mod) mods.Add(mod); LogManager.Debug( @@ -345,17 +404,15 @@ internal static List Load(List modules, SummonedCustomRole private static bool ValidateModule(CustomModule module, string name, SummonedCustomRole role) { - if (module.RequiredArgs is { Count: > 0 }) + var missing = module.GetMissingArgs(); + + if (missing.Count > 0) { - List missing = module.RequiredArgs.Where(arg => !module.Args.ContainsKey(arg)).ToList(); - if (missing.Count > 0) - { - LogManager.Error( - $"[CM Loader] CustomModule '{name}' on role {RoleLabel(role)} is missing required setting(s): {string.Join(", ", missing)}.\n" + - $"Provided setting(s): {(module.Args.Count == 0 ? "(none)" : string.Join(", ", module.Args.Keys))}.\n" + - "This flag will be skipped.", "CM0004"); - return false; - } + LogManager.Error( + $"[CM Loader] CustomModule '{name}' on role {RoleLabel(role)} is missing required setting(s): {string.Join(", ", missing)}.\n" + + $"Provided setting(s): {(module.Args.Count == 0 ? "(none)" : string.Join(", ", module.Args.Keys))}.\n" + + "This flag will be skipped.", "CM0004"); + return false; } if (!module.Validate(out var error)) @@ -373,4 +430,36 @@ private static string RoleLabel(SummonedCustomRole role) { return role?.Role is null ? "?" : $"{role.Role.Name} ({role.Role.Id})"; } + + private readonly struct ArgKey : IEquatable + { + private readonly string _param; + + private readonly Type _type; + + internal ArgKey(string param, Type type) + { + _param = param; + _type = type; + } + + public bool Equals(ArgKey other) + { + return _type == other._type && string.Equals(_param, other._param, StringComparison.OrdinalIgnoreCase); + } + + public override bool Equals(object? obj) + { + return obj is ArgKey other && Equals(other); + } + + public override int GetHashCode() + { + unchecked + { + return ((_param is null ? 0 : StringComparer.OrdinalIgnoreCase.GetHashCode(_param)) * 397) ^ + (_type?.GetHashCode() ?? 0); + } + } + } } \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Manager/RoleValidator.cs b/UncomplicatedCustomRoles/Manager/RoleValidator.cs index f42ef6c..fde634f 100644 --- a/UncomplicatedCustomRoles/Manager/RoleValidator.cs +++ b/UncomplicatedCustomRoles/Manager/RoleValidator.cs @@ -533,8 +533,8 @@ private static void ValidateCustomFlags(ICustomRole role, string label) module.Initialize(null, flag.Value); - var missing = module.RequiredArgs?.Where(arg => !module.Args.ContainsKey(arg)).ToList(); - if (missing is { Count: > 0 }) + var missing = module.GetMissingArgs(); + if (missing.Count > 0) { LogManager.Warn( $"[Role Validator] {label}: custom flag '{type.Name}' is missing required setting(s): {string.Join(", ", missing)}; it will be skipped on spawn."); From 5ec8fa62ed45682c43a478ff57b617f7c88a2656 Mon Sep 17 00:00:00 2001 From: MedveMarci Date: Fri, 28 Aug 2026 12:33:25 +0200 Subject: [PATCH 44/47] Run every UCS cloud call through UnityWebRequest --- .../API/Features/Controllers/Presence.cs | 37 +++- .../API/Features/Messages/PresenceMessage.cs | 2 +- .../API/Features/Messages/ShareLogMessage.cs | 12 +- UncomplicatedCustomRoles/Commands/LogShare.cs | 72 ++++--- UncomplicatedCustomRoles/Commands/Owner.cs | 21 +- UncomplicatedCustomRoles/Commands/Version.cs | 13 +- .../Extensions/StringExtension.cs | 18 +- .../Manager/LogManager.cs | 32 +-- .../Manager/NET/HttpManager.cs | 204 +++++++----------- .../Manager/NET/HttpResponse.cs | 37 ++++ .../Manager/NET/WebQuery.cs | 103 +++++++++ .../Manager/VersionManager.cs | 40 +++- UncomplicatedCustomRoles/Plugin.cs | 13 +- .../UncomplicatedCustomRoles.csproj | 2 +- 14 files changed, 393 insertions(+), 213 deletions(-) create mode 100644 UncomplicatedCustomRoles/Manager/NET/HttpResponse.cs create mode 100644 UncomplicatedCustomRoles/Manager/NET/WebQuery.cs diff --git a/UncomplicatedCustomRoles/API/Features/Controllers/Presence.cs b/UncomplicatedCustomRoles/API/Features/Controllers/Presence.cs index 3aa6f6a..1ec89e3 100644 --- a/UncomplicatedCustomRoles/API/Features/Controllers/Presence.cs +++ b/UncomplicatedCustomRoles/API/Features/Controllers/Presence.cs @@ -4,26 +4,43 @@ using MEC; using UncomplicatedCustomRoles.API.Features.Messages; using UncomplicatedCustomRoles.Manager; +using UncomplicatedCustomRoles.Manager.NET; namespace UncomplicatedCustomRoles.API.Features.Controllers; internal static class Presence { + private const string Endpoint = "https://api.ucserver.it/v3/plugin/ucr/presence"; + internal static IEnumerator PresenceCoroutine() { while (true) { - try - { - HttpQuery.Post("https://api.ucserver.it/v3/plugin/ucr/presence", - JsonSerializer.Serialize(new PresenceMessage()), "application/json"); - } - catch (Exception e) - { - LogManager.Error($"Failed to send presence data: {e.Message}"); - } + var payload = BuildPayload(); + + if (payload is not null) + yield return Timing.WaitUntilDone(WebQuery.Post(Endpoint, payload, "application/json", OnAnswer)); yield return Timing.WaitForSeconds(60f); } } -} \ No newline at end of file + + private static string BuildPayload() + { + try + { + return JsonSerializer.Serialize(new PresenceMessage()); + } + catch (Exception e) + { + LogManager.Error($"Failed to build the presence data: {e.Message}"); + return null; + } + } + + private static void OnAnswer(HttpResponse response) + { + if (!response.IsSuccess) + LogManager.Debug($"Failed to send the presence data: {response.Reason}"); + } +} diff --git a/UncomplicatedCustomRoles/API/Features/Messages/PresenceMessage.cs b/UncomplicatedCustomRoles/API/Features/Messages/PresenceMessage.cs index 73f84f6..01ebbd2 100644 --- a/UncomplicatedCustomRoles/API/Features/Messages/PresenceMessage.cs +++ b/UncomplicatedCustomRoles/API/Features/Messages/PresenceMessage.cs @@ -19,5 +19,5 @@ internal class PresenceMessage [JsonPropertyName("plugin")] public string PluginName => "UCR"; - [JsonPropertyName("version")] public string Version { get; set; } = Plugin.Instance.Version.ToString(4); + [JsonPropertyName("version")] public string Version { get; set; } = Plugin.Instance.Version.ToString(); } \ No newline at end of file diff --git a/UncomplicatedCustomRoles/API/Features/Messages/ShareLogMessage.cs b/UncomplicatedCustomRoles/API/Features/Messages/ShareLogMessage.cs index b732d83..45c976d 100644 --- a/UncomplicatedCustomRoles/API/Features/Messages/ShareLogMessage.cs +++ b/UncomplicatedCustomRoles/API/Features/Messages/ShareLogMessage.cs @@ -1,22 +1,18 @@ using System.Text.Json.Serialization; using LabApi.Features; +using UncomplicatedCustomRoles.Extensions; using UncomplicatedCustomRoles.Manager; namespace UncomplicatedCustomRoles.API.Features.Messages; -internal class ShareLogMessage +internal class ShareLogMessage(string message) { - public ShareLogMessage(string message) - { - Message = message; - } - [JsonPropertyName("labapi_version")] public string LabAPIVersion { get; set; } = LabApiProperties.CompiledVersion; [JsonPropertyName("plugin_version")] - public string PluginVersion { get; set; } = Plugin.Instance.Version.ToString(4); + public string PluginVersion { get; set; } = Plugin.Instance.Version.ToString(); [JsonPropertyName("hash")] public string Hash { get; set; } = VersionManager.HashFile(Plugin.Instance.FilePath); - [JsonPropertyName("message")] public string Message { get; set; } + [JsonPropertyName("message")] public string Message { get; set; } = message; } \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Commands/LogShare.cs b/UncomplicatedCustomRoles/Commands/LogShare.cs index 3a0890d..0f392b6 100644 --- a/UncomplicatedCustomRoles/Commands/LogShare.cs +++ b/UncomplicatedCustomRoles/Commands/LogShare.cs @@ -12,9 +12,10 @@ using System.Collections.Generic; using System.Net; using System.Text.Json; -using System.Threading.Tasks; using CommandSystem; +using MEC; using UncomplicatedCustomRoles.Manager; +using UncomplicatedCustomRoles.Manager.NET; namespace UncomplicatedCustomRoles.Commands; @@ -48,39 +49,44 @@ protected override bool ExecuteParent(ArraySegment arguments, ICommandSe response = "Loading the JSON content to share with the developers..."; var online = arguments.Count < 1; - Task.Run(() => - { - var Response = LogManager.SendReport(out var content, online); - try - { - if (!online) - LogManager.Info("Logs saved to file successfully."); - - if (Response is HttpStatusCode.OK) - { - if (string.IsNullOrEmpty(content)) - { - LogManager.Error("Server returned OK but the response body was empty."); - return; - } - - LogManager.Debug($"Received content: {content}"); - var Data = JsonSerializer.Deserialize>(content); - LogManager.Info( - $"Successfully shared the UCR logs with the developers!\nSend this Id to the developers: {Data["id"].GetString()}\n\nTook {DateTimeOffset.Now.ToUnixTimeMilliseconds() - Start}ms"); - } - else - { - LogManager.Info($"Failed to share the UCR logs with the developers: Server says: {Response}"); - } - } - catch (Exception e) - { - LogManager.Error(e.ToString()); - } - }); + Timing.RunCoroutine( + LogManager.SendReport(online, (status, content) => OnReportSent(status, content, online, Start)), + "UCR_Http"); return true; } -} \ No newline at end of file + + private static void OnReportSent(HttpStatusCode status, string content, bool online, long start) + { + if (!online) + { + LogManager.Info("Logs saved to file successfully."); + return; + } + + if (status is not HttpStatusCode.OK) + { + LogManager.Info($"Failed to share the UCR logs with the developers: Server says: {status}"); + return; + } + + if (string.IsNullOrEmpty(content)) + { + LogManager.Error("Server returned OK but the response body was empty."); + return; + } + + try + { + LogManager.Debug($"Received content: {content}"); + var data = JsonSerializer.Deserialize>(content); + LogManager.Info( + $"Successfully shared the UCR logs with the developers!\nSend this Id to the developers: {data["id"].GetString()}\n\nTook {DateTimeOffset.Now.ToUnixTimeMilliseconds() - start}ms"); + } + catch (Exception e) + { + LogManager.Error(e.ToString()); + } + } +} diff --git a/UncomplicatedCustomRoles/Commands/Owner.cs b/UncomplicatedCustomRoles/Commands/Owner.cs index 2b0a6cd..a5b330e 100644 --- a/UncomplicatedCustomRoles/Commands/Owner.cs +++ b/UncomplicatedCustomRoles/Commands/Owner.cs @@ -9,10 +9,12 @@ */ using System.Collections.Generic; +using System.Net; using CommandSystem; using LabApi.Features.Wrappers; using UncomplicatedCustomRoles.API.Interfaces; using UncomplicatedCustomRoles.Extensions; +using UncomplicatedCustomRoles.Manager.NET; namespace UncomplicatedCustomRoles.Commands; @@ -38,9 +40,22 @@ public bool Executor(List arguments, ICommandSender sender, out string r return false; } - var code = Plugin.HttpManager.AddServerOwner(player, arguments[0]).GetStatusCode(out response); + HttpManager.AddServerOwner(player, arguments[0], answer => Answer(sender, answer)); - response = $"{code} - {response}"; + response = "Asking our central server to give you the 'Server Owner' role..."; return true; } -} \ No newline at end of file + + private static void Answer(ICommandSender sender, HttpResponse answer) + { + if (!answer.Completed) + { + sender.Respond($"Failed to reach the UCS Central Server: {answer.Reason}", false); + return; + } + + var code = answer.Body.GetStatusCode(out var message); + + sender.Respond($"{code} - {message}", code is HttpStatusCode.OK); + } +} diff --git a/UncomplicatedCustomRoles/Commands/Version.cs b/UncomplicatedCustomRoles/Commands/Version.cs index 9c91401..cc5f09e 100644 --- a/UncomplicatedCustomRoles/Commands/Version.cs +++ b/UncomplicatedCustomRoles/Commands/Version.cs @@ -28,7 +28,7 @@ public bool Executor(List arguments, ICommandSender sender, out string r if (VersionManager.VersionInfo is null) { response = - $"UncomplicatedCustomRoles\nAuthors: {Plugin.Instance.Author}\nVersion: {Plugin.Instance.Version}\n\nThe UCS cloud has no informations about this version, so it can't be verified.\nThis is expected on an unreleased build, otherwise check the server console for the reason."; + $"UncomplicatedCustomRoles\nAuthors: {Plugin.Instance.Author}\nVersion: {Plugin.Instance.Version}\n\nThe UCS cloud has no informations about this version, so it can't be verified.\nThis is expected on an unreleased build, otherwise check the server console for the reason.{UpdateNotice()}"; return false; } @@ -42,6 +42,8 @@ public bool Executor(List arguments, ICommandSender sender, out string r response = $"UncomplicatedCustomRoles\nAuthors: {Plugin.Instance.Author}\nVersion: {VersionManager.VersionInfo.Name}{(VersionManager.VersionInfo.CustomName is not null ? $" '{VersionManager.VersionInfo.CustomName}'" : string.Empty)} ({Plugin.Instance.Version})\nSource: {source}\nPre release: {(VersionManager.VersionInfo.PreRelease != 0 ? "TRUE" : "FALSE")}\nForced debug: {(VersionManager.VersionInfo.ForceDebug != 0 ? "TRUE" : "FALSE")}\nHash: {(!VersionManager.CorrectHash ? "NOT MATCHING!" : "Matching")}"; + response += UpdateNotice(); + if (!VersionManager.CorrectHash) response += "\n\n⚠ WARNING!\nYou are using a NON-OFFICIAL version of the plugin!\nThis version might contain viruses and it's NOT ours!"; @@ -52,4 +54,13 @@ public bool Executor(List arguments, ICommandSender sender, out string r return true; } + + private static string UpdateNotice() + { + if (VersionManager.UpdateTarget is null) + return string.Empty; + + return + $"\n\n⚠ UPDATE AVAILABLE\nv{VersionManager.UpdateTarget} has been released.\n{Plugin.HttpManager.GetDownloadHint(VersionManager.UpdateTarget)}"; + } } \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Extensions/StringExtension.cs b/UncomplicatedCustomRoles/Extensions/StringExtension.cs index de29343..b7cff21 100644 --- a/UncomplicatedCustomRoles/Extensions/StringExtension.cs +++ b/UncomplicatedCustomRoles/Extensions/StringExtension.cs @@ -81,10 +81,24 @@ public static string RemoveBracketsOnEndOfName(this string name) public static HttpStatusCode GetStatusCode(this string str, out string message) { LogManager.Debug($"Parsing JSON for status code: {str}"); - var doc = JsonDocument.Parse(str); - var root = doc.RootElement; message = null; + + JsonDocument doc; + + try + { + doc = JsonDocument.Parse(str); + } + catch (Exception e) + { + LogManager.Debug($"The answer is not a valid JSON ({e.Message}), returning HttpStatusCode.Unused"); + message = str; + return HttpStatusCode.Unused; + } + + var root = doc.RootElement; + if (root.TryGetProperty("message", out var messageElement)) { message = messageElement.GetString(); diff --git a/UncomplicatedCustomRoles/Manager/LogManager.cs b/UncomplicatedCustomRoles/Manager/LogManager.cs index 5ad8915..0d22859 100644 --- a/UncomplicatedCustomRoles/Manager/LogManager.cs +++ b/UncomplicatedCustomRoles/Manager/LogManager.cs @@ -16,8 +16,10 @@ using LabApi.Features.Console; using LabApi.Loader.Features.Paths; using LabApi.Loader.Features.Yaml; +using MEC; using NorthwoodLib.Pools; using UncomplicatedCustomRoles.API.Features; +using UncomplicatedCustomRoles.Extensions; namespace UncomplicatedCustomRoles.Manager; @@ -68,13 +70,14 @@ public static void System(string message) { History.Add(new LogEntry(DateTimeOffset.Now.ToUnixTimeMilliseconds(), "System", message)); } - - internal static HttpStatusCode SendReport(out string content, bool online = true) + + internal static IEnumerator SendReport(bool online, Action callback) { - content = null; - if (History.Count < 1) - return HttpStatusCode.Forbidden; + { + callback?.Invoke(HttpStatusCode.Forbidden, null); + yield break; + } var builder = StringBuilderPool.Shared.Rent(); @@ -87,14 +90,19 @@ internal static HttpStatusCode SendReport(out string content, bool online = true foreach (var Role in CustomRole.CustomRoles.Values) builder.Append($"{YamlConfigParser.Serializer.Serialize(Role)}\n\n---\n\n"); - var response = HttpStatusCode.OK; - if (online) - response = Plugin.HttpManager.ShareLogs(StringBuilderPool.Shared.ToStringReturn(builder), out content); - else + var report = StringBuilderPool.Shared.ToStringReturn(builder); + + if (!online) + { File.WriteAllText( Path.Combine(PathManager.Configs.FullName, $"UCR-Report-{DateTimeOffset.Now.ToUnixTimeSeconds()}.txt"), - StringBuilderPool.Shared.ToStringReturn(builder)); - - return response; + report); + callback?.Invoke(HttpStatusCode.OK, null); + yield break; + } + + yield return Timing.WaitUntilDone(Plugin.HttpManager.ShareLogs(report, + response => callback?.Invoke(response.Completed ? response.Body.GetStatusCode(out _) : response.Status, + response.Body))); } } \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Manager/NET/HttpManager.cs b/UncomplicatedCustomRoles/Manager/NET/HttpManager.cs index 655ce4f..3481410 100644 --- a/UncomplicatedCustomRoles/Manager/NET/HttpManager.cs +++ b/UncomplicatedCustomRoles/Manager/NET/HttpManager.cs @@ -11,10 +11,7 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Net; -using System.Net.Http; using System.Text.Json; -using System.Threading.Tasks; using LabApi.Events.Arguments.PlayerEvents; using LabApi.Events.Handlers; using LabApi.Features.Wrappers; @@ -34,6 +31,10 @@ internal class HttpManager private const string DiscordInvite = "https://discord.gg/5StRGu8EJV"; + private const string CreditsEndpoint = "https://api.ucserver.it/credits.json"; + + private const string OwnersEndpoint = "https://api.ucserver.it/v3/owners"; + /// /// Create a new istance of the HttpManager /// @@ -42,30 +43,14 @@ public HttpManager(string prefix) { Prefix = prefix; RegisterEvents(); - HttpClient = new HttpClient(); - Task.Run(LoadCreditTags); + LoadCreditTags(); } - /// - /// Gets the of the presence coroutine. - /// - public CoroutineHandle PresenceCoroutine { get; internal set; } - - /// - /// Gets if the feature can be activated - missing library - /// - public bool IsAllowed { get; internal set; } = true; - /// /// Gets the prefix of the plugin for our APIs /// public string Prefix { get; } - /// - /// Gets the public istance - /// - public HttpClient HttpClient { get; } - /// /// Gets the UCS APIs endpoint /// @@ -82,70 +67,30 @@ public HttpManager(string prefix) public List IsJobRole { get; } = []; /// - /// Gets every version of the plugin known by the UCS cloud + /// Gets every version of the plugin known by the UCS cloud, empty until is done /// - public List Versions - { - get - { - if (_versions is null) - LoadVersions(); - return _versions; - } - } + public List Versions { get; private set; } = []; /// /// Gets the latest of the plugin, pre-releases included, loaded by the UCS cloud /// - public Version LatestVersion - { - get - { - if (_latestVersion is null) - LoadVersions(); - return _latestVersion; - } - } + public Version LatestVersion { get; private set; } = new(); /// /// Gets the latest stable (non pre-release) of the plugin, loaded by the UCS cloud. /// - public Version LatestStableVersion - { - get - { - if (_latestStableVersion is null) - LoadVersions(); - return _latestStableVersion; - } - } + public Version LatestStableVersion { get; private set; } = new(); /// /// Gets the latest pre-release of the plugin, loaded by the UCS cloud. /// - public Version LatestPreRelease - { - get - { - if (_latestPreRelease is null) - LoadVersions(); - return _latestPreRelease; - } - } + public Version LatestPreRelease { get; private set; } = new(); /// /// Gets whether the running build is a pre-release /// public bool IsPreRelease => IsPreReleaseVersion(Plugin.Instance.Version); - private List _versions { get; set; } - - private Version _latestVersion { get; set; } - - private Version _latestStableVersion { get; set; } - - private Version _latestPreRelease { get; set; } - internal void RegisterEvents() { PlayerEvents.Joined += OnVerified; @@ -160,11 +105,11 @@ public void OnVerified(PlayerJoinedEventArgs ev) { ApplyCreditTag(ev.Player); } - - public string AddServerOwner(Player player, string discordId) + + public static void AddServerOwner(Player player, string discordId, Action callback) { - return HttpQuery.Post("https://api.ucserver.it/v3/owners", - JsonSerializer.Serialize(new OwnerMessage(player, discordId)), "application/json"); + WebQuery.Post(OwnersEndpoint, JsonSerializer.Serialize(new OwnerMessage(player, discordId)), + "application/json", callback); } internal static int CompareReleases(Version left, Version right) @@ -191,74 +136,85 @@ public bool IsPreReleaseVersion(Version version) { return TryGetVersionInfo(version, out var info) ? info.PreRelease != 0 : version.Revision > 0; } + + public CoroutineHandle LoadVersions() + { + return Timing.RunCoroutine(LoadVersionsCoroutine(), "UCR_Http"); + } - public void LoadVersions() + private IEnumerator LoadVersionsCoroutine() { - _versions = []; - _latestVersion = new Version(); - _latestStableVersion = new Version(); - _latestPreRelease = new Version(); + Versions = []; + LatestVersion = new Version(); + LatestStableVersion = new Version(); + LatestPreRelease = new Version(); - string answer = null; + yield return Timing.WaitUntilDone(WebQuery.Get($"{Endpoint}/{Prefix}/versions", LoadVersionList)); + + if (Versions.Count is 0) + yield return Timing.WaitUntilDone(WebQuery.Get($"{Endpoint}/{Prefix}/versions/latest@text/plain", + LoadLatestVersionFallback)); + } + private void LoadVersionList(HttpResponse response) + { try { - answer = HttpQuery.Get($"{Endpoint}/{Prefix}/versions"); - _versions = JsonSerializer.Deserialize>(answer) ?? []; + Versions = JsonSerializer.Deserialize>(response.Body) ?? []; } catch { - LogManager.Debug($"Failed to load the version list from the UCS cloud: '{answer}'"); + LogManager.Debug($"Failed to load the version list from the UCS cloud ({response.Reason}): '{response.Body}'"); + Versions = []; + return; } - foreach (var version in _versions) + foreach (var version in Versions) { if (!Version.TryParse(version.Name, out var parsed)) continue; - if (CompareReleases(parsed, _latestVersion) > 0) - _latestVersion = parsed; + if (CompareReleases(parsed, LatestVersion) > 0) + LatestVersion = parsed; if (version.PreRelease == 0) { - if (CompareReleases(parsed, _latestStableVersion) > 0) - _latestStableVersion = parsed; + if (CompareReleases(parsed, LatestStableVersion) > 0) + LatestStableVersion = parsed; } - else if (CompareReleases(parsed, _latestPreRelease) > 0) + else if (CompareReleases(parsed, LatestPreRelease) > 0) { - _latestPreRelease = parsed; + LatestPreRelease = parsed; } } - - if (_versions.Count is 0) - LoadLatestVersionFallback(); } /// /// Loads the latest version from the single-value endpoint, used when the version list is unavailable. /// - private void LoadLatestVersionFallback() + private void LoadLatestVersionFallback(HttpResponse response) { - string answer = null; + var answer = response.Body; try { - answer = HttpQuery.Get($"{Endpoint}/{Prefix}/versions/latest@text/plain"); - if (string.IsNullOrEmpty(answer) || !answer.Contains(".")) + { + LogManager.Debug($"The UCS cloud gave us no latest version to fall back on ({response.Reason})"); return; + } - _latestVersion = new Version(answer.Trim()); + LatestVersion = new Version(answer.Trim()); - if (_latestVersion.Revision <= 0) - _latestStableVersion = _latestVersion; + if (LatestVersion.Revision <= 0) + LatestStableVersion = LatestVersion; else - _latestPreRelease = _latestVersion; + LatestPreRelease = LatestVersion; } catch { LogManager.Debug($"Failed to parse the latest version received from the UCS cloud: '{answer}'"); - _latestVersion = new Version(); + LatestVersion = new Version(); } } @@ -267,7 +223,8 @@ private void LoadLatestVersionFallback() /// public bool TryGetVersionInfo(Version version, out VersionInfo info) { - info = Versions.FirstOrDefault(v => Version.TryParse(v.Name, out var parsed) && parsed == version); + info = Versions.FirstOrDefault(v => + Version.TryParse(v.Name, out var parsed) && CompareReleases(parsed, version) is 0); return info is not null; } @@ -304,15 +261,20 @@ public string GetDownloadHint(Version version) _ => $"Download it from GitHub: {link ?? (IsPreReleaseVersion(version) ? GitHubReleases : GitHubLatestRelease)}" }; } - + public void LoadCreditTags() { Credits = new Dictionary>(); IsJobRole.Clear(); + + WebQuery.Get(CreditsEndpoint, LoadCreditTagList); + } + + private void LoadCreditTagList(HttpResponse response) + { try { - var Data = JsonSerializer.Deserialize>>( - HttpQuery.Get("https://api.ucserver.it/credits.json")); + var Data = JsonSerializer.Deserialize>>(response.Body); if (Data is null) { @@ -342,7 +304,7 @@ public void LoadCreditTags() { LogManager.Error("An error occurred while loading the credit tags from the UCS Central Server!"); LogManager.Debug( - $"Failed to act HttpManager::LoadCreditTags() - {e.GetType().FullName}: {e.Message}\n{e.StackTrace}"); + $"Failed to act HttpManager::LoadCreditTagList() ({response.Reason}) - {e.GetType().FullName}: {e.Message}\n{e.StackTrace}"); } } @@ -359,7 +321,7 @@ public void ApplyCreditTag(Player player) if (!Plugin.Instance.Config.EnableCreditTags) return; - var Tag = GetCreditTag(player); + var tag = GetCreditTag(player); if (!string.IsNullOrEmpty(player.ReferenceHub.serverRoles.Network_myText)) { @@ -368,37 +330,25 @@ public void ApplyCreditTag(Player player) k.Value.Second == player.ReferenceHub.serverRoles.Network_myColor)) return; - if (!Tag.Third) + if (!tag.Third) return; // Do not override } - if (Tag.First is not null && Tag.Second is not null) + if (tag.First is not null && tag.Second is not null) { - player.ReferenceHub.serverRoles.SetText(Tag.First); - player.ReferenceHub.serverRoles.SetColor(Tag.Second); + player.ReferenceHub.serverRoles.SetText(tag.First); + player.ReferenceHub.serverRoles.SetColor(tag.Second); } } - - public bool IsLatestVersion(out Version latest) - { - latest = ResolveChannelTarget(); - return GetUpdateTarget() is null; - } - - public bool IsLatestVersion() - { - return GetUpdateTarget() is null; - } - - internal HttpStatusCode ShareLogs(string data, out string content) + + internal CoroutineHandle ShareLogs(string data, Action callback) { - content = HttpQuery.Post($"{Endpoint}/{Prefix}/logs", JsonSerializer.Serialize(new ShareLogMessage(data)), - "application/json"); - return content.GetStatusCode(out _); + return WebQuery.Post($"{Endpoint}/{Prefix}/logs", JsonSerializer.Serialize(new ShareLogMessage(data)), + "application/json", callback); } -#nullable enable - internal string VersionInfo() + + internal CoroutineHandle VersionInfo(Action callback) { - return HttpQuery.Get($"{Endpoint}/{Prefix}/versions/{Plugin.Instance.Version.ToString(4)}"); + return WebQuery.Get($"{Endpoint}/{Prefix}/versions/{Plugin.Instance.Version}", callback); } -} \ No newline at end of file +} diff --git a/UncomplicatedCustomRoles/Manager/NET/HttpResponse.cs b/UncomplicatedCustomRoles/Manager/NET/HttpResponse.cs new file mode 100644 index 0000000..e0712d7 --- /dev/null +++ b/UncomplicatedCustomRoles/Manager/NET/HttpResponse.cs @@ -0,0 +1,37 @@ +/* + * This file is a part of the UncomplicatedCustomRoles project. + * + * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) + * + * This file is licensed under the GNU Affero General Public License v3.0. + * You should have received a copy of the AGPL license along with this file. + * If not, see . + */ + +using System.Net; + +namespace UncomplicatedCustomRoles.Manager.NET; + +internal readonly struct HttpResponse +{ + internal HttpResponse(long statusCode, string body, string error) + { + StatusCode = statusCode; + Body = body; + Error = error; + } + + public long StatusCode { get; } + + public string Body { get; } + + public string Error { get; } + + public bool Completed => StatusCode > 0; + + public bool IsSuccess => StatusCode is >= 200 and < 300; + + public HttpStatusCode Status => Completed ? (HttpStatusCode)StatusCode : HttpStatusCode.ServiceUnavailable; + + public string Reason => Error ?? (Completed ? $"HTTP {StatusCode}" : "the server did not answer"); +} diff --git a/UncomplicatedCustomRoles/Manager/NET/WebQuery.cs b/UncomplicatedCustomRoles/Manager/NET/WebQuery.cs new file mode 100644 index 0000000..817b7cb --- /dev/null +++ b/UncomplicatedCustomRoles/Manager/NET/WebQuery.cs @@ -0,0 +1,103 @@ +/* + * This file is a part of the UncomplicatedCustomRoles project. + * + * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) + * + * This file is licensed under the GNU Affero General Public License v3.0. + * You should have received a copy of the AGPL license along with this file. + * If not, see . + */ + +using System; +using System.Collections.Generic; +using System.Text; +using MEC; +using UnityEngine.Networking; + +namespace UncomplicatedCustomRoles.Manager.NET; + +internal static class WebQuery +{ + public static CoroutineHandle Get(string url, Action callback = null) + { + return Timing.RunCoroutine(Send(UnityWebRequest.Get(url), callback), "UCR_Http"); + } + + public static CoroutineHandle Post(string url, string body, string contentType, + Action callback = null) + { + UnityWebRequest request = new(url, UnityWebRequest.kHttpVerbPOST) + { + uploadHandler = new UploadHandlerRaw(Encoding.UTF8.GetBytes(body ?? string.Empty)), + downloadHandler = new DownloadHandlerBuffer() + }; + + request.SetRequestHeader("Content-Type", contentType); + + return Timing.RunCoroutine(Send(request, callback), "UCR_Http"); + } + + private static IEnumerator Send(UnityWebRequest request, Action callback) + { + using (request) + { + request.timeout = 10; + + if (!TrySend(request, out var error)) + { + Answer(callback, new HttpResponse(0, null, error)); + yield break; + } + + while (!request.isDone) + yield return Timing.WaitForOneFrame; + + Answer(callback, Read(request)); + } + } + + private static bool TrySend(UnityWebRequest request, out string error) + { + try + { + request.SendWebRequest(); + error = null; + return true; + } + catch (Exception e) + { + error = e.Message; + LogManager.Debug( + $"Failed to send the {request.method} request to {request.url} - {e.GetType().FullName}: {e.Message}"); + return false; + } + } + + private static HttpResponse Read(UnityWebRequest request) + { + try + { + return new HttpResponse(request.responseCode, request.downloadHandler?.text, + string.IsNullOrEmpty(request.error) ? null : request.error); + } + catch (Exception e) + { + LogManager.Debug( + $"Failed to read the answer of {request.url} - {e.GetType().FullName}: {e.Message}"); + return new HttpResponse(0, null, e.Message); + } + } + + private static void Answer(Action callback, HttpResponse response) + { + try + { + callback?.Invoke(response); + } + catch (Exception e) + { + LogManager.Error("An error occurred while handling the answer of an HTTP request!"); + LogManager.Debug($"Failed to act WebQuery::Answer() - {e.GetType().FullName}: {e.Message}\n{e.StackTrace}"); + } + } +} diff --git a/UncomplicatedCustomRoles/Manager/VersionManager.cs b/UncomplicatedCustomRoles/Manager/VersionManager.cs index 8790643..a0e5859 100644 --- a/UncomplicatedCustomRoles/Manager/VersionManager.cs +++ b/UncomplicatedCustomRoles/Manager/VersionManager.cs @@ -9,6 +9,7 @@ */ using System; +using System.Collections.Generic; using System.IO; using System.Net; using System.Security.Cryptography; @@ -26,15 +27,24 @@ internal static class VersionManager public static bool CorrectHash { get; private set; } #nullable enable - public static void Init() + public static Version? UpdateTarget { get; private set; } + + public static IEnumerator Init() + { + yield return Timing.WaitUntilDone(Plugin.HttpManager.LoadVersions()); + yield return Timing.WaitUntilDone(Plugin.HttpManager.VersionInfo(LoadVersionInfo)); + } + + private static void LoadVersionInfo(HttpResponse response) { try { - var data = Plugin.HttpManager.VersionInfo(); + var data = response.Body; if (string.IsNullOrWhiteSpace(data)) { - LogManager.Silent("The UCS cloud gave us an empty answer while asking for the version info."); + LogManager.Silent( + $"The UCS cloud gave us an empty answer while asking for the version info ({response.Reason})."); return; } @@ -70,7 +80,9 @@ public static void Init() LogManager.Info( $"You are using UncomplicatedCustomRoles v{VersionInfo.Name}{(VersionInfo.CustomName is not null ? $" '{VersionInfo.CustomName}'" : string.Empty)}!"); } - + + CheckForUpdates(); + var hash = HashFile(Plugin.Instance.FilePath); if (hash != VersionInfo.Hash) HashNotMatchMessageSender(hash); @@ -95,6 +107,26 @@ public static void Init() LogManager.Debug(e.ToString()); } } + + public static void CheckForUpdates() + { + try + { + UpdateTarget = Plugin.HttpManager.GetUpdateTarget(); + + if (UpdateTarget is null) + return; + + LogManager.Warn(Plugin.HttpManager.IsPreReleaseVersion(UpdateTarget) + ? $"A newer PRE-RELEASE of UncomplicatedCustomRoles is available!\nCurrent: v{Plugin.Instance.Version} | Latest pre-release: v{UpdateTarget}\n{Plugin.HttpManager.GetDownloadHint(UpdateTarget)}" + : $"You are NOT using the latest version of UncomplicatedCustomRoles!\nCurrent: v{Plugin.Instance.Version} | Latest available: v{UpdateTarget}\n{Plugin.HttpManager.GetDownloadHint(UpdateTarget)}"); + } + catch (Exception e) + { + LogManager.Error("An error occurred while checking for a newer version of the plugin."); + LogManager.Debug(e.ToString()); + } + } public static void HashNotMatchMessageSender(string hash) { diff --git a/UncomplicatedCustomRoles/Plugin.cs b/UncomplicatedCustomRoles/Plugin.cs index dfc1279..b6543de 100644 --- a/UncomplicatedCustomRoles/Plugin.cs +++ b/UncomplicatedCustomRoles/Plugin.cs @@ -11,7 +11,6 @@ using System; using System.Collections.Generic; using System.Reflection; -using System.Threading.Tasks; using HarmonyLib; using LabApi.Features; using LabApi.Features.Wrappers; @@ -72,16 +71,7 @@ public override void Enable() new ScpEventHandler() }); - Task.Run(delegate - { - var updateTarget = HttpManager.GetUpdateTarget(); - if (updateTarget is not null) - LogManager.Warn(HttpManager.IsPreReleaseVersion(updateTarget) - ? $"A newer PRE-RELEASE of UncomplicatedCustomRoles is available!\nCurrent: v{Version} | Latest pre-release: v{updateTarget}\n{HttpManager.GetDownloadHint(updateTarget)}" - : $"You are NOT using the latest version of UncomplicatedCustomRoles!\nCurrent: v{Version} | Latest available: v{updateTarget}\n{HttpManager.GetDownloadHint(updateTarget)}"); - - VersionManager.Init(); - }); + Timing.RunCoroutine(VersionManager.Init(), "UCR_Http"); ImportManager.Unload(); @@ -109,6 +99,7 @@ public override void Enable() public override void Disable() { Timing.KillCoroutines("UCR_Presence"); + Timing.KillCoroutines("UCR_Http"); ScriptedEvents.UnregisterCustomActions(); diff --git a/UncomplicatedCustomRoles/UncomplicatedCustomRoles.csproj b/UncomplicatedCustomRoles/UncomplicatedCustomRoles.csproj index 967b8f1..e3871e0 100644 --- a/UncomplicatedCustomRoles/UncomplicatedCustomRoles.csproj +++ b/UncomplicatedCustomRoles/UncomplicatedCustomRoles.csproj @@ -33,7 +33,6 @@ - @@ -44,6 +43,7 @@ + From 03fdf163e8d78c75bcfe8cb07704884041c640b0 Mon Sep 17 00:00:00 2001 From: MedveMarci Date: Fri, 28 Aug 2026 12:33:50 +0200 Subject: [PATCH 45/47] Using LatestUnitName instead of the first one --- .../Extensions/MirrorExtension.cs | 14 +++++++++----- .../Extensions/RoleExtension.cs | 13 +++++++++++++ UncomplicatedCustomRoles/Patches/SetRolePatch.cs | 11 ++++------- 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/UncomplicatedCustomRoles/Extensions/MirrorExtension.cs b/UncomplicatedCustomRoles/Extensions/MirrorExtension.cs index 9a0ada5..b721817 100644 --- a/UncomplicatedCustomRoles/Extensions/MirrorExtension.cs +++ b/UncomplicatedCustomRoles/Extensions/MirrorExtension.cs @@ -208,9 +208,10 @@ public static void ResetIntercomDisplayText() /// Whether to skip the little jump that works around an invisibility issue. /// /// The UnitNameId to use for the player's new role, if the player's new role uses unit names. (is - /// NTF). + /// NTF). If the latest generated unit name of the role's team will be used. /// - public static void ChangeAppearance(this Player player, RoleTypeId type, bool skipJump = false, byte unitId = 0) + public static void ChangeAppearance(this Player player, RoleTypeId type, bool skipJump = false, + byte? unitId = null) { player.ChangeAppearance(type, Player.ReadyList.Where(x => x != player), skipJump, unitId); } @@ -225,10 +226,10 @@ public static void ChangeAppearance(this Player player, RoleTypeId type, bool sk /// Whether to skip the little jump that works around an invisibility issue. /// /// The UnitNameId to use for the player's new role, if the player's new role uses unit names. (is - /// NTF). + /// NTF). If the latest generated unit name of the role's team will be used. /// public static void ChangeAppearance(this Player player, RoleTypeId type, IEnumerable playersToAffect, - bool skipJump = false, byte unitId = 0) + bool skipJump = false, byte? unitId = null) { if (!player.Connection.isReady || !type.TryGetRoleBase(out var roleBase)) return; @@ -244,7 +245,10 @@ public static void ChangeAppearance(this Player player, RoleTypeId type, IEnumer { if (player.RoleBase is not HumanRole) isRisky = true; - writer.WriteByte(unitId); + + writer.WriteByte(unitId ?? (humanRole.Team.TryGetLatestUnitNameId(out var latestUnitId) + ? latestUnitId + : (byte)0)); } if (roleBase is ZombieRole) diff --git a/UncomplicatedCustomRoles/Extensions/RoleExtension.cs b/UncomplicatedCustomRoles/Extensions/RoleExtension.cs index 0e15115..102584d 100644 --- a/UncomplicatedCustomRoles/Extensions/RoleExtension.cs +++ b/UncomplicatedCustomRoles/Extensions/RoleExtension.cs @@ -11,6 +11,7 @@ using Footprinting; using PlayerRoles; using PlayerRoles.FirstPersonControl; +using Respawning.NamingRules; using UnityEngine; namespace UncomplicatedCustomRoles.Extensions; @@ -52,6 +53,18 @@ public static bool TryGetRoleBase(this RoleTypeId roleType, out T roleBase) w return roleType.TryGetRoleTemplate(out roleBase); } + public static bool TryGetLatestUnitNameId(this Team team, out byte unitNameId) + { + unitNameId = 0; + + if (!NamingRulesManager.TryGetNamingRule(team, out _) || + !NamingRulesManager.GeneratedNames.TryGetValue(team, out var names) || names.Count is 0) + return false; + + unitNameId = (byte)Mathf.Min(names.Count - 1, byte.MaxValue); + return true; + } + public static Vector3 GetRandomSpawnLocation(this RoleTypeId roleType) { if (roleType.TryGetRoleBase(out FpcStandardRoleBase fpcRole) && fpcRole.SpawnpointHandler != null && diff --git a/UncomplicatedCustomRoles/Patches/SetRolePatch.cs b/UncomplicatedCustomRoles/Patches/SetRolePatch.cs index 7618439..7b761a2 100644 --- a/UncomplicatedCustomRoles/Patches/SetRolePatch.cs +++ b/UncomplicatedCustomRoles/Patches/SetRolePatch.cs @@ -12,8 +12,8 @@ using HarmonyLib; using Mirror; using PlayerRoles; -using Respawning.NamingRules; using UncomplicatedCustomRoles.API.Features; +using UncomplicatedCustomRoles.Extensions; namespace UncomplicatedCustomRoles.Patches; @@ -50,11 +50,8 @@ private static void Postfix(PlayerRoleManager __instance, RoleChangeReason reaso if (!UcrSpawnContext.Active || reason is not RoleChangeReason.Respawn) return; - if (__instance.CurrentRole is HumanRole humanRole - && NamingRulesManager.TryGetNamingRule(humanRole.Team, out _) - && NamingRulesManager.GeneratedNames.TryGetValue(humanRole.Team, out var names) - && names.Count > 0 - && humanRole.UnitNameId >= names.Count) - humanRole.UnitNameId = (byte)(names.Count - 1); + if (__instance.CurrentRole is HumanRole { UsesUnitNames: true } humanRole + && humanRole.Team.TryGetLatestUnitNameId(out var unitNameId)) + humanRole.UnitNameId = unitNameId; } } \ No newline at end of file From 1fa91c4a9cc4c5567587bd3ad0485c7ba7ce852b Mon Sep 17 00:00:00 2001 From: MedveMarci Date: Fri, 28 Aug 2026 12:53:48 +0200 Subject: [PATCH 46/47] Cleaned up and formatted the solution --- .../API/Features/Behaviour/SpawnBehaviour.cs | 3 ++- .../API/Features/Controllers/Presence.cs | 2 +- .../API/Features/CustomInfo.cs | 4 +-- .../Features/CustomModules/CustomModule.cs | 25 +++++++++--------- .../API/Features/CustomModules/CustomTeam.cs | 9 ++++--- .../API/Features/CustomModules/InfoTag.cs | 13 +++++----- .../API/Features/CustomRole.cs | 4 +-- .../API/Features/Messages/ShareLogMessage.cs | 4 +-- .../API/Features/SummonedCustomRole.cs | 12 +++++---- UncomplicatedCustomRoles/Commands/LogShare.cs | 3 +-- UncomplicatedCustomRoles/Commands/Owner.cs | 2 +- .../Commands/Percentages.cs | 2 +- .../Commands/SpawnPoint.cs | 2 +- UncomplicatedCustomRoles/Commands/Version.cs | 2 +- .../Events/PlayerEventHandler.cs | 6 ++--- .../Extensions/StringExtension.cs | 7 ++--- .../Manager/DelayedSpawnManager.cs | 6 ++--- .../Manager/FlagMigrator.cs | 9 +++---- .../Manager/InfoColors.cs | 9 ++++--- .../Manager/InventoryLimitOverride.cs | 2 +- .../Manager/LogManager.cs | 2 +- .../Manager/NET/HttpManager.cs | 26 ++++++++++--------- .../Manager/NET/HttpResponse.cs | 16 ++++++------ .../Manager/NET/WebQuery.cs | 4 +-- .../Manager/SpawnPointManager.cs | 13 +++++----- .../Manager/VersionManager.cs | 6 ++--- .../Manager/YamlFlagsHandler.cs | 4 --- .../Patches/CategoryLimitPatch.cs | 2 +- 28 files changed, 100 insertions(+), 99 deletions(-) diff --git a/UncomplicatedCustomRoles/API/Features/Behaviour/SpawnBehaviour.cs b/UncomplicatedCustomRoles/API/Features/Behaviour/SpawnBehaviour.cs index ca6343d..2a27cb1 100644 --- a/UncomplicatedCustomRoles/API/Features/Behaviour/SpawnBehaviour.cs +++ b/UncomplicatedCustomRoles/API/Features/Behaviour/SpawnBehaviour.cs @@ -29,7 +29,8 @@ public class SpawnBehaviour public int MaxPlayers { get; set; } = 10; /// - /// Gets or sets the minimum number of players on the server that are required by the given to spawn + /// Gets or sets the minimum number of players on the server that are required by the given + /// to spawn /// public int MinPlayers { get; set; } = 1; diff --git a/UncomplicatedCustomRoles/API/Features/Controllers/Presence.cs b/UncomplicatedCustomRoles/API/Features/Controllers/Presence.cs index 1ec89e3..a33f770 100644 --- a/UncomplicatedCustomRoles/API/Features/Controllers/Presence.cs +++ b/UncomplicatedCustomRoles/API/Features/Controllers/Presence.cs @@ -43,4 +43,4 @@ private static void OnAnswer(HttpResponse response) if (!response.IsSuccess) LogManager.Debug($"Failed to send the presence data: {response.Reason}"); } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/API/Features/CustomInfo.cs b/UncomplicatedCustomRoles/API/Features/CustomInfo.cs index b679998..8363c7b 100644 --- a/UncomplicatedCustomRoles/API/Features/CustomInfo.cs +++ b/UncomplicatedCustomRoles/API/Features/CustomInfo.cs @@ -23,8 +23,8 @@ namespace UncomplicatedCustomRoles.API.Features; public class CustomInfo { private const string ColorPrefix = ""; - private Player _lastOwner; private bool _detached; + private Player _lastOwner; private bool _nativeNickname = true; private bool _nativeRole = true; private bool _nativeUnit = true; @@ -294,7 +294,7 @@ public void UpdateInfo(Player player) SuppressExternalSync = previousSuppress; } } - + private bool IsNativeRoleName(Player player, SummonedCustomRole summonedCustomRole) { var shownRole = summonedCustomRole is null diff --git a/UncomplicatedCustomRoles/API/Features/CustomModules/CustomModule.cs b/UncomplicatedCustomRoles/API/Features/CustomModules/CustomModule.cs index 869372a..49a8754 100644 --- a/UncomplicatedCustomRoles/API/Features/CustomModules/CustomModule.cs +++ b/UncomplicatedCustomRoles/API/Features/CustomModules/CustomModule.cs @@ -20,6 +20,14 @@ namespace UncomplicatedCustomRoles.API.Features.CustomModules; public abstract class CustomModule { + private readonly Dictionary _castedLists = []; + + private readonly Dictionary _castedValues = []; + + private readonly HashSet _unconvertibleValues = []; + + private Dictionary _stringArgs; + /// /// Gets the display name of the given /// @@ -75,14 +83,6 @@ public Dictionary StringArgs /// public Player Player => CustomRole.Player; - private readonly Dictionary _castedValues = []; - - private readonly Dictionary _castedLists = []; - - private readonly HashSet _unconvertibleValues = []; - - private Dictionary _stringArgs; - internal void Initialize(SummonedCustomRole summonedCustomRole, Dictionary args) { CustomRole = summonedCustomRole; @@ -92,7 +92,7 @@ internal void Initialize(SummonedCustomRole summonedCustomRole, Dictionary GetMissingArgs() { List missing = []; @@ -328,7 +328,8 @@ internal static List Load(List modules, SummonedCustomRole List mods = []; foreach (var module in data) - if (InitializeCustomModule(module.Key, module.Value, YamlFlagsHandler.Modules, summonedCustomRole) is { } mod) + if (InitializeCustomModule(module.Key, module.Value, YamlFlagsHandler.Modules, summonedCustomRole) is + { } mod) mods.Add(mod); LogManager.Debug( @@ -430,7 +431,7 @@ private static string RoleLabel(SummonedCustomRole role) { return role?.Role is null ? "?" : $"{role.Role.Name} ({role.Role.Id})"; } - + private readonly struct ArgKey : IEquatable { private readonly string _param; diff --git a/UncomplicatedCustomRoles/API/Features/CustomModules/CustomTeam.cs b/UncomplicatedCustomRoles/API/Features/CustomModules/CustomTeam.cs index 22d670d..ac2991d 100644 --- a/UncomplicatedCustomRoles/API/Features/CustomModules/CustomTeam.cs +++ b/UncomplicatedCustomRoles/API/Features/CustomModules/CustomTeam.cs @@ -17,17 +17,18 @@ public class CustomTeam : CustomModule { public override List RequiredArgs => ["team"]; internal string Team => TryGetStringValue("team", string.Empty); - + internal bool IsSameTeam(CustomTeam other) { return other is not null && !string.IsNullOrWhiteSpace(Team) && string.Equals(Team, other.Team, StringComparison.OrdinalIgnoreCase); } - + internal static bool SameTeam(ReferenceHub first, ReferenceHub second) { return first is not null && second is not null && first != second && - SummonedCustomRole.TryGet(first, out var firstRole) && firstRole.TryGetModule(out CustomTeam firstTeam) && + SummonedCustomRole.TryGet(first, out var firstRole) && + firstRole.TryGetModule(out CustomTeam firstTeam) && SummonedCustomRole.TryGet(second, out var secondRole) && secondRole.TryGetModule(out CustomTeam secondTeam) && firstTeam.IsSameTeam(secondTeam); @@ -44,4 +45,4 @@ public override bool Validate(out string error) error = null; return true; } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/API/Features/CustomModules/InfoTag.cs b/UncomplicatedCustomRoles/API/Features/CustomModules/InfoTag.cs index 130e3aa..ffcdfad 100644 --- a/UncomplicatedCustomRoles/API/Features/CustomModules/InfoTag.cs +++ b/UncomplicatedCustomRoles/API/Features/CustomModules/InfoTag.cs @@ -8,6 +8,7 @@ * If not, see . */ +using System; using System.Linq; using System.Text.RegularExpressions; using LabApi.Features.Wrappers; @@ -32,9 +33,9 @@ public class InfoTag : CustomModule internal string UnitFormat => TryGetStringValue("unit_format", "({unit})"); internal bool ShowUnitName => TryGetCastedValue("show_unitname", true); - + internal bool ShowBadge => TryGetCastedValue("show_badge", true); - + internal bool ShowPowerStatus => TryGetCastedValue("show_powerstatus", true); private (string Token, string Color, bool Bold)[] Parts => @@ -63,13 +64,13 @@ public override bool Validate(out string error) var tokens = TokenRegex.Matches(Order).Cast().Select(m => m.Groups[1].Value).ToList(); - var unknown = tokens.Where(t => !KnownTokens.Contains(t, System.StringComparer.OrdinalIgnoreCase)).Distinct() + var unknown = tokens.Where(t => !KnownTokens.Contains(t, StringComparer.OrdinalIgnoreCase)).Distinct() .ToList(); if (unknown.Count > 0) LogManager.Warn( $"[CustomModule] InfoTag 'order' contains unknown token(s): {string.Join(", ", unknown.Select(t => $"%{t}%"))}; they will be shown as-is. Valid tokens: %custominfo%, %nickname%, %rolename%, %unitname%."); - if (!tokens.Any(t => KnownTokens.Contains(t, System.StringComparer.OrdinalIgnoreCase))) + if (!tokens.Any(t => KnownTokens.Contains(t, StringComparer.OrdinalIgnoreCase))) { error = "'order' must contain at least one of %custominfo%, %nickname%, %rolename% or %unitname%; otherwise the name tag would show static text only."; @@ -79,7 +80,7 @@ public override bool Validate(out string error) error = null!; return true; } - + internal string Compose(Player player, string customInfoText, string nickname, string roleName, string unitName, bool showUnit) { @@ -123,4 +124,4 @@ public override void OnAdded() Timing.CallDelayed(Timing.WaitForOneFrame, () => { CustomRole.CustomInfo.UpdateInfo(CustomRole.Player); }); base.OnAdded(); } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/API/Features/CustomRole.cs b/UncomplicatedCustomRoles/API/Features/CustomRole.cs index 6168dc9..6bcb55a 100644 --- a/UncomplicatedCustomRoles/API/Features/CustomRole.cs +++ b/UncomplicatedCustomRoles/API/Features/CustomRole.cs @@ -426,11 +426,11 @@ internal static bool Validate(ICustomRole role, out string error) internal static LoadStatusType InternalRegister(ICustomRole customRole) { FlagMigrator.Migrate(customRole); - + if (customRole.RoleAppearance is RoleTypeId.None || customRole.RoleAppearance.GetTeam() is PlayerRoles.Team.Dead) customRole.RoleAppearance = customRole.Role; - + if (Plugin.Instance.Config.EnableValidator) { RoleValidator.Validate(customRole, out var errors, out var warnings); diff --git a/UncomplicatedCustomRoles/API/Features/Messages/ShareLogMessage.cs b/UncomplicatedCustomRoles/API/Features/Messages/ShareLogMessage.cs index 45c976d..32b5d2e 100644 --- a/UncomplicatedCustomRoles/API/Features/Messages/ShareLogMessage.cs +++ b/UncomplicatedCustomRoles/API/Features/Messages/ShareLogMessage.cs @@ -1,6 +1,5 @@ using System.Text.Json.Serialization; using LabApi.Features; -using UncomplicatedCustomRoles.Extensions; using UncomplicatedCustomRoles.Manager; namespace UncomplicatedCustomRoles.API.Features.Messages; @@ -9,8 +8,7 @@ internal class ShareLogMessage(string message) { [JsonPropertyName("labapi_version")] public string LabAPIVersion { get; set; } = LabApiProperties.CompiledVersion; - [JsonPropertyName("plugin_version")] - public string PluginVersion { get; set; } = Plugin.Instance.Version.ToString(); + [JsonPropertyName("plugin_version")] public string PluginVersion { get; set; } = Plugin.Instance.Version.ToString(); [JsonPropertyName("hash")] public string Hash { get; set; } = VersionManager.HashFile(Plugin.Instance.FilePath); diff --git a/UncomplicatedCustomRoles/API/Features/SummonedCustomRole.cs b/UncomplicatedCustomRoles/API/Features/SummonedCustomRole.cs index 7fd1bdf..52000c6 100644 --- a/UncomplicatedCustomRoles/API/Features/SummonedCustomRole.cs +++ b/UncomplicatedCustomRoles/API/Features/SummonedCustomRole.cs @@ -46,7 +46,7 @@ public class SummonedCustomRole private static readonly ConcurrentDictionary _cachedCountByRoleId = new(); internal static int EventTriggeredModuleTotal; - + private readonly int _playerId; private int _eventModuleCount; @@ -325,14 +325,14 @@ public void Destroy() Remove(); Untrack(); } - + internal void DestroyDetached() { LogManager.Silent($"Detaching instance {Id} of CR {Role.Id} because the player is gone"); RemoveInternal(true); Untrack(); } - + private void Untrack() { if (!List.TryRemove(Id, out _)) @@ -358,7 +358,7 @@ public void Remove() { RemoveInternal(false); } - + private void RemoveInternal(bool detached) { try @@ -394,7 +394,9 @@ private void RemoveInternal(bool detached) DisguiseTeam.Remove(_playerId); if (detached) + { InventoryLimitOverride.ClearAll(_playerId); + } else { // Reset ammo limit @@ -815,7 +817,7 @@ public static void TryParseRemoteAdmin(ReferenceHub player, StringBuilder builde builder.AppendLine(Info.BuildInfo(role.Role)); } } - + internal static void ClearAll() { foreach (var role in List.Values.ToArray()) diff --git a/UncomplicatedCustomRoles/Commands/LogShare.cs b/UncomplicatedCustomRoles/Commands/LogShare.cs index 0f392b6..2812e27 100644 --- a/UncomplicatedCustomRoles/Commands/LogShare.cs +++ b/UncomplicatedCustomRoles/Commands/LogShare.cs @@ -15,7 +15,6 @@ using CommandSystem; using MEC; using UncomplicatedCustomRoles.Manager; -using UncomplicatedCustomRoles.Manager.NET; namespace UncomplicatedCustomRoles.Commands; @@ -89,4 +88,4 @@ private static void OnReportSent(HttpStatusCode status, string content, bool onl LogManager.Error(e.ToString()); } } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Commands/Owner.cs b/UncomplicatedCustomRoles/Commands/Owner.cs index a5b330e..a5f3897 100644 --- a/UncomplicatedCustomRoles/Commands/Owner.cs +++ b/UncomplicatedCustomRoles/Commands/Owner.cs @@ -58,4 +58,4 @@ private static void Answer(ICommandSender sender, HttpResponse answer) sender.Respond($"{code} - {message}", code is HttpStatusCode.OK); } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Commands/Percentages.cs b/UncomplicatedCustomRoles/Commands/Percentages.cs index c053fc4..fc8348d 100644 --- a/UncomplicatedCustomRoles/Commands/Percentages.cs +++ b/UncomplicatedCustomRoles/Commands/Percentages.cs @@ -41,7 +41,7 @@ public bool Executor(List args, ICommandSender sender, out string respon continue; var total = customRoles.Sum(r => r.SpawnSettings.SpawnChance); - + var effective = Math.Min(total, 100); response += $"\n\n{(total >= 100 ? "❗" : "✔️")} {role.GetFullName()} ({customRoles.Count})"; diff --git a/UncomplicatedCustomRoles/Commands/SpawnPoint.cs b/UncomplicatedCustomRoles/Commands/SpawnPoint.cs index b941114..047c2e1 100644 --- a/UncomplicatedCustomRoles/Commands/SpawnPoint.cs +++ b/UncomplicatedCustomRoles/Commands/SpawnPoint.cs @@ -117,7 +117,7 @@ public bool Executor(List arguments, ICommandSender sender, out string r spawnPoint.Destroy(); response = SpawnPointManager.Save() ? "SpawnPoint successfully removed!" - : $"SpawnPoint removed!\nThe SpawnPoint list has been updated but it could NOT be saved on the disk: check the server console!"; + : "SpawnPoint removed!\nThe SpawnPoint list has been updated but it could NOT be saved on the disk: check the server console!"; } else { diff --git a/UncomplicatedCustomRoles/Commands/Version.cs b/UncomplicatedCustomRoles/Commands/Version.cs index cc5f09e..384a60a 100644 --- a/UncomplicatedCustomRoles/Commands/Version.cs +++ b/UncomplicatedCustomRoles/Commands/Version.cs @@ -54,7 +54,7 @@ public bool Executor(List arguments, ICommandSender sender, out string r return true; } - + private static string UpdateNotice() { if (VersionManager.UpdateTarget is null) diff --git a/UncomplicatedCustomRoles/Events/PlayerEventHandler.cs b/UncomplicatedCustomRoles/Events/PlayerEventHandler.cs index 0f1c503..abc355a 100644 --- a/UncomplicatedCustomRoles/Events/PlayerEventHandler.cs +++ b/UncomplicatedCustomRoles/Events/PlayerEventHandler.cs @@ -107,7 +107,7 @@ public void OnLeft(PlayerLeftEventArgs ev) return; var playerId = ev.Player.PlayerId; - + if (SummonedCustomRole.TryGet(ev.Player, out var customRole)) { LogManager.Debug( @@ -311,7 +311,7 @@ public void OnHurting(PlayerHurtingEventArgs Hurting) Hurting.IsAllowed = false; return; } - + if (Hurting.Attacker.TryGetSummonedInstance(out var attackerCustomRole)) { if (attackerCustomRole.Role.IsFriendOf is not null && @@ -431,7 +431,7 @@ public void OnEscaping(PlayerEscapingEventArgs Escaping) module.DropItems; RespawnInventoryQueue[Escaping.Player.PlayerId] = new Tuple, Dictionary, bool>( - [..Escaping.Player.Items.Select(i => i.Type)], + [.. Escaping.Player.Items.Select(i => i.Type)], new Dictionary(Escaping.Player.Ammo), dropOldInventory); API.Features.Escape.AddBucket(Escaping.Player); diff --git a/UncomplicatedCustomRoles/Extensions/StringExtension.cs b/UncomplicatedCustomRoles/Extensions/StringExtension.cs index b7cff21..0178580 100644 --- a/UncomplicatedCustomRoles/Extensions/StringExtension.cs +++ b/UncomplicatedCustomRoles/Extensions/StringExtension.cs @@ -33,9 +33,10 @@ public static class StringExtension '8', '9' ]; - - private static readonly Regex CustomInfoRejectedChars = new(@"[\[\]]|[^\p{L}\p{P}\p{Sc}\p{N} ^=+|~`<>\n]", RegexOptions.Compiled); - + + private static readonly Regex CustomInfoRejectedChars = + new(@"[\[\]]|[^\p{L}\p{P}\p{Sc}\p{N} ^=+|~`<>\n]", RegexOptions.Compiled); + public static string SanitizeCustomInfo(this string str) { return string.IsNullOrEmpty(str) ? str : CustomInfoRejectedChars.Replace(str, string.Empty); diff --git a/UncomplicatedCustomRoles/Manager/DelayedSpawnManager.cs b/UncomplicatedCustomRoles/Manager/DelayedSpawnManager.cs index 4b0c89e..751be0f 100644 --- a/UncomplicatedCustomRoles/Manager/DelayedSpawnManager.cs +++ b/UncomplicatedCustomRoles/Manager/DelayedSpawnManager.cs @@ -22,7 +22,7 @@ namespace UncomplicatedCustomRoles.Manager; internal static class DelayedSpawnManager { private static readonly List Scheduled = []; - + internal static void ScheduleAll() { Cancel(); @@ -39,7 +39,7 @@ internal static void ScheduleAll() Scheduled.Add(Timing.CallDelayed(delay, () => Execute(id))); } } - + internal static void Cancel() { foreach (var handle in Scheduled.Where(handle => handle.IsRunning)) @@ -120,4 +120,4 @@ private static bool IsEligible(Player player, ICustomRole role) return SpawnManager.HasRequiredPermission(player, role); } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Manager/FlagMigrator.cs b/UncomplicatedCustomRoles/Manager/FlagMigrator.cs index 86d72ad..a5c7e16 100644 --- a/UncomplicatedCustomRoles/Manager/FlagMigrator.cs +++ b/UncomplicatedCustomRoles/Manager/FlagMigrator.cs @@ -19,9 +19,10 @@ namespace UncomplicatedCustomRoles.Manager; internal static class FlagMigrator { + private const string InfoTagDefaultOrder = "%custominfo%%nickname%%rolename%"; private static readonly Regex RoleNameToken = new("%rolename%", RegexOptions.Compiled | RegexOptions.IgnoreCase); internal static List Migrated { get; } = []; - + internal static void Migrate(ICustomRole role) { if (role.CustomFlags is not { Count: > 0 } flags) @@ -85,7 +86,7 @@ internal static void Migrate(ICustomRole role) infoArgs["show_unitname"] = false; else if (RoleNameToken.IsMatch(infoOrder)) infoOrder = RoleNameToken.Replace(infoOrder, "%rolename% %unitname%"); - + infoArgs["order"] = infoOrder; if (hasColor && !string.IsNullOrEmpty(nickColor)) @@ -107,8 +108,6 @@ internal static void Migrate(ICustomRole role) RenderYaml(infoArgs)); } - private const string InfoTagDefaultOrder = "%custominfo%%nickname%%rolename%"; - private static string DeprecatedList(bool order, bool color, bool noUnit) { List names = []; @@ -151,4 +150,4 @@ private static string RenderYaml(Dictionary infoArgs) return sb.ToString().TrimEnd(); } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Manager/InfoColors.cs b/UncomplicatedCustomRoles/Manager/InfoColors.cs index 61bfc16..5500089 100644 --- a/UncomplicatedCustomRoles/Manager/InfoColors.cs +++ b/UncomplicatedCustomRoles/Manager/InfoColors.cs @@ -44,9 +44,9 @@ internal static class InfoColors { "white", "FFFFFF" }, { "black", "000000" } }; - + internal static IEnumerable Names => NameToHex.Keys; - + internal static bool TryResolve(string? input, out string hex) { hex = string.Empty; @@ -54,7 +54,8 @@ internal static bool TryResolve(string? input, out string hex) if (string.IsNullOrWhiteSpace(input)) return false; - var raw = input!.Trim().TrimStart('#').Replace("_", string.Empty).Replace("-", string.Empty).Replace(" ", string.Empty); + var raw = input!.Trim().TrimStart('#').Replace("_", string.Empty).Replace("-", string.Empty) + .Replace(" ", string.Empty); if (NameToHex.TryGetValue(raw, out var mapped)) { @@ -70,4 +71,4 @@ internal static bool TryResolve(string? input, out string hex) return false; } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Manager/InventoryLimitOverride.cs b/UncomplicatedCustomRoles/Manager/InventoryLimitOverride.cs index 7e7a2eb..124e038 100644 --- a/UncomplicatedCustomRoles/Manager/InventoryLimitOverride.cs +++ b/UncomplicatedCustomRoles/Manager/InventoryLimitOverride.cs @@ -47,4 +47,4 @@ internal static bool TryGet(int playerId, ItemCategory category, out sbyte limit limit = 0; return Categories.TryGetValue(playerId, out var map) && map.TryGetValue(category, out limit); } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Manager/LogManager.cs b/UncomplicatedCustomRoles/Manager/LogManager.cs index 0d22859..d964f5b 100644 --- a/UncomplicatedCustomRoles/Manager/LogManager.cs +++ b/UncomplicatedCustomRoles/Manager/LogManager.cs @@ -70,7 +70,7 @@ public static void System(string message) { History.Add(new LogEntry(DateTimeOffset.Now.ToUnixTimeMilliseconds(), "System", message)); } - + internal static IEnumerator SendReport(bool online, Action callback) { if (History.Count < 1) diff --git a/UncomplicatedCustomRoles/Manager/NET/HttpManager.cs b/UncomplicatedCustomRoles/Manager/NET/HttpManager.cs index 3481410..5947860 100644 --- a/UncomplicatedCustomRoles/Manager/NET/HttpManager.cs +++ b/UncomplicatedCustomRoles/Manager/NET/HttpManager.cs @@ -18,14 +18,14 @@ using MEC; using UncomplicatedCustomRoles.API.Features.Messages; using UncomplicatedCustomRoles.API.Struct; -using UncomplicatedCustomRoles.Extensions; namespace UncomplicatedCustomRoles.Manager.NET; #pragma warning disable IDE1006 internal class HttpManager { - private const string GitHubReleases = "https://github.com/UncomplicatedCustomServer/UncomplicatedCustomRoles/releases"; + private const string GitHubReleases = + "https://github.com/UncomplicatedCustomServer/UncomplicatedCustomRoles/releases"; private const string GitHubLatestRelease = GitHubReleases + "/latest"; @@ -105,7 +105,7 @@ public void OnVerified(PlayerJoinedEventArgs ev) { ApplyCreditTag(ev.Player); } - + public static void AddServerOwner(Player player, string discordId, Action callback) { WebQuery.Post(OwnersEndpoint, JsonSerializer.Serialize(new OwnerMessage(player, discordId)), @@ -131,12 +131,12 @@ internal static int CompareReleases(Version left, Version right) return rightPreRelease is 0 ? -1 : leftPreRelease.CompareTo(rightPreRelease); } - + public bool IsPreReleaseVersion(Version version) { return TryGetVersionInfo(version, out var info) ? info.PreRelease != 0 : version.Revision > 0; } - + public CoroutineHandle LoadVersions() { return Timing.RunCoroutine(LoadVersionsCoroutine(), "UCR_Http"); @@ -164,7 +164,8 @@ private void LoadVersionList(HttpResponse response) } catch { - LogManager.Debug($"Failed to load the version list from the UCS cloud ({response.Reason}): '{response.Body}'"); + LogManager.Debug( + $"Failed to load the version list from the UCS cloud ({response.Reason}): '{response.Body}'"); Versions = []; return; } @@ -227,7 +228,7 @@ public bool TryGetVersionInfo(Version version, out VersionInfo info) Version.TryParse(v.Name, out var parsed) && CompareReleases(parsed, version) is 0); return info is not null; } - + private Version ResolveChannelTarget() { var target = LatestStableVersion; @@ -258,10 +259,11 @@ public string GetDownloadHint(Version version) { "discord" => $"Download it from our Discord server: {link ?? DiscordInvite}", "other" when link is not null => $"Download it from: {link}", - _ => $"Download it from GitHub: {link ?? (IsPreReleaseVersion(version) ? GitHubReleases : GitHubLatestRelease)}" + _ => + $"Download it from GitHub: {link ?? (IsPreReleaseVersion(version) ? GitHubReleases : GitHubLatestRelease)}" }; } - + public void LoadCreditTags() { Credits = new Dictionary>(); @@ -340,15 +342,15 @@ public void ApplyCreditTag(Player player) player.ReferenceHub.serverRoles.SetColor(tag.Second); } } - + internal CoroutineHandle ShareLogs(string data, Action callback) { return WebQuery.Post($"{Endpoint}/{Prefix}/logs", JsonSerializer.Serialize(new ShareLogMessage(data)), "application/json", callback); } - + internal CoroutineHandle VersionInfo(Action callback) { return WebQuery.Get($"{Endpoint}/{Prefix}/versions/{Plugin.Instance.Version}", callback); } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Manager/NET/HttpResponse.cs b/UncomplicatedCustomRoles/Manager/NET/HttpResponse.cs index e0712d7..6a96013 100644 --- a/UncomplicatedCustomRoles/Manager/NET/HttpResponse.cs +++ b/UncomplicatedCustomRoles/Manager/NET/HttpResponse.cs @@ -20,18 +20,18 @@ internal HttpResponse(long statusCode, string body, string error) Body = body; Error = error; } - + public long StatusCode { get; } - + public string Body { get; } - + public string Error { get; } - + public bool Completed => StatusCode > 0; - + public bool IsSuccess => StatusCode is >= 200 and < 300; - + public HttpStatusCode Status => Completed ? (HttpStatusCode)StatusCode : HttpStatusCode.ServiceUnavailable; - + public string Reason => Error ?? (Completed ? $"HTTP {StatusCode}" : "the server did not answer"); -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Manager/NET/WebQuery.cs b/UncomplicatedCustomRoles/Manager/NET/WebQuery.cs index 817b7cb..f42d5d9 100644 --- a/UncomplicatedCustomRoles/Manager/NET/WebQuery.cs +++ b/UncomplicatedCustomRoles/Manager/NET/WebQuery.cs @@ -22,7 +22,7 @@ public static CoroutineHandle Get(string url, Action callback = nu { return Timing.RunCoroutine(Send(UnityWebRequest.Get(url), callback), "UCR_Http"); } - + public static CoroutineHandle Post(string url, string body, string contentType, Action callback = null) { @@ -100,4 +100,4 @@ private static void Answer(Action callback, HttpResponse response) LogManager.Debug($"Failed to act WebQuery::Answer() - {e.GetType().FullName}: {e.Message}\n{e.StackTrace}"); } } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Manager/SpawnPointManager.cs b/UncomplicatedCustomRoles/Manager/SpawnPointManager.cs index 6414112..8911982 100644 --- a/UncomplicatedCustomRoles/Manager/SpawnPointManager.cs +++ b/UncomplicatedCustomRoles/Manager/SpawnPointManager.cs @@ -17,16 +17,15 @@ using LabApi.Loader.Features.Paths; using UncomplicatedCustomRoles.API.Enums; using UncomplicatedCustomRoles.API.Features; -using UncomplicatedCustomRoles.API.Interfaces; namespace UncomplicatedCustomRoles.Manager; internal static class SpawnPointManager { private static readonly JsonSerializerOptions SerializerOptions = new() { WriteIndented = true }; - + public static string FilePath => Path.Combine(PathManager.Configs.FullName, $".{Server.Port}-spawnpoints.json"); - + public static void Init() { if (!File.Exists(FilePath)) @@ -43,7 +42,7 @@ public static void Init() Load(); } - + public static int Load() { SpawnPoint.List.Clear(); @@ -96,7 +95,7 @@ public static int Load() return loaded.Count; } - + public static bool Save() { try @@ -112,7 +111,7 @@ public static bool Save() return false; } } - + private static void CustomRoleSpawnCompatibilityChecker() { foreach (var role in CustomRole.CustomRoles.Values.Where(role => @@ -123,4 +122,4 @@ role.SpawnSettings is not null && role.SpawnSettings.SpawnPoints is not null && LogManager.Warn( $"CustomRole {role.Name} ({role.Id}) has an invalid SpawnPoint '{spawnPoint}' inside its configuration: the selected SpawnPoint does not exist!"); } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Manager/VersionManager.cs b/UncomplicatedCustomRoles/Manager/VersionManager.cs index a0e5859..155db9d 100644 --- a/UncomplicatedCustomRoles/Manager/VersionManager.cs +++ b/UncomplicatedCustomRoles/Manager/VersionManager.cs @@ -80,9 +80,9 @@ private static void LoadVersionInfo(HttpResponse response) LogManager.Info( $"You are using UncomplicatedCustomRoles v{VersionInfo.Name}{(VersionInfo.CustomName is not null ? $" '{VersionInfo.CustomName}'" : string.Empty)}!"); } - + CheckForUpdates(); - + var hash = HashFile(Plugin.Instance.FilePath); if (hash != VersionInfo.Hash) HashNotMatchMessageSender(hash); @@ -107,7 +107,7 @@ private static void LoadVersionInfo(HttpResponse response) LogManager.Debug(e.ToString()); } } - + public static void CheckForUpdates() { try diff --git a/UncomplicatedCustomRoles/Manager/YamlFlagsHandler.cs b/UncomplicatedCustomRoles/Manager/YamlFlagsHandler.cs index 64ea3fd..121fab0 100644 --- a/UncomplicatedCustomRoles/Manager/YamlFlagsHandler.cs +++ b/UncomplicatedCustomRoles/Manager/YamlFlagsHandler.cs @@ -51,7 +51,6 @@ internal static void InvalidateCache() foreach (var flag in flags) if (flag is Dictionary str) - { foreach (var res in str) if (res.Value is Dictionary dict) result.Add(new KeyValuePair?>(res.Key.ToString(), @@ -61,11 +60,8 @@ internal static void InvalidateCache() else LogManager.Warn( $"[CM Loader] The custom flag '{res.Key}' has its settings written as '{res.Value}' instead of a list of 'setting: value' lines, so it can't be read and will be ignored."); - } else - { result.Add(new KeyValuePair?>(flag.ToString(), null)); - } return result; } diff --git a/UncomplicatedCustomRoles/Patches/CategoryLimitPatch.cs b/UncomplicatedCustomRoles/Patches/CategoryLimitPatch.cs index 1fce972..3888e8e 100644 --- a/UncomplicatedCustomRoles/Patches/CategoryLimitPatch.cs +++ b/UncomplicatedCustomRoles/Patches/CategoryLimitPatch.cs @@ -25,7 +25,7 @@ private static void Postfix(ItemCategory category, ReferenceHub player, ref sbyt if (TryGetCustomLimit(player, category, out var limit)) __result = limit; } - + internal static bool TryGetCustomLimit(ReferenceHub player, ItemCategory category, out sbyte limit) { limit = 0; From 83c024e689d1780a98509f12359fc8a913d3377d Mon Sep 17 00:00:00 2001 From: MedveMarci Date: Fri, 28 Aug 2026 14:05:12 +0200 Subject: [PATCH 47/47] Added several null checks --- .../API/Features/CustomInfo.cs | 6 +++--- .../Extensions/RoleExtension.cs | 4 ++-- .../Extensions/StringExtension.cs | 15 +++++++++++++-- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/UncomplicatedCustomRoles/API/Features/CustomInfo.cs b/UncomplicatedCustomRoles/API/Features/CustomInfo.cs index 8363c7b..620a7e1 100644 --- a/UncomplicatedCustomRoles/API/Features/CustomInfo.cs +++ b/UncomplicatedCustomRoles/API/Features/CustomInfo.cs @@ -59,7 +59,7 @@ public string Nickname get; set { - field = value; + field = value ?? string.Empty;; if (_lastOwner is not null) UpdateInfo(_lastOwner); } @@ -70,7 +70,7 @@ public string Role get; set { - field = value; + field = value ?? string.Empty;; if (_lastOwner is not null) UpdateInfo(_lastOwner); } @@ -81,7 +81,7 @@ public string Info get; set { - field = value; + field = value ?? string.Empty;; if (_lastOwner is not null) UpdateInfo(_lastOwner); } diff --git a/UncomplicatedCustomRoles/Extensions/RoleExtension.cs b/UncomplicatedCustomRoles/Extensions/RoleExtension.cs index 102584d..8530599 100644 --- a/UncomplicatedCustomRoles/Extensions/RoleExtension.cs +++ b/UncomplicatedCustomRoles/Extensions/RoleExtension.cs @@ -30,12 +30,12 @@ public static bool CompareLife(this Footprint footprint, ReferenceHub other) public static Color GetColor(this RoleTypeId roleType) { - return roleType is RoleTypeId.None ? Color.white : roleType.GetRoleBase().RoleColor; + return roleType is RoleTypeId.None ? Color.white : roleType.GetRoleBase()?.RoleColor ?? Color.white; } public static string GetFullName(this RoleTypeId typeId) { - return typeId.GetRoleBase().RoleName; + return typeId.GetRoleBase()?.RoleName ?? string.Empty; } public static PlayerRoleBase GetRoleBase(this RoleTypeId roleType) diff --git a/UncomplicatedCustomRoles/Extensions/StringExtension.cs b/UncomplicatedCustomRoles/Extensions/StringExtension.cs index 0178580..3c8662a 100644 --- a/UncomplicatedCustomRoles/Extensions/StringExtension.cs +++ b/UncomplicatedCustomRoles/Extensions/StringExtension.cs @@ -55,8 +55,19 @@ public static string ToInt(this string str, string separator = "") public static string BulkReplace(this string str, Dictionary replace, string matrix = null) { - foreach (var kvp in replace.Where(kvp => kvp.Value is not null)) - str = str.Replace(matrix is null ? kvp.Key : matrix.Replace("", kvp.Key), kvp.Value?.ToString()); + if (string.IsNullOrEmpty(str) || replace is null) + return str ?? string.Empty; + + foreach (var kvp in replace) + { + if (kvp.Value is null || string.IsNullOrEmpty(kvp.Key)) + continue; + + var placeholder = matrix is null ? kvp.Key : matrix.Replace("", kvp.Key); + + if (!string.IsNullOrEmpty(placeholder)) + str = str.Replace(placeholder, kvp.Value.ToString()); + } return str; }