diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..139c6a9d --- /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'] diff --git a/README.md b/README.md index c8034e23..f240a260 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 diff --git a/UncomplicatedCustomRoles/API/Attributes/PluginCustomRole.cs b/UncomplicatedCustomRoles/API/Attributes/PluginCustomRole.cs index e1b993c7..d68208f0 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 01e92d1c..2ad596f9 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 670ea372..31d66ab1 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 44c9759b..851e5ee3 100644 --- a/UncomplicatedCustomRoles/API/Enums/LoadStatusType.cs +++ b/UncomplicatedCustomRoles/API/Enums/LoadStatusType.cs @@ -1,19 +1,19 @@ /* * 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, + Denied +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/API/Enums/SpawnType.cs b/UncomplicatedCustomRoles/API/Enums/SpawnType.cs index e5d5d170..1e0c3620 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/Events/CustomRoleEvents.cs b/UncomplicatedCustomRoles/API/Events/CustomRoleEvents.cs new file mode 100644 index 00000000..3e5863fd --- /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}"); + } + } +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/API/Events/EventArgs.cs b/UncomplicatedCustomRoles/API/Events/EventArgs.cs new file mode 100644 index 00000000..dc71dd71 --- /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; +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/API/Features/Behaviour/AhpBehaviour.cs b/UncomplicatedCustomRoles/API/Features/Behaviour/AhpBehaviour.cs index f2809d3b..ff25bc7d 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 34eb9515..f56e5207 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 d0d8d072..cfb85308 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) - { - 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 555b81b4..2a27cb1d 100644 --- a/UncomplicatedCustomRoles/API/Features/Behaviour/SpawnBehaviour.cs +++ b/UncomplicatedCustomRoles/API/Features/Behaviour/SpawnBehaviour.cs @@ -1,82 +1,77 @@ /* * 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 on the server 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 how many seconds after the round starts the role is spawned. + /// + public float SpawnDelay { get; set; } = 0; - /// - /// 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 726464df..cc77dae9 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 a33c5d88..1707383e 100644 --- a/UncomplicatedCustomRoles/API/Features/Controllers/EscapeController.cs +++ b/UncomplicatedCustomRoles/API/Features/Controllers/EscapeController.cs @@ -8,31 +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 bool _wasInEscapeZone; + + 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; + + if (!_wasInEscapeZone) + PlayerEventHandler.Instance.OnEscaping(new PlayerEscapingEventArgs(_role.Player.ReferenceHub, + _role.Player.Role, RoleTypeId.ChaosConscript, global::Escape.EscapeScenarioType.Custom, + escapeZone)); + + break; + } + + _wasInEscapeZone = inZone; + } + + private void OnDestroy() + { + _role = null; + } + + public void Init(SummonedCustomRole role) { - private SummonedCustomRole _role; - - public void Init(SummonedCustomRole role) - { - _role = role; - } - - private void Update() - { - 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)); - } - - private void OnDestroy() - { - _role = null; - } + _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 6fcbe1bb..a33f7709 100644 --- a/UncomplicatedCustomRoles/API/Features/Controllers/Presence.cs +++ b/UncomplicatedCustomRoles/API/Features/Controllers/Presence.cs @@ -4,25 +4,43 @@ using MEC; using UncomplicatedCustomRoles.API.Features.Messages; using UncomplicatedCustomRoles.Manager; +using UncomplicatedCustomRoles.Manager.NET; -namespace UncomplicatedCustomRoles.API.Features.Controllers +namespace UncomplicatedCustomRoles.API.Features.Controllers; + +internal static class Presence { - internal static class Presence + private const string Endpoint = "https://api.ucserver.it/v3/plugin/ucr/presence"; + + internal static IEnumerator PresenceCoroutine() + { + while (true) + { + var payload = BuildPayload(); + + if (payload is not null) + yield return Timing.WaitUntilDone(WebQuery.Post(Endpoint, payload, "application/json", OnAnswer)); + + yield return Timing.WaitForSeconds(60f); + } + } + + private static string BuildPayload() { - internal static IEnumerator PresenceCoroutine() + try + { + return JsonSerializer.Serialize(new PresenceMessage()); + } + catch (Exception e) { - 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}"); - } - yield return Timing.WaitForSeconds(60f); - } + 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}"); + } +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/API/Features/Controllers/SchematicController.cs b/UncomplicatedCustomRoles/API/Features/Controllers/SchematicController.cs index 429d2f61..be24e2cf 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 aaa59eab..620a7e1f 100644 --- a/UncomplicatedCustomRoles/API/Features/CustomInfo.cs +++ b/UncomplicatedCustomRoles/API/Features/CustomInfo.cs @@ -1,13 +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 System; +using System.Collections.Generic; using LabApi.Features.Wrappers; using PlayerRoles; using Respawning.NamingRules; @@ -16,180 +18,357 @@ using UncomplicatedCustomRoles.Extensions; using UncomplicatedCustomRoles.Manager; -namespace UncomplicatedCustomRoles.API.Features +namespace UncomplicatedCustomRoles.API.Features; + +public class CustomInfo { - public class CustomInfo + private const string ColorPrefix = ""; + private bool _detached; + private Player _lastOwner; + private bool _nativeNickname = true; + private bool _nativeRole = true; + private bool _nativeUnit = true; + + public CustomInfo(string nickname, string role, string info) + { + Nickname = nickname; + Role = role; + Info = info; + } + + public CustomInfo(Player player, string info) { - public string Nickname + Nickname = player.Nickname; + Role = player.Role.GetFullName(); + Info = info; + + UpdateInfo(player); + } + + public CustomInfo(Player player, ICustomRole role) + { + Nickname = player.Nickname; + Role = role.OverrideRoleName ? role.Name : role.Role.GetFullName(); + Info = role.CustomInfo; + + UpdateInfo(player); + } + + public string Nickname + { + get; + set { - get; - set - { - field = value; - if (_lastOwner is not null) - UpdateInfo(_lastOwner); - } + field = value ?? string.Empty;; + if (_lastOwner is not null) + UpdateInfo(_lastOwner); } + } - public string Role + public string Role + { + get; + set { - get; - set - { - field = value; - if (_lastOwner is not null) - UpdateInfo(_lastOwner); - } + field = value ?? string.Empty;; + if (_lastOwner is not null) + UpdateInfo(_lastOwner); } + } - public string Info + public string Info + { + get; + set { - get; - set - { - field = value; - if (_lastOwner is not null) - UpdateInfo(_lastOwner); - } + field = value ?? string.Empty;; + if (_lastOwner is not null) + UpdateInfo(_lastOwner); } + } - private Player _lastOwner; - - internal static bool SuppressExternalSync { get; set; } + internal static bool SuppressExternalSync { get; set; } - public CustomInfo(string nickname, string role, string info) - { - Nickname = nickname; - Role = role; - Info = info; - } + internal void Detach() + { + _detached = true; + _lastOwner = null; + } - public CustomInfo(Player player, string info) - { - Nickname = player.Nickname; - Role = player.Role.GetFullName(); - Info = info; + internal PlayerInfoArea ApplyAreas(PlayerInfoArea value, PlayerInfoArea? original = null) + { + var restore = original ?? value; - UpdateInfo(player); - } + value |= PlayerInfoArea.CustomInfo; - public CustomInfo(Player player, ICustomRole role) - { - Nickname = player.Nickname; - Role = role.OverrideRoleName ? role.Name : role.Role.GetFullName(); - Info = role.CustomInfo; + value = _nativeNickname + ? value | (restore & PlayerInfoArea.Nickname) + : value & ~PlayerInfoArea.Nickname; - UpdateInfo(player); - } + 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) + return; - public void UpdateInfo(Player player) + _lastOwner = player; + + var previousSuppress = SuppressExternalSync; + SuppressExternalSync = true; + try { - _lastOwner = player; + 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; + + _nativeRole = !customLayout && IsNativeRoleName(player, summonedCustomRole); - bool previousSuppress = SuppressExternalSync; - SuppressExternalSync = true; - try + _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; + + if (!NicknameSync.ValidateCustomInfo(Info.SanitizeCustomInfo(), 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 (!_nativeRole && !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}"); + rawCustomInfo = rawCustomInfo.Replace("%rolename%", ""); + rawRole = string.Empty; + } - if (!NicknameSync.ValidateCustomInfo(Info, out string customInfoError) && !string.IsNullOrEmpty(Info)) + if (hasCustomRole) + { + 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 (!_nativeUnit && !hidesUnitName && !string.IsNullOrEmpty(rawRole) && + TryGetUnitName(player, infoTeam, out var ownUnit)) { - 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; + showUnit = true; + rawUnit = ownUnit; } - if (!NicknameSync.ValidateCustomInfo(Role, out string roleNameError) && !string.IsNullOrEmpty(Role)) + if (infoTag is not null) { - 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 (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 (player.TryGetSummonedInstance(out SummonedCustomRole summonedCustomRole)) - { - rawInfo = PlaceholderManager.ApplyPlaceholders(rawInfo, player, summonedCustomRole.Role); + if (customInfoOrderModule is not null) + rawCustomInfo = $"{ColorPrefix}{customInfoOrderModule.Order}"; - if (summonedCustomRole.TryGetModule(out CustomInfoOrder customInfoOrderModule)) - rawCustomInfo = $"{customInfoOrderModule.Order}"; + if (colorfulNickname is not null) + { + LogManager.Debug( + $"Applying ColorfulNickname module to player {player.PlayerId} with color {colorfulNickname.Color} and nickname {Nickname}"); - 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%", ""); + if (showUnit) + rawRole = $"{rawRole} ({rawUnit})"; + } + else + { + 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)) - rawNickname = player.Nickname; - - if (string.IsNullOrEmpty(rawInfo) && string.IsNullOrEmpty(rawRole) && string.IsNullOrEmpty(player.Nickname)) + if (!_nativeNickname && string.IsNullOrEmpty(rawNickname)) + rawNickname = player.Nickname; + + var composed = rawCustomInfo.Replace("%%", "%\n%").BulkReplace(new Dictionary + { { - player.InfoArea |= PlayerInfoArea.Nickname | PlayerInfoArea.Role | PlayerInfoArea.UnitName; - player.CustomInfo = string.Empty; - return; + "custominfo", + rawInfo + }, + { + "nickname", + rawNickname + }, + { + "rolename", + rawRole } + }, "%%"); - player.CustomInfo = rawCustomInfo.Replace("%%", "%\n%").BulkReplace(new() - { - { - "custominfo", - rawInfo - }, - { - "nickname", - rawNickname - }, - { - "rolename", - rawRole - }, - }, "%%"); + if (string.IsNullOrWhiteSpace(composed.Replace(ColorPrefix, string.Empty))) + { + player.CustomInfo = string.Empty; + return; } - finally + + ApplyCustomInfo(player, composed); + } + finally + { + SuppressExternalSync = previousSuppress; + } + } + + 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)) { - SuppressExternalSync = previousSuppress; + unitName = ownUnitName; + return true; } } + + unitName = namingRule.LastGeneratedName; + return !string.IsNullOrEmpty(unitName); + } + + private 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', or a rich text tag the game does not allow."); + composed = string.Empty; + } + + if (string.IsNullOrEmpty(composed)) + { + _nativeNickname = true; + _nativeRole = true; + _nativeUnit = true; + + 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/AmnesiaResistance.cs b/UncomplicatedCustomRoles/API/Features/CustomModules/AmnesiaResistance.cs index dc833faf..83f35561 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 4773d3f6..cf85341d 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 4d29c899..2308af0d 100644 --- a/UncomplicatedCustomRoles/API/Features/CustomModules/ColorfulNickname.cs +++ b/UncomplicatedCustomRoles/API/Features/CustomModules/ColorfulNickname.cs @@ -8,27 +8,45 @@ * If not, see . */ +using System; using System.Collections.Generic; +using System.Linq; using MEC; -namespace UncomplicatedCustomRoles.API.Features.CustomModules +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 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 f063e2ef..4e22288c 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 d249fb4b..9791b147 100644 --- a/UncomplicatedCustomRoles/API/Features/CustomModules/CustomInfoOrder.cs +++ b/UncomplicatedCustomRoles/API/Features/CustomModules/CustomInfoOrder.cs @@ -8,27 +8,50 @@ * 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; + +[Obsolete("This module is deprecated and will be removed in a future version. Use InfoTag instead.")] +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 21872b7c..677725a7 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,47 @@ namespace UncomplicatedCustomRoles.API.Features.CustomModules; public class CustomKeycard : CustomModule { - public override List RequiredArgs => new() + 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 static readonly string ValidKeycardTypes = + string.Join(", ", KeycardTypeAliases.Keys.OrderBy(k => k)); + private KeycardItem _keycardItem; + public override List RequiredArgs => ["KeycardType"]; + + internal ItemType KeycardType + { + get + { + var raw = TryGetStringValue("KeycardType")?.Trim(); + if (string.IsNullOrEmpty(raw)) + return ItemType.None; + + if (KeycardTypeAliases.TryGetValue(raw, out var alias)) + return alias; + + return Enum.TryParse(raw, true, out ItemType parsed) ? parsed : ItemType.None; + } + } - 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 KeycardLevels Permissions => new(ParseEnum("Permissions", DoorPermissionFlags.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 => BuildPermissions(); internal Color KeycardColor => ParseColor("KeycardColor", Color.white); internal Color PermissionsColor => ParseColor("PermissionsColor", Color.white); internal Color LabelColor => ParseColor("LabelColor", Color.white); @@ -40,23 +71,97 @@ public class CustomKeycard : CustomModule internal string SerialLabel => TryGetStringValue("SerialLabel", "000000000000"); internal int RankIndex => TryGetCastedValue("RankIndex", 0); - private const string ValidKeycardTypes = - "KeycardCustomManagement, KeycardCustomMetalCase, KeycardCustomSite02, KeycardCustomTaskForce"; + public override bool Validate(out string error) + { + 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 _)) + { + 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))) + { + error = + $"'{colorParam}' '{TryGetStringValue(colorParam)}' is not a valid hex color. Use a value like #FF0000."; + return false; + } + + if (HasArg("Permissions")) + { + var joined = JoinArg("Permissions"); + if (!string.IsNullOrWhiteSpace(joined) && + !Enum.TryParse(joined.Replace(" ", string.Empty), true, out DoorPermissionFlags _)) + { + 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, () => { - if (KeycardType == ItemType.None) - { - LogManager.Error($"[CustomKeycard] 'KeycardType' is missing or invalid for player {Player?.Nickname}. Valid values: {ValidKeycardTypes}"); + if (Player is null || !Player.IsAlive) return; - } - if (!KeycardType.TryGetTemplate(out var template) || - !template.Customizable) + if (Player.IsInventoryFull) { - LogManager.Error($"[CustomKeycard] '{KeycardType}' is not a customizable keycard type for player {Player?.Nickname}. Valid values: {ValidKeycardTypes}"); + 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; } @@ -66,10 +171,12 @@ public override void OnAdded() 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), @@ -78,21 +185,46 @@ 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}. If the type is a valid customizable keycard this is a bug, please report it."); }); base.OnAdded(); } - private T ParseEnum(string param, T def) where T : struct, Enum + private KeycardLevels BuildPermissions() { - string raw = TryGetStringValue(param); - if (raw is null) - return def; + var hasLevels = HasArg("ContainmentLevel") || HasArg("ArmoryLevel") || HasArg("AdminLevel"); + + KeycardLevels levels = new( + TryGetCastedValue("ContainmentLevel", 0), + TryGetCastedValue("ArmoryLevel", 0), + TryGetCastedValue("AdminLevel", 0)); + + var rawFlags = ParseFlags("Permissions"); + + if (!hasLevels && rawFlags == DoorPermissionFlags.None) + return levels; + + return new KeycardLevels(levels.Permissions | rawFlags); + } - if (!Enum.TryParse(raw, true, out T result)) + private bool HasArg(string param) + { + return Args is not null && Args.ContainsKey(param); + } + + private DoorPermissionFlags ParseFlags(string param) + { + 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 '{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; @@ -100,19 +232,20 @@ private T ParseEnum(string param, T def) where T : struct, Enum 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 df994daa..49a8754d 100644 --- a/UncomplicatedCustomRoles/API/Features/CustomModules/CustomModule.cs +++ b/UncomplicatedCustomRoles/API/Features/CustomModules/CustomModule.cs @@ -1,290 +1,466 @@ /* * 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 + private readonly Dictionary _castedLists = []; + + private readonly Dictionary _castedValues = []; + + private readonly HashSet _unconvertibleValues = []; + + private Dictionary _stringArgs; + + /// + /// 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; - } + 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; } + } + + /// + /// Gets the instance of the in which the current is + /// embedded + /// + public SummonedCustomRole CustomRole { get; private set; } - /// - /// 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) + /// + /// 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); + + 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; + } + + /// + /// 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) + { + 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 is not null && 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) + { + 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 { - CustomRole = summonedCustomRole; - Args = args; + var converted = (T)Convert.ChangeType(value, typeof(T)); + _castedValues[key] = converted; + return converted; } - - /// - /// 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; - } + _unconvertibleValues.Add(key); + 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) + { + ArgKey key = new(param, typeof(T)); + + if (_castedLists.TryGetValue(key, out var cached)) + return (List)cached; - /// - /// 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) + 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) { - 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..."); + + var data = YamlFlagsHandler.Decode(modules) ?? []; - Dictionary?> data = YamlFlagsHandler.Decode(modules) ?? new(); + List mods = []; - List mods = new(); + foreach (var module in data) + if (InitializeCustomModule(module.Key, module.Value, YamlFlagsHandler.Modules, summonedCustomRole) is + { } mod) + mods.Add(mod); - foreach (KeyValuePair?> 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; } + catch (Exception e) + { + LogManager.Error( + $"[CM Loader] Unexpected error while enabling CustomModule '{name}' on role {RoleLabel(summonedCustomRole)}:\n{e}"); + + return null; + } + } - private static CustomModule? InitializeCustomModule(string name, Dictionary? args, Type[] types, SummonedCustomRole summonedCustomRole) + private static bool ValidateModule(CustomModule module, string name, SummonedCustomRole role) + { + var missing = module.GetMissingArgs(); + + if (missing.Count > 0) { - try - { - LogManager.Silent($"[CM Loader] Initialize loading module '{name}' for {summonedCustomRole}"); + 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; + } - Type type = types.FirstOrDefault(t => t.Name == name); + if (!module.Validate(out var error)) + { + LogManager.Error( + $"[CM Loader] CustomModule '{name}' on role {RoleLabel(role)} has an invalid setting: {error}\n" + + "This flag will be skipped.", "CM0005"); + return false; + } - if (type is null) - { - LogManager.Error($"[CM Loader] Failed to enable CustomModule '{name}'!\nError: ERR_CUSTOM_MODULE_NOT_FOUND", "CM0001"); - return null; - } + return true; + } - 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; - } + private static string RoleLabel(SummonedCustomRole role) + { + return role?.Role is null ? "?" : $"{role.Role.Name} ({role.Role.Id})"; + } - module.Initialize(summonedCustomRole, args ?? new()); - module.OnAdded(); // Invoke added event + private readonly struct ArgKey : IEquatable + { + private readonly string _param; - LogManager.Silent($"[CM Loader] CustomModule '{name}' successfully enabled for {summonedCustomRole}!"); + private readonly Type _type; - return module; - } - catch (Exception e) - { - LogManager.Error(e.ToString()); + internal ArgKey(string param, Type type) + { + _param = param; + _type = type; + } - return null; - } + public bool Equals(ArgKey other) + { + return _type == other._type && string.Equals(_param, other._param, StringComparison.OrdinalIgnoreCase); } -/* internal static List ConvertToList(object items) + public override bool Equals(object? obj) { - switch (items) + return obj is ArgKey other && Equals(other); + } + + public override int GetHashCode() + { + unchecked { - 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() }; + return ((_param is null ? 0 : StringComparer.OrdinalIgnoreCase.GetHashCode(_param)) * 397) ^ + (_type?.GetHashCode() ?? 0); } - }*/ + } } } \ No newline at end of file diff --git a/UncomplicatedCustomRoles/API/Features/CustomModules/CustomPermissions.cs b/UncomplicatedCustomRoles/API/Features/CustomModules/CustomPermissions.cs index 983e1d90..eda4bc7f 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 ee7a73f3..487ffeba 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/CustomTeam.cs b/UncomplicatedCustomRoles/API/Features/CustomModules/CustomTeam.cs new file mode 100644 index 00000000..ac2991d0 --- /dev/null +++ b/UncomplicatedCustomRoles/API/Features/CustomModules/CustomTeam.cs @@ -0,0 +1,48 @@ +/* + * 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; + } +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/API/Features/CustomModules/DamageResistance.cs b/UncomplicatedCustomRoles/API/Features/CustomModules/DamageResistance.cs index 7f115d59..80510b91 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 141653da..b8460e1a 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 0024b46f..a416d836 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 f1069ccb..b9b2e4b8 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 d795f5de..c2733f5d 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 5470dbfd..9e1c8ecb 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/InfoTag.cs b/UncomplicatedCustomRoles/API/Features/CustomModules/InfoTag.cs new file mode 100644 index 00000000..ffcdfada --- /dev/null +++ b/UncomplicatedCustomRoles/API/Features/CustomModules/InfoTag.cs @@ -0,0 +1,127 @@ +/* + * 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.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, 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, 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(); + } +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/API/Features/CustomModules/ItemBan.cs b/UncomplicatedCustomRoles/API/Features/CustomModules/ItemBan.cs index 564bfc30..5190e4d2 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 7e85cf43..6572f54f 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 60f0e93e..dc9d7f42 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 f4997f55..af0dd040 100644 --- a/UncomplicatedCustomRoles/API/Features/CustomModules/NoUnitName.cs +++ b/UncomplicatedCustomRoles/API/Features/CustomModules/NoUnitName.cs @@ -1,15 +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.Features.CustomModules +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 { - 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 6110b9ae..da180f6a 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 ca5a97af..e273c112 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 787ce8e2..774f2dee 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 755143ad..9858483f 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 1a1e65a6..ddbccd6b 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 aaf6a55e..71297ac0 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 18a34f39..34508a95 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 8c91f10b..6bcb55a4 100644 --- a/UncomplicatedCustomRoles/API/Features/CustomRole.cs +++ b/UncomplicatedCustomRoles/API/Features/CustomRole.cs @@ -1,335 +1,456 @@ /* * 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.Concurrent; -using PlayerRoles; using System.Collections.Generic; using System.Linq; 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; 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.

+ /// 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.None; + + /// + /// 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) + { + } + + /// + /// 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})"; + } #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) - { - if (CustomRoles.ContainsKey(id)) + /// + /// Try to get a registered by it's Id. + /// + /// + /// + /// if the operation was successfull. + public static bool TryGet(int id, out ICustomRole customRole) + { + 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 = CustomRoles[id]; + customRole = role; return true; } - customRole = null; - return false; - } + return false; + } - /// - /// 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; + /// + /// 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; + } - 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"; + /// + /// 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; + } - if (role is null) - return false; + /// + /// 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; + } - if (role.SpawnSettings is null) - { - error = $"Role has no spawn_settings"; - return false; - } + /// + /// Gets whether a with the given Id is registered + /// + /// + /// + public static bool IsRegistered(int id) + { + return CustomRoles.ContainsKey(id); + } - 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; - } + /// + /// 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); + } - return true; - } + /// + /// Register a new instance. + /// + /// + public static LoadStatusType Register(ICustomRole customRole) + { + return CompatibilityManager.RegisterCustomRole(customRole); + } + + /// + /// Unregister a registered . + /// + /// + /// + /// 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) + { + 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(); + + CustomRoleEvents.OnUnregistered(new 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) + { + return RoleValidator.IsValid(role, out error); + } - internal static LoadStatusType InternalRegister(ICustomRole customRole) + 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) { - if (!Validate(customRole, out string _)) + RoleValidator.Validate(customRole, out var errors, out var warnings); + + 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; - } + var registeringArgs = new CustomRoleRegisteringEventArgs(customRole); + CustomRoleEvents.OnRegistering(registeringArgs); + if (!registeringArgs.IsAllowed) + return LoadStatusType.Denied; + if (!CustomRoles.TryAdd(customRole.Id, customRole)) return LoadStatusType.SameId; - } + + CustomRoleEvents.OnRegistered(new CustomRoleRegisteredEventArgs(customRole)); + return LoadStatusType.Success; } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/API/Features/CustomRoleEventHandler.cs b/UncomplicatedCustomRoles/API/Features/CustomRoleEventHandler.cs index 76903b19..5d4321c8 100644 --- a/UncomplicatedCustomRoles/API/Features/CustomRoleEventHandler.cs +++ b/UncomplicatedCustomRoles/API/Features/CustomRoleEventHandler.cs @@ -1,95 +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(); + public ICustomRole Role => SummonedInstance.Role; - internal CustomRoleEventHandler(SummonedCustomRole summonedInstance) - { - SummonedInstance = summonedInstance; - LoadListeners(); - } + public List Listeners { get; } = []; + + internal void Unload() + { + _activeListeners -= Listeners.Count; + if (_activeListeners < 0) + _activeListeners = 0; + Listeners.Clear(); + } - private void LoadListeners() + 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) - { - if (playerEvent is ICancellableEvent cancellableEvent && !cancellableEvent.IsAllowed) - return; - - Listener listener = Listeners.FirstOrDefault(l => l.Event == playerEvent.GetType()); - - listener?.Method.Invoke(listener.Instance, new object[] { playerEvent }); } - - internal static void InvokeAll(IPlayerEvent ev) + catch (Exception e) { - foreach (SummonedCustomRole summonedCustomRole in SummonedCustomRole.List.Values) - summonedCustomRole.EventHandler?.InvokeSafely(ev); + LogManager.Error( + $"Failed to act CustomRoleEventHandler::LoadListeners() - {e.GetType().FullName}: {e.Message}\n{e.StackTrace}"); } } - public class Listener + internal void InvokeSafely(IPlayerEvent playerEvent) { - public Type Event { get; } + if (Listeners.Count == 0) + return; - public MethodInfo Method { get; } + if (playerEvent is ICancellableEvent { IsAllowed: false }) + return; + + var eventType = playerEvent.GetType(); + foreach (var listener in Listeners) + if (listener.Event == eventType) + { + listener.Method.Invoke(listener.Instance, [playerEvent]); + return; + } + } - public object Instance { get; } + internal static void InvokeAll(IPlayerEvent ev) + { + if (_activeListeners == 0) + return; - public Listener(Type @event, MethodInfo method, object instance) - { - Event = @event; - Method = method; - Instance = 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 f98b9a97..6b400244 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 61b56e74..b45413c1 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 72cce11a..52bbb8cc 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 76bef164..e5178ef3 100644 --- a/UncomplicatedCustomRoles/API/Features/EventCustomRole.cs +++ b/UncomplicatedCustomRoles/API/Features/EventCustomRole.cs @@ -1,1389 +1,1556 @@ /* * 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 UncomplicatedCustomRoles.API.Features.Behaviour; -using UncomplicatedCustomRoles.API.Interfaces; -using UncomplicatedCustomRoles.Manager; -using UnityEngine; +using LabApi.Events.Arguments.ServerEvents; -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() - { - { - "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; } = 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() - { - { - 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) { } - } -} +public class EventCustomRole : CustomRole +{ + /// + /// 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 790188e9..9e1aecc1 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 2e04f209..e70383da 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 214a5c34..55a207ba 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 eeb21197..01ebbd20 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(); +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/API/Features/Messages/ShareLogMessage.cs b/UncomplicatedCustomRoles/API/Features/Messages/ShareLogMessage.cs index bfad1332..32b5d2e4 100644 --- a/UncomplicatedCustomRoles/API/Features/Messages/ShareLogMessage.cs +++ b/UncomplicatedCustomRoles/API/Features/Messages/ShareLogMessage.cs @@ -1,26 +1,16 @@ -using LabApi.Features; -using System.Text.Json.Serialization; +using System.Text.Json.Serialization; +using LabApi.Features; using UncomplicatedCustomRoles.Manager; -namespace UncomplicatedCustomRoles.API.Features.Messages -{ - internal class ShareLogMessage - { - [JsonPropertyName("labapi_version")] - public string LabAPIVersion { get; set; } = LabApiProperties.CompiledVersion; +namespace UncomplicatedCustomRoles.API.Features.Messages; - [JsonPropertyName("plugin_version")] - public string PluginVersion { get; set; } = Plugin.Instance.Version.ToString(4); +internal class ShareLogMessage(string message) +{ + [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(); - [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; } = message; +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/API/Features/Spawn.cs b/UncomplicatedCustomRoles/API/Features/Spawn.cs index 6ccf5db9..05f82701 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 dcc243f2..c76c7f84 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 stored in the server + /// + public static HashSet List { get; } = []; + + /// + /// Gets the list of every in the server that is not written to the local storage file + /// + 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 stored inside the local SpawnPoint 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 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 stored on the disk. + 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 61de6d54..52000c60 100644 --- a/UncomplicatedCustomRoles/API/Features/SummonedCustomRole.cs +++ b/UncomplicatedCustomRoles/API/Features/SummonedCustomRole.cs @@ -1,24 +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 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.Events; using UncomplicatedCustomRoles.API.Features.Controllers; using UncomplicatedCustomRoles.API.Features.CustomModules; using UncomplicatedCustomRoles.API.Interfaces; @@ -28,307 +29,387 @@ 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 - { - /// - /// 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; } - - 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) +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 readonly int _playerId; + + private int _eventModuleCount; + + internal SummonedCustomRole(Player player, ICustomRole role, Triplet? badge, + List infiniteEffects, PlayerInfoArea playerInfo, CustomInfo customInfo, bool isCustomNickname = false) + { + Id = Guid.NewGuid().ToString(); + Player = player; + _playerId = player.PlayerId; + 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); - - 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 CustomRole customRole) + customRole.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 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 + /// + 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?.Maximum ?? 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 + { + _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 { - LogManager.Error("Failed to evaluate RoleBase for SummonedCustomRole::EvaluateRoleBase() - originalRole is null"); + _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 + { + if (!_internalValid || _roleBase 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 - }; - + + _roleBase.Pooled = false; DisguiseTeam.Set(Player.PlayerId, Role.Team ?? Role.Role.GetTeam(), _roleBase); + }); + } + catch (Exception e) + { + LogManager.Error($"Failed to evaluate RoleBase for SummonedCustomRole::EvaluateRoleBase() - {e}"); + } + } - Timing.CallDelayed(3.25f, delegate - { - if (!_internalValid || _roleBase is null) - return; + /// + /// 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; + } - _roleBase.Pooled = false; - DisguiseTeam.Set(Player.PlayerId, Role.Team ?? Role.Role.GetTeam(), _roleBase); - }); - } - catch (Exception e) - { - LogManager.Error($"Failed to evaluate RoleBase for SummonedCustomRole::EvaluateRoleBase() - {e}"); - } - } + /// + /// 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(); + 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 _)) + return; + + if (_cachedListByPlayerId.TryGetValue(_playerId, out var cached) && cached == this) + _cachedListByPlayerId.TryRemove(_playerId, out _); - /// - /// Runs every custom action in and evaluate their results - /// - /// - private bool EvaluateCustomActions() + if (_cachedCountByRoleId.TryGetValue(Role.Id, out var count) && count > 0) { - bool _result = false; - foreach (Func func in CustomActions) - _result &= func(this); - return _result; + count--; + if (count == 0) + _cachedCountByRoleId.TryRemove(Role.Id, out _); + else + _cachedCountByRoleId[Role.Id] = count; } + } - /// - /// Remove the SummonedCustomRole from the list by destroying it! - /// - 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 { - 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) + foreach (var module in _customModules.ToArray()) { - count--; - if (count == 0) - _cachedCountByRoleId.TryRemove(Role.Id, out _); - else - _cachedCountByRoleId[Role.Id] = count; + module.OnRemoved(); + _customModules.Remove(module); } - } - /// - /// Remove the current CustomRole from the player without destroying the instance - /// - public void Remove() - { - try + if (!detached) { - foreach (CustomModule module in _customModules.ToArray()) - { - 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) + if (Badge is { } 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?.Detach(); + + if (IsCustomNickname) + Player.DisplayName = null!; + + LogManager.Debug("Scale reset to 1, 1, 1"); + Player.Scale = new Vector3(1, 1, 1); + + Player.IsDisarmed = false; + } + + DisguiseTeam.Remove(_playerId); + + if (detached) + { + InventoryLimitOverride.ClearAll(_playerId); + } + else + { + // 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 { @@ -339,372 +420,438 @@ public void Remove() { 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 (IsDefaultCoroutineRole && GenericCoroutine.IsRunning) + Timing.KillCoroutines(GenericCoroutine); - if (IsCustomNickname) - Player.DisplayName = null; + if (NicknameReapplyCoroutine.IsRunning) + Timing.KillCoroutines(NicknameReapplyCoroutine); - if (IsDefaultCoroutineRole && GenericCoroutine.IsRunning) - Timing.KillCoroutines(GenericCoroutine); - + if (!detached) + { // Remove effects Player.DisableAllEffects(); - InfiniteEffects.Clear(); - + if (Appearance != RoleTypeId.None && LabApiExtensions.IsAvailable) LabApiExtensions.RemoveFakeRole(Player); - - if (Role is EventCustomRole eventCustomRole) - eventCustomRole.OnRemoved(this); } - catch (Exception e) - { - LogManager.Error($"Failed to act SummonedCustomRole::Remove() - {e.GetType().FullName}: {e.Message}\n{e.StackTrace}"); - } - - _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 (Player.HumeShield < Role.HumeShield.Maximum && DateTimeOffset.UtcNow.ToUnixTimeSeconds() - LastDamageTime >= Role.HumeShield.RegenerationDelay && !_isRegeneratingHume) - Timing.RunCoroutine(HumeShieldCoroutine()); + InfiniteEffects.Clear(); - yield return Timing.WaitForSeconds(TickDuration); - } + if (Role is CustomRole customRole) + customRole.OnRemoved(this); } - - /// - /// The coroutine to regenerate Hume Shield - /// - /// - public IEnumerator HumeShieldCoroutine() + 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(detached: {detached}) - {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(); + + var wasValid = _internalValid; + _internalValid = false; + + if (wasValid) + CustomRoleEvents.OnRemoved(new CustomRoleRemovedEventArgs(this)); + } + + /// + /// 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); + _customModules.Add(module); + if (module.TriggerOnEvents.Count > 0) + { + _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) { - 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 id is not null && List.TryGetValue(id, out var role) ? role : null; + } + + /// + /// 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)); } + } + + internal static void ClearAll() + { + foreach (var role in List.Values.ToArray()) + role.DestroyDetached(); + + List.Clear(); + _cachedListByPlayerId.Clear(); + _cachedCountByRoleId.Clear(); + EventTriggeredModuleTotal = 0; + } - /// - /// 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 75c14d6a..7954e6b8 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 5504d42b..c386a564 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 85b1faa5..88f497f3 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 49072610..90a701f6 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 ec4990fd..dbeae480 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 fca0cae2..9a374048 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 9a954519..ccb18356 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 a09d70f4..5fc6bf9f 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 c3cac542..fa74d784 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 9bc86958..dad8e5da 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 d525d967..3a7a3739 100644 --- a/UncomplicatedCustomRoles/Commands/Info.cs +++ b/UncomplicatedCustomRoles/Commands/Info.cs @@ -1,95 +1,105 @@ /* * 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 System.Linq; +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.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 ?? [])); + 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.Select(f => f.Key))); } + + 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 4338266b..8b4cc24b 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 f7f54f3f..2812e272 100644 --- a/UncomplicatedCustomRoles/Commands/LogShare.cs +++ b/UncomplicatedCustomRoles/Commands/LogShare.cs @@ -1,78 +1,91 @@ /* * 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 MEC; +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 void LoadGeneratedCommands() + { + } + + protected override bool ExecuteParent(ArraySegment arguments, ICommandSender sender, out string response) + { + if (sender.LogName is not "SERVER CONSOLE") + { + response = "Sorry but this command is reserved to the game console!"; + return false; + } - public override string Description { get; } = "Share the UCR Debug logs with the developers"; + var Start = DateTimeOffset.Now.ToUnixTimeMilliseconds(); + response = "Loading the JSON content to share with the developers..."; - public override void LoadGeneratedCommands() { } + var online = arguments.Count < 1; - protected override bool ExecuteParent(ArraySegment arguments, ICommandSender sender, out string response) + Timing.RunCoroutine( + LogManager.SendReport(online, (status, content) => OnReportSent(status, content, online, Start)), + "UCR_Http"); + + return true; + } + + 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) { - if (sender.LogName is not "SERVER CONSOLE") - { - 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..."; - - bool online = arguments.Count < 1; - Task.Run(() => - { - HttpStatusCode Response = LogManager.SendReport(out string 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}"); - 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"); - } - else - LogManager.Info($"Failed to share the UCR logs with the developers: Server says: {Response}"); - } - catch (Exception e) - { - LogManager.Error(e.ToString()); - } - }); - - - return true; + LogManager.Error(e.ToString()); } } } \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Commands/Owner.cs b/UncomplicatedCustomRoles/Commands/Owner.cs index 7e1bbf97..a5f38972 100644 --- a/UncomplicatedCustomRoles/Commands/Owner.cs +++ b/UncomplicatedCustomRoles/Commands/Owner.cs @@ -1,48 +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 CommandSystem; -using LabApi.Features.Wrappers; 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; -namespace UncomplicatedCustomRoles.Commands +public class Owner : IUCRCommand { - public class Owner : IUCRCommand + public string Name { get; } = "owner"; + + public string Description { get; } = "Get the 'Server Owner' role on our Discord server"; + + public string RequiredPermission { get; } = "ucr.owner"; + + public bool Executor(List arguments, ICommandSender sender, out string response) { - public string Name { get; } = "owner"; + if (arguments.Count != 1) + { + response = "Usage: ucr owner "; + return false; + } + + if (!Player.TryGet(sender, out var player)) + { + response = "This command can only be executed by a player."; + return false; + } - public string Description { get; } = "Get the 'Server Owner' role on our Discord server"; + HttpManager.AddServerOwner(player, arguments[0], answer => Answer(sender, answer)); - public string RequiredPermission { get; } = "ucr.owner"; + response = "Asking our central server to give you the 'Server Owner' role..."; + return true; + } - public bool Executor(List arguments, ICommandSender sender, out string response) + private static void Answer(ICommandSender sender, HttpResponse answer) + { + if (!answer.Completed) { - 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; + 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); } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Commands/Percentages.cs b/UncomplicatedCustomRoles/Commands/Percentages.cs index 15d1b36b..fc8348db 100644 --- a/UncomplicatedCustomRoles/Commands/Percentages.cs +++ b/UncomplicatedCustomRoles/Commands/Percentages.cs @@ -1,55 +1,96 @@ /* * 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) + { + response = "Spawn percentages for each base Role:"; - public bool Executor(List args, ICommandSender sender, out string response) + foreach (RoleTypeId role in Enum.GetValues(typeof(RoleTypeId))) { - bool detailed = args.Any() && args[0] is "details"; - response = "Spawn percentages for each base Role:"; + 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); - foreach (RoleTypeId role in Enum.GetValues(typeof(RoleTypeId))) + 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 += + $"\nThe configured chances add up to {total}%, so this role is always replaced and the chances below only weight which CustomRole wins."; + + foreach (var customRole in customRoles.OrderByDescending(r => r.SpawnSettings.SpawnChance)) { - IEnumerable roles = CustomRole.List.Where(r => r.SpawnSettings?.CanReplaceRoles != null && r.SpawnSettings.CanReplaceRoles.Contains(role)); - if (roles.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}%"; - } + 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)}"; } + } - response += "\nOwO"; // We want to render everything + 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)"; + } - return true; + var manualRoles = CustomRole.List.Where(r => + r.IgnoreSpawnSystem || r.SpawnSettings?.CanReplaceRoles == null || + !r.SpawnSettings.CanReplaceRoles.Any()).ToList(); + if (manualRoles.Any()) + { + response += + $"\n\nℹ️ Roles that never spawn on their own ({manualRoles.Count}) - spawned manually or by another plugin:"; + foreach (var customRole in manualRoles) + response += customRole.IgnoreSpawnSystem + ? $"\n ∟ {customRole} - ignore_spawn_system is enabled" + : $"\n ∟ {customRole} - no can_replace_roles"; } + + 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 b5827060..e6c192e8 100644 --- a/UncomplicatedCustomRoles/Commands/Reload.cs +++ b/UncomplicatedCustomRoles/Commands/Reload.cs @@ -1,55 +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 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(); + FlagMigrator.Migrated.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 (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 480b6b8f..9f4c5624 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 bd8acd75..04deb7f9 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 c3123485..047c2e1a 100644 --- a/UncomplicatedCustomRoles/Commands/SpawnPoint.cs +++ b/UncomplicatedCustomRoles/Commands/SpawnPoint.cs @@ -1,219 +1,168 @@ /* * 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 SpawnPointInstance = UncomplicatedCustomRoles.API.Features.SpawnPoint; -using UncomplicatedCustomRoles.Manager.NET; -using System.Net; -using System.Threading.Tasks; -using UncomplicatedCustomRoles.Manager; using UncomplicatedCustomRoles.API.Interfaces; -using UncomplicatedCustomRoles.Extensions; +using UncomplicatedCustomRoles.Manager; +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 readonly 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") + }, + { + "reload", + new KeyValuePair("", + "Reload the SpawnPoint list from the local file, discarding every unsaved change") + }, + { + "path", + new KeyValuePair("", "Show where the SpawnPoints of this server are stored") + } + }; - 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; - } - - response = null; + case "list": + response = + $"{CommandHeader}Currently registered SpawnPoints ({SpawnPointInstance.List.Count}):\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; + } - 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.TryGet(arguments[1], out _)) - { - response = $"A SpawnPoint with the name '{arguments[1]}' is already registered!"; - 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!"; - break; - case "delete": - if (arguments.Count != 2) - { - response = "Wrong usage!\nucr spawnpoint delete (Name)"; - 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 (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 (!Player.IsAlive) - { - response = "You have to be alive..."; - return false; - } - - if (SpawnPointInstance.TryGet(arguments[1], out SpawnPointInstance spawn)) - { - response = "Teleporting to spawnpoint..."; - spawn.Spawn(Player); - } - 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!"; + if (SpawnPointInstance.TryGet(arguments[1], out _)) + { + response = $"A SpawnPoint with the name '{arguments[1]}' is already registered!"; return false; - } + } + + new SpawnPointInstance(arguments[1], player); + + 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) + { + response = "Wrong usage!\nucr spawnpoint delete (Name)"; + return false; + } + + if (SpawnPointInstance.TryGet(arguments[1], out var spawnPoint)) + { + 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!"; + } + else + { + response = $"SpawnPoint '{arguments[1]}' not found!"; + } + + 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 "reload": + case "sync": + 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!"; + 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 e7146f09..150b575d 100644 --- a/UncomplicatedCustomRoles/Commands/Update.cs +++ b/UncomplicatedCustomRoles/Commands/Update.cs @@ -1,62 +1,95 @@ /* * 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; +using UncomplicatedCustomRoles.Manager; + +namespace UncomplicatedCustomRoles.Commands; -namespace UncomplicatedCustomRoles.Commands +public class Update : IUCRCommand { - public class Update : IUCRCommand + public string Name { get; } = "update"; + + 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"; + + public bool Executor(List arguments, ICommandSender sender, out string response) { - public string Name { get; } = "update"; + response = null; + if (arguments.Count is 0) + { + response = "Usage: ucr update "; + return false; + } + + var updated = 0; + + if (arguments[0].ToLower() is "all") + { + foreach (var role in CustomRole.OutdatedRoles.ToList()) + if (UpdateRole(role)) + updated++; - public string Description { get; } = "Update one or more outdated (but loaded) CustomRole(s)"; + 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); - public string RequiredPermission { get; } = "ucr.update"; + if (outdated is not null && UpdateRole(outdated)) + updated++; + if (migrated is not null && PersistMigrated(migrated)) + updated++; - public bool Executor(List arguments, ICommandSender sender, out string response) + if (outdated is null && migrated is null) + response = $"CustomRole {arguments[0]} is not outdated / doesn't need a config update!"; + } + else { - response = null; - if (arguments.Count is 0) - { - response = "Usage: ucr update "; - return false; - } - - if (arguments[0].ToLower() is "all") - foreach (OutdatedCustomRole role in CustomRole.OutdatedRoles) - UpdateRole(role); - else - { - 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!"; - } - else - response = $"CustomRole {arguments[0]} not found!"; - } - - response ??= "Successfully updated CustomRole(s)!"; - return true; + response = $"CustomRole {arguments[0]} not found!"; } - private static void UpdateRole(OutdatedCustomRole role) => File.WriteAllText(role.Path, YamlConfigParser.Serializer.Serialize(role.CustomRole)); + response ??= updated > 0 + ? $"Successfully updated {updated} CustomRole config file(s)!" + : "Nothing to update."; + return true; + } + + 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/Commands/Version.cs b/UncomplicatedCustomRoles/Commands/Version.cs index cab832b5..384a60a7 100644 --- a/UncomplicatedCustomRoles/Commands/Version.cs +++ b/UncomplicatedCustomRoles/Commands/Version.cs @@ -1,45 +1,66 @@ /* * 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 = + $"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; + } - 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")}"; + var source = string.IsNullOrWhiteSpace(VersionManager.VersionInfo.Source) + ? "unknown" + : VersionManager.VersionInfo.Source; - 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 (!string.IsNullOrWhiteSpace(VersionManager.VersionInfo.SourceLink)) + source += $" - {VersionManager.VersionInfo.SourceLink}"; - 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)}"; + 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")}"; - return true; - } + 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!"; + + 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; + } + + 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/Compatibility/CompatibilityManager.cs b/UncomplicatedCustomRoles/Compatibility/CompatibilityManager.cs index cd4e8790..5e2cc99b 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) + { + // 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; } - 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 = 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 a699a2df..ab313054 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 69f15a56..f7a15938 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 e86b54b7..d5c1dd64 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 032119fc..025f5aca 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 d5ed3448..a3a0e394 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 a438a108..8e9c5e49 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 d8d60b6a..a1d5371c 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 76171cbc..8ddf0f49 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.None; - 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 f0f30c71..fa3b2f7d 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.None; - 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 951bc83d..2944b995 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 c90d3fb0..38188dc6 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.None; - 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 817d2555..241c189c 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,63 @@ 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("Auto load the Custom Role ID from the file, bypassing YAML")] + public bool UseIdFixer { 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("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 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 1bb76216..3475f3fb 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 ea4d41b2..abc355af 100644 --- a/UncomplicatedCustomRoles/Events/PlayerEventHandler.cs +++ b/UncomplicatedCustomRoles/Events/PlayerEventHandler.cs @@ -1,441 +1,505 @@ /* * 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.Left += OnLeft; + 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.Left -= OnLeft; + 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); + + // 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]); + + foreach (var role in SummonedCustomRole.List.Values.Where(role => role.Scale != Vector3.one)) + role.Player.Scale = role.Scale; + } + + public void OnLeft(PlayerLeftEventArgs ev) + { + if (ev.Player is null) + return; - internal override void OnUnregistered() + var playerId = ev.Player.PlayerId; + + if (SummonedCustomRole.TryGet(ev.Player, out var customRole)) { - 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; + LogManager.Debug( + $"Player {ev.Player.Nickname} ({playerId}) left as CustomRole {customRole.Role.Name} ({customRole.Role.Id}), releasing the instance"); + customRole.DestroyDetached(); } - public void OnJoined(PlayerJoinedEventArgs ev) - { - FirstRoundPlayers.Add(ev.Player.PlayerId); + 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); + } - // 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 }); + public void OnUpdatingEffect(PlayerEffectUpdatingEventArgs ev) + { + if (ev.Player is null) + return; - foreach (SummonedCustomRole role in SummonedCustomRole.List.Values.Where(role => role.Scale != Vector3.one)) - role.Player.Scale = role.Scale; - } + if (!ev.IsAllowed) + return; - public void OnUpdatingEffect(PlayerEffectUpdatingEventArgs ev) - { - if (ev.Player is null) - return; + 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; + } + } - 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; - } - } + public void OnGenerator(PlayerActivatingGeneratorEventArgs ev) + { + if (SummonedCustomRole.TryGetCustomTeam(ev.Player.ReferenceHub) == Team.SCPs) + ev.IsAllowed = false; + } - public void OnGenerator(PlayerActivatingGeneratorEventArgs 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 OnUnlockingWarheadButton(PlayerUnlockingWarheadButtonEventArgs ev) - { - if (ev.Player.ReferenceHub.GetTeam() == Team.SCPs) - ev.IsAllowed = false; - } + public void OnDamagingWindow(PlayerDamagingWindowEventArgs ev) + { + if (SummonedCustomRole.TryGetCustomTeam(ev.Player.ReferenceHub) != Team.SCPs) + return; - public void OnDamagingWindow(PlayerDamagingWindowEventArgs ev) - { - if (ev.Player.Team == Team.SCPs && ev.Window.name == UnityEngine.Object.FindAnyObjectByType()?._activatorGlass.name) - ev.IsAllowed = false; - } + if (_recontainer == null) + _recontainer = Object.FindAnyObjectByType(); - public void OnDying(PlayerDyingEventArgs ev) + if (_recontainer != null && ev.Window.name == _recontainer._activatorGlass.name) + ev.IsAllowed = false; + } + + 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) && + !(changeAppearanceOnKill.Forever && changeAppearanceOnKill.AlreadyChanged)) + { + 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; + public void OnRagdollSpawn(PlayerSpawningRagdollEventArgs ev) + { + if (ev.Player is null) + return; - if (!RagdollAppearanceQueue.Contains(ev.Player.PlayerId)) - return; + if (!RagdollAppearanceQueue.Contains(ev.Player.PlayerId)) + return; - ev.IsAllowed = false; - RagdollAppearanceQueue.Remove(ev.Player.PlayerId); + ev.IsAllowed = false; + RagdollAppearanceQueue.Remove(ev.Player.PlayerId); - Ragdoll.SpawnRagdoll(RoleTypeId.Tutorial, ev.RagdollPrefab.Position, ev.RagdollPrefab.Rotation, ev.DamageHandler, ev. Player.Nickname); - } + Ragdoll.SpawnRagdoll(RoleTypeId.Tutorial, ev.RagdollPrefab.Position, ev.RagdollPrefab.Rotation, + ev.DamageHandler, ev.Player.Nickname); + } - public void OnChangingRole(PlayerChangingRoleEventArgs ev) - { - if (ev.Player is null) - return; + 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; - if (!ev.IsAllowed) - return; + // Let's clear for custom types + SpawnManager.ClearCustomTypes(ev.Player); - if (!LabApi.Features.Wrappers.Round.IsRoundStarted) - return; + if (!Round.IsRoundStarted) + return; - if (ev.NewRole is RoleTypeId.Spectator or RoleTypeId.None or RoleTypeId.Filmmaker) - return; + if (ev.NewRole is RoleTypeId.Spectator or RoleTypeId.None or RoleTypeId.Filmmaker) + return; - if (Spawn.Spawning.Contains(ev.Player.PlayerId)) - return; + if (Spawn.Spawning.Contains(ev.Player.PlayerId)) + return; - if (ev.Player.HasCustomRole()) - return; + if (ev.Player.HasCustomRole()) + return; - if (Plugin.Instance.Config.IgnoreNpcs && ev.Player.IsNpc) - return; + if (Plugin.Instance.Config.IgnoreNpcs && ev.Player.IsNpc) + return; - if (Started || !FirstRoundPlayers.Contains(ev.Player.PlayerId)) + if (Started || !FirstRoundPlayers.Contains(ev.Player.PlayerId)) + { + if (Plugin.Instance.Config.AllowOnlyNaturalSpawns && !Spawn.SpawnQueue.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.SpawnQueue.Contains(ev.Player.PlayerId)) - { - Spawn.SpawnQueue.Remove(ev.Player.PlayerId); - } + LogManager.Debug("The player is not in the queue for respawning!"); + return; } - ICustomRole Role = SpawnManager.DoEvaluateSpawnForPlayer(ev.Player, ev.NewRole); + if (Spawn.SpawnQueue.Contains(ev.Player.PlayerId)) Spawn.SpawnQueue.Remove(ev.Player.PlayerId); + } - if (Role is not null) - { - LogManager.Debug($"Summoning player {ev.Player.Nickname} ({ev.Player.PlayerId}) as {Role.Name} ({Role.Id})"); - SpawnManager.SummonCustomSubclass(ev.Player, Role.Id); - ev.IsAllowed = false; - } + var Role = SpawnManager.DoEvaluateSpawnForPlayer(ev.Player, ev.NewRole); - LogManager.Debug($"No CustomRole found for player {ev.Player.Nickname}, allowing natural spawn with {ev.NewRole}"); + if (Role is not null) + { + 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}"); + } + } - public void OnHurting(PlayerHurtingEventArgs Hurting) + 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.IsAllowed) + if (CustomTeam.SameTeam(Hurting.Attacker.ReferenceHub, Hurting.Player.ReferenceHub)) + { + Hurting.IsAllowed = false; 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()) + 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()) + 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 (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; - return; - } - if (!NewRole.Key) + if (Escaping.EscapeScenario is Escape.EscapeScenarioType.None) + Escaping.EscapeScenario = Escape.EscapeScenarioType.Custom; + } + } + 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)) { - // Natural role, let's try to parse it - if (Enum.TryParse(NewRole.Value.ToString(), out RoleTypeId role)) + LogManager.Silent("Role found!"); + + 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..."); + + 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); } - } - 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 d2ed7a8d..bfc16fa3 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 cb3e17c3..89ac242c 100644 --- a/UncomplicatedCustomRoles/Events/ServerEventHandler.cs +++ b/UncomplicatedCustomRoles/Events/ServerEventHandler.cs @@ -1,83 +1,98 @@ 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(); - } + public void OnWaitingForPlayers() + { + Started = false; + Plugin.Instance.OnFinishedLoadingPlugins(); + if (Plugin.Instance.Config.EnableValidator) + MapSpawnValidator.ValidateAll(); + } - 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(); - } - - public void OnRoundEnded(RoundEndedEventArgs _) - { - Started = false; - InfiniteEffect.Terminate(); - } + DelayedSpawnManager.ScheduleAll(); + InfiniteEffect.Stop(); + InfiniteEffect.EffectAssociationAllowed = true; + InfiniteEffect.Start(); + } - public void OnRoundRestarted() - { - Announcer.SavedCustomAnnouncements.Clear(); - } + public void OnRoundEnded(RoundEndedEventArgs _) + { + Started = false; + DelayedSpawnManager.Cancel(); + InfiniteEffect.Terminate(); + } + + 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(); + FirstRoundPlayers.Clear(); + Spawn.SpawnQueue.Clear(); + Spawn.Spawning.Clear(); + API.Features.Escape.Bucket.Clear(); + InventoryLimitOverride.ClearAll(); + } - 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 3f7210df..947ee0fb 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/CustomRoleExtension.cs b/UncomplicatedCustomRoles/Extensions/CustomRoleExtension.cs new file mode 100644 index 00000000..f8fee5e2 --- /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); + } +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Extensions/DictionaryExtension.cs b/UncomplicatedCustomRoles/Extensions/DictionaryExtension.cs index c3ef9a1c..c6442ecd 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 fe0ccf39..417dbbc5 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 dffc63e8..b7218170 100644 --- a/UncomplicatedCustomRoles/Extensions/MirrorExtension.cs +++ b/UncomplicatedCustomRoles/Extensions/MirrorExtension.cs @@ -5,547 +5,603 @@ // // ----------------------------------------------------------------------- -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; + /// + /// Gets a 's . + /// + public static MethodInfo SetDirtyBitsMethodInfo => setDirtyBitsMethodInfoValue ??= + typeof(NetworkBehaviour).GetMethod(nameof(NetworkBehaviour.SetSyncVarDirtyBit)); - bool isRisky = type.GetTeam() is Team.Dead || !player.IsAlive; + /// + /// Gets a NetworkServer.SendSpawnMessage's . + /// + public static MethodInfo SendSpawnMessageMethodInfo => sendSpawnMessageMethodInfoValue ??= + typeof(NetworkServer).GetMethod("SendSpawnMessage", BindingFlags.NonPublic | BindingFlags.Static); - NetworkWriterPooled writer = NetworkWriterPool.Get(); - writer.WriteUShort(38952); - writer.WriteUInt(player.NetworkId); - writer.WriteRoleType(type); + /// + /// 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); + } - if (roleBase is HumanRole humanRole && humanRole.UsesUnitNames) - { - if (player.RoleBase is not HumanRole) - isRisky = true; - writer.WriteByte(unitId); - } + /// + /// 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 ZombieRole) - { - if (player.RoleBase is not ZombieRole) - 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.WriteUShort((ushort)Mathf.Clamp(Mathf.CeilToInt(player.MaxHealth), ushort.MinValue, ushort.MaxValue)); - writer.WriteBool(true); - } + /// + /// Resync . + /// + public static void ResetIntercomDisplayText() + { + ResyncSyncVar(IntercomDisplay._singleton.netIdentity, typeof(IntercomDisplay), + nameof(IntercomDisplay.Network_overrideText)); + } - if (roleBase is Scp1507Role) - { - if (player.RoleBase is not Scp1507Role) - isRisky = true; + /// + /// 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). 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 = null) + { + player.ChangeAppearance(type, Player.ReadyList.Where(x => x != player), skipJump, unitId); + } - writer.WriteByte((byte)player.RoleBase.ServerSpawnReason); - } + /// + /// 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). 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 = null) + { + if (!player.Connection.isReady || !type.TryGetRoleBase(out var roleBase)) + return; - 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); - } + var isRisky = type.GetTeam() is Team.Dead || !player.IsAlive; - 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}"); - } + var writer = NetworkWriterPool.Get(); + writer.WriteUShort(38952); + writer.WriteUInt(player.NetworkId); + writer.WriteRoleType(type); - NetworkWriterPool.Return(writer); + if (roleBase is HumanRole humanRole && humanRole.UsesUnitNames) + { + if (player.RoleBase is not HumanRole) + isRisky = true; - // 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; + writer.WriteByte(unitId ?? (humanRole.Team.TryGetLatestUnitNameId(out var latestUnitId) + ? latestUnitId + : (byte)0)); } - // 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 ZombieRole) { - 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 ZombieRole) + isRisky = true; + + writer.WriteUShort((ushort)Mathf.Clamp(Mathf.CeilToInt(player.MaxHealth), ushort.MinValue, + ushort.MaxValue)); + writer.WriteBool(true); } - /// - /// 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 Scp1507Role) { - StringBuilder announcement = new(); - - string[] cassies = words.Split('\n'); - string[] translations = translation.Split('\n'); + if (player.RoleBase is not Scp1507Role) + isRisky = true; - for (int i = 0; i < cassies.Length; i++) - announcement.Append($"{translations[i].Replace(' ', ' ')} {cassies[i]} "); - - string message = announcement.ToString(); + writer.WriteByte((byte)player.RoleBase.ServerSpawnReason); + } - 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 FpcStandardRoleBase fpc) { - identity.gameObject.transform.position = pos; - 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 _); + writer.WriteRelativePosition(new RelativePosition(player.Position)); + writer.WriteUShort(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, - }; + 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}"); - player.Connection.Send(objectDestroyMessage, 0); - SendSpawnMessageMethodInfo?.Invoke(null, new object[] { identity, player.Connection }); - } + NetworkWriterPool.Return(writer); - /// - /// 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, - }; + // 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); } } + } - /// - /// 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 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(); - foreach (Player ply in Player.ReadyList) - { - ply.Connection.Send(objectDestroyMessage, 0); - SendSpawnMessageMethodInfo?.Invoke(null, new object[] { identity, ply.Connection }); - } - } + string[] cassies = words.Split('\n'); + string[] translations = translation.Split('\n'); - /// - /// 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; + for (int i = 0; i < cassies.Length; i++) + announcement.Append($"{translations[i].Replace(' ', ' ')} {cassies[i]} "); - 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 message = announcement.ToString(); - NetworkWriterPool.Return(writer); - NetworkWriterPool.Return(writer2); - void CustomSyncVarGenerator(NetworkWriter targetWriter) + 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 f498f522..84259519 100644 --- a/UncomplicatedCustomRoles/Extensions/PlayerExtension.cs +++ b/UncomplicatedCustomRoles/Extensions/PlayerExtension.cs @@ -1,263 +1,312 @@ /* * 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; + /// + /// 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; + } - 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); - } + /// + /// 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; + } - /// - /// 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); - } + /// + /// 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; + } - /// - /// 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)); - } + 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; - /// - /// Set a to a . - /// - /// - /// - public static void SetCustomRole(this Player player, ICustomRole role) - { - SpawnManager.ClearCustomTypes(player); - Timing.RunCoroutine(SpawnManager.AsyncPlayerSpawner(player, role.Id)); - } + effect.ForceIntensity(intensity); + effect.ServerChangeDuration(duration, addDuration); + } - /// - /// 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 without a coroutine. + /// + /// + /// + /// The created instance or if the spawn failed. + public static SummonedCustomRole SetCustomRoleSync(this Player player, ICustomRole role) + { + if (role is null) + return 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 = GetSummonedInstance(player); - return summonedInstance != null; - } + SpawnManager.ClearCustomTypes(player); + return SummonedCustomRole.Summon(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; - } + /// + /// Set a (via it's Id) to a without a coroutine. + /// + /// + /// + /// The created instance or if the spawn failed. + public static SummonedCustomRole SetCustomRoleSync(this Player player, int role) + { + return CustomRole.TryGet(role, out var customRole) ? player.SetCustomRoleSync(customRole) : 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(); + /// + /// 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)); + } - if (doResetRole) - { - Vector3 OriginalPosition = player.Position; + /// + /// Set a to a . + /// + /// + /// + public static void SetCustomRole(this Player player, ICustomRole role) + { + SpawnManager.ClearCustomTypes(player); + Timing.RunCoroutine(SpawnManager.AsyncPlayerSpawner(player, role.Id)); + } - player.SetRole(Role, RoleChangeReason.Destroyed, RoleSpawnFlags.AssignInventory); + /// + /// 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); + } - player.Position = OriginalPosition; - } + /// + /// 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; + } - return true; - } + /// + /// 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; + } - return false; - } + /// + /// 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); + } - /// - /// 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) + /// + /// Get the current of a if it has one. + /// + /// + /// + public static SummonedCustomRole GetSummonedInstance(this ReferenceHub player) + { + return 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 var result)) { - _ = new CustomInfo(player, ProcessCustomInfo(customInfo)); - } + var Role = result.Role.Role; + result.Destroy(); - /// - /// Changes in the given string [br] with the UNICODE escape char "\n" - /// - /// - /// - private static string ProcessCustomInfo(string customInfo) => customInfo.Replace("[br]", "\n"); + if (doResetRole) + { + var OriginalPosition = player.Position; - // 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); + player.SetRole(Role, RoleChangeReason.Destroyed, RoleSpawnFlags.AssignInventory); - if (index is -1) - return; + player.Position = OriginalPosition; + } - 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); - }); + return true; } - // 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); + return false; + } - if (index is -1) - return; + /// + /// 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)); + } - 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]); - }); - } + /// + /// Changes in the given string [br] with the UNICODE escape char "\n" + /// + /// + /// + private static string ProcessCustomInfo(string customInfo) + { + return customInfo.Replace("[br]", "\n"); + } - internal static void ResetInventory(this Player player, IEnumerable items) - { - if (items is null) - 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) + { + InventoryLimitOverride.Set(player.PlayerId, category, limit); + SendCategoryLimit(player, category, limit); + } - player.ClearInventory(); - foreach (ItemType item in items) - player.AddItem(item); - } + // 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) + { + InventoryLimitOverride.Clear(player.PlayerId, category); - // 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; + var config = ServerConfigSynchronizer.Singleton; + var index = (int)category; + if (config is null || index < 0 || index >= config.CategoryLimits.Count) + return; - return InventoryLimits.GetAmmoLimit(type, player.ReferenceHub); - } + SendCategoryLimit(player, category, config.CategoryLimits[index]); + } - // 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) + 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 => { - int index = ServerConfigSynchronizer.Singleton.AmmoLimitsSync.FindIndex(x => x.AmmoType == type); - MirrorExtensions.SendFakeSyncObject(player, ServerConfigSynchronizer.Singleton.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) + { + if (items is null) + return; + + 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; + + 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) + { + 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 +314,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 a41238b1..85305997 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 . @@ -11,33 +11,66 @@ using Footprinting; using PlayerRoles; using PlayerRoles.FirstPersonControl; +using Respawning.NamingRules; 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; + return footprint.LifeIdentifier == other.LifeIdentifier; + } - public static bool CompareLife(this Footprint footprint, ReferenceHub other) => footprint.LifeIdentifier == other.roleManager.CurrentRole.UniqueLifeIdentifier; + public static bool CompareLife(this Footprint footprint, ReferenceHub other) + { + return footprint.LifeIdentifier == other.roleManager.CurrentRole.UniqueLifeIdentifier; + } - public static Color GetColor(this RoleTypeId roleType) => roleType is RoleTypeId.None ? Color.white : roleType.GetRoleBase().RoleColor; + public static Color GetColor(this RoleTypeId roleType) + { + return roleType is RoleTypeId.None ? Color.white : roleType.GetRoleBase()?.RoleColor ?? Color.white; + } - public static string GetFullName(this RoleTypeId typeId) => typeId.GetRoleBase().RoleName; + public static string GetFullName(this RoleTypeId typeId) + { + return typeId.GetRoleBase()?.RoleName ?? string.Empty; + } - public static PlayerRoleBase GetRoleBase(this RoleTypeId roleType) => roleType.TryGetRoleBase(out PlayerRoleBase roleBase) ? roleBase : null; + public static PlayerRoleBase GetRoleBase(this RoleTypeId roleType) + { + return roleType.TryGetRoleBase(out var roleBase) ? roleBase : null; + } - public static bool TryGetRoleBase(this RoleTypeId roleType, out PlayerRoleBase roleBase) => PlayerRoleLoader.TryGetRoleTemplate(roleType, out roleBase); + public static bool TryGetRoleBase(this RoleTypeId roleType, out PlayerRoleBase roleBase) + { + return roleType.TryGetRoleTemplate(out roleBase); + } - 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 T roleBase) where T : PlayerRoleBase + { + 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 TryGetLatestUnitNameId(this Team team, out byte unitNameId) + { + unitNameId = 0; - return Vector3.zero; - } + 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 && + 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 0aa515c7..3c8662ac 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 . @@ -13,84 +13,118 @@ using System.Linq; using System.Net; using System.Text.Json; +using System.Text.RegularExpressions; 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' + ]; + + private static readonly Regex CustomInfoRejectedChars = + new(@"[\[\]]|[^\p{L}\p{P}\p{Sc}\p{N} ^=+|~`<>\n]", RegexOptions.Compiled); + + public static string SanitizeCustomInfo(this string str) { - 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(); + return string.IsNullOrEmpty(str) ? str : CustomInfoRejectedChars.Replace(str, string.Empty); + } - foreach (char ch in str) - if (_intChars.Contains(ch)) - result.Add(ch); + public static string ToInt(this string str, string separator = "") + { + List result = []; - return string.Join(separator, result); - } + foreach (var ch in str) + if (INTChars.Contains(ch)) + result.Add(ch); - 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()); + return string.Join(separator, result); + } - return str; - } + public static string BulkReplace(this string str, Dictionary replace, string matrix = null) + { + if (string.IsNullOrEmpty(str) || replace is null) + return str ?? string.Empty; - public static string GenerateWithBuffer(this string str, int bufferSize) + foreach (var kvp in replace) { - for (int a = str.Length; a < bufferSize; a++) - str += " "; + if (kvp.Value is null || string.IsNullOrEmpty(kvp.Key)) + continue; - return str; + var placeholder = matrix is null ? kvp.Key : matrix.Replace("", kvp.Key); + + if (!string.IsNullOrEmpty(placeholder)) + str = str.Replace(placeholder, kvp.Value.ToString()); } - public static string RemoveBracketsOnEndOfName(this string name) - { - var bracketStart = name.IndexOf('('); + return str; + } + + public static string GenerateWithBuffer(this string str, int bufferSize) + { + for (var a = str.Length; a < bufferSize; a++) + str += " "; + + return str; + } + + public static string RemoveBracketsOnEndOfName(this string name) + { + var bracketStart = name.IndexOf('('); + + if (bracketStart > 0) + name = name.Remove(bracketStart, name.Length - bracketStart); + + return name; + } + + public static HttpStatusCode GetStatusCode(this string str, out string message) + { + LogManager.Debug($"Parsing JSON for status code: {str}"); + + message = null; - if (bracketStart > 0) - name = name.Remove(bracketStart, name.Length - bracketStart); + JsonDocument doc; - return name; + try + { + doc = JsonDocument.Parse(str); } - - public static HttpStatusCode GetStatusCode(this string str, out string message) + catch (Exception e) { - 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"); + 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(); + LogManager.Debug($"Extracted message: {message}"); + } + + if (root.TryGetProperty("status", out var 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; } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Extensions/Vector3Extension.cs b/UncomplicatedCustomRoles/Extensions/Vector3Extension.cs index 1f2ef439..8097bd0c 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 ffe6b0be..03313c38 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,156 @@ 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) + } + + internal 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.Contains(pluginName, StringComparison.CurrentCultureIgnoreCase)); + + if (plugin is not null) + return plugin.Value.Value; + + return null; + } + catch (Exception e) + { + LogManager.Error(e.ToString()); + return null; + } + } + + internal static Assembly GetExiledAssembly(string pluginName) + { + try + { + var assembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(p => + p.FullName.Contains(pluginName, StringComparison.CurrentCultureIgnoreCase)); + 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 7f2a6e5f..2b27d0d3 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 a399f20f..7f54460c 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 7f5df1ea..3c28901e 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 52ce9014..0e1af66c 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 17168d37..8051c82d 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 b9667d0e..6959153d 100644 --- a/UncomplicatedCustomRoles/Integrations/UCI.cs +++ b/UncomplicatedCustomRoles/Integrations/UCI.cs @@ -1,67 +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.Linq; using System.Reflection; using LabApi.Features.Wrappers; -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 readonly Assembly Assembly = DynamicInvoke.GetLabAPIAssembly("UncomplicatedCustomItems") ?? + DynamicInvoke.GetExiledAssembly("UncomplicatedCustomItems"); - public static Type SummonedCustomItem = Assembly?.GetType("UncomplicatedCustomItems.API.Features.SummonedCustomItem"); + public static readonly 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 97f8492d..638bf0f1 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/DelayedSpawnManager.cs b/UncomplicatedCustomRoles/Manager/DelayedSpawnManager.cs new file mode 100644 index 00000000..751be0fb --- /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); + } +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Manager/FileConfigs.cs b/UncomplicatedCustomRoles/Manager/FileConfigs.cs index a16802e7..bb1a9a21 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/FlagMigrator.cs b/UncomplicatedCustomRoles/Manager/FlagMigrator.cs new file mode 100644 index 00000000..a5c7e168 --- /dev/null +++ b/UncomplicatedCustomRoles/Manager/FlagMigrator.cs @@ -0,0 +1,153 @@ +/* + * 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 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) + 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 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(); + } +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Manager/ImportManager.cs b/UncomplicatedCustomRoles/Manager/ImportManager.cs index 5e240039..23f3f6e0 100644 --- a/UncomplicatedCustomRoles/Manager/ImportManager.cs +++ b/UncomplicatedCustomRoles/Manager/ImportManager.cs @@ -1,87 +1,91 @@ /* * 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 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; + 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 && (type.IsSubclassOf(typeof(ICustomRole)) || type.IsSubclassOf(typeof(CustomRole)) || type.IsSubclassOf(typeof(EventCustomRole)))) - { - 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/InfoColors.cs b/UncomplicatedCustomRoles/Manager/InfoColors.cs new file mode 100644 index 00000000..55000890 --- /dev/null +++ b/UncomplicatedCustomRoles/Manager/InfoColors.cs @@ -0,0 +1,74 @@ +/* + * 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; + } +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Manager/InventoryLimitOverride.cs b/UncomplicatedCustomRoles/Manager/InventoryLimitOverride.cs new file mode 100644 index 00000000..124e038b --- /dev/null +++ b/UncomplicatedCustomRoles/Manager/InventoryLimitOverride.cs @@ -0,0 +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.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 void ClearAll(int playerId) + { + Categories.TryRemove(playerId, out _); + } + + 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 5fb58702..d964f5b9 100644 --- a/UncomplicatedCustomRoles/Manager/LogManager.cs +++ b/UncomplicatedCustomRoles/Manager/LogManager.cs @@ -1,95 +1,108 @@ /* * 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 MEC; using NorthwoodLib.Pools; using UncomplicatedCustomRoles.API.Features; -using UncomplicatedCustomRoles.API.Interfaces; +using UncomplicatedCustomRoles.Extensions; + +namespace UncomplicatedCustomRoles.Manager; -namespace UncomplicatedCustomRoles.Manager +internal class LogManager { - internal class LogManager - { - // We should store the data here - public static readonly HashSet History = new(); - private static bool DebugEnabled => Plugin.Instance.Config.Debug; + // 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(DateTimeOffset.Now.ToUnixTimeMilliseconds(), nameof(LogLevel.Debug), message)); - if (!DebugEnabled) - return; - Logger.Debug(message); - } + 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(DateTimeOffset.Now.ToUnixTimeMilliseconds(), label, message)); - Logger.Raw($"[{label}] [{Plugin.Instance.Name}] {message}", ConsoleColor.Gray); - } + 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(DateTimeOffset.Now.ToUnixTimeMilliseconds(), nameof(LogLevel.Info), message)); - Logger.Raw($"[INFO] [{Plugin.Instance.Name}] {message}", color); - } + 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(DateTimeOffset.Now.ToUnixTimeMilliseconds(), nameof(LogLevel.Warn), message, error)); - Logger.Warn(message); - } + 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") - { - History.Add(new(DateTimeOffset.Now.ToUnixTimeMilliseconds(), nameof(LogLevel.Warn), message, error)); - Logger.Error(message); - } + public static void Error(string message, string error = "CS0000") + { + History.Add(new LogEntry(DateTimeOffset.Now.ToUnixTimeMilliseconds(), nameof(LogLevel.Error), message, error)); + Logger.Error(message); + } - public static void Silent(string message) => History.Add(new(DateTimeOffset.Now.ToUnixTimeMilliseconds(), "Silent", 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(DateTimeOffset.Now.ToUnixTimeMilliseconds(), "System", 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) + internal static IEnumerator SendReport(bool online, Action callback) + { + if (History.Count < 1) { - content = null; - - if (History.Count < 1) - return HttpStatusCode.Forbidden; + callback?.Invoke(HttpStatusCode.Forbidden, null); + yield break; + } - StringBuilder builder = StringBuilderPool.Shared.Rent(); + var builder = StringBuilderPool.Shared.Rent(); - foreach (LogEntry Element in History) - builder.Append($"{Element}\n"); + foreach (var Element in History) + builder.Append($"{Element}\n"); - // Now let's add the separator - builder.Append("\n======== BEGIN CUSTOM ROLES ========\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"); + foreach (var 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)); + var report = StringBuilderPool.Shared.ToStringReturn(builder); - return response; + if (!online) + { + File.WriteAllText( + Path.Combine(PathManager.Configs.FullName, $"UCR-Report-{DateTimeOffset.Now.ToUnixTimeSeconds()}.txt"), + 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/MapSpawnValidator.cs b/UncomplicatedCustomRoles/Manager/MapSpawnValidator.cs new file mode 100644 index 00000000..0f5aeaba --- /dev/null +++ b/UncomplicatedCustomRoles/Manager/MapSpawnValidator.cs @@ -0,0 +1,88 @@ +/* + * 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() + { + foreach (var role in CustomRole.CustomRoles.Values) + RoleValidator.ValidatePostLoad(role); + + 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) + { + 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 b68129a5..59478601 100644 --- a/UncomplicatedCustomRoles/Manager/NET/HttpManager.cs +++ b/UncomplicatedCustomRoles/Manager/NET/HttpManager.cs @@ -1,221 +1,356 @@ /* * 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.Threading.Tasks; +using System.Text.Json; using LabApi.Events.Arguments.PlayerEvents; using LabApi.Events.Handlers; using LabApi.Features.Wrappers; -using UncomplicatedCustomRoles.API.Struct; -using UncomplicatedCustomRoles.Extensions; +using MEC; using UncomplicatedCustomRoles.API.Features.Messages; +using UncomplicatedCustomRoles.API.Struct; -namespace UncomplicatedCustomRoles.Manager.NET -{ +namespace UncomplicatedCustomRoles.Manager.NET; #pragma warning disable IDE1006 - internal class HttpManager - { - /// - /// 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; - } - } +internal class HttpManager +{ + private const string GitHubReleases = + "https://github.com/UncomplicatedCustomServer/UncomplicatedCustomRoles/releases"; - private Version _latestVersion { get; set; } = null; + private const string GitHubLatestRelease = GitHubReleases + "/latest"; - private bool _alreadyManaged { get; set; } = false; + private const string DiscordInvite = "https://discord.gg/5StRGu8EJV"; - /// - /// Create a new istance of the HttpManager - /// - /// - public HttpManager(string prefix) - { - Prefix = prefix; - RegisterEvents(); - HttpClient = new(); - Task.Run(LoadCreditTags); - } + private const string CreditsEndpoint = "https://api.ucserver.it/credits.json"; - internal void RegisterEvents() - { - PlayerEvents.Joined += OnVerified; - } + private const string OwnersEndpoint = "https://api.ucserver.it/v3/owners"; - internal void UnregisterEvents() - { - PlayerEvents.Joined -= OnVerified; - } + /// + /// Create a new istance of the HttpManager + /// + /// + public HttpManager(string prefix) + { + Prefix = prefix; + RegisterEvents(); + LoadCreditTags(); + } + + /// + /// Gets the prefix of the plugin for our APIs + /// + public string Prefix { 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 every version of the plugin known by the UCS cloud, empty until is done + /// + public List Versions { get; private set; } = []; + + /// + /// Gets the latest of the plugin, pre-releases included, loaded by the UCS cloud + /// + 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; private set; } = new(); + + /// + /// Gets the latest pre-release of the plugin, loaded by the UCS cloud. + /// + public Version LatestPreRelease { get; private set; } = new(); + + /// + /// Gets whether the running build is a pre-release + /// + public bool IsPreRelease => IsPreReleaseVersion(Plugin.Instance.Version); + + 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) + 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)), + "application/json", callback); + } + + 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 CoroutineHandle LoadVersions() + { + return Timing.RunCoroutine(LoadVersionsCoroutine(), "UCR_Http"); + } + + private IEnumerator LoadVersionsCoroutine() + { + Versions = []; + LatestVersion = new Version(); + LatestStableVersion = new Version(); + LatestPreRelease = new Version(); + + 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 { - return HttpQuery.Post("https://api.ucserver.it/v3/owners", JsonSerializer.Serialize(new OwnerMessage(player, discordId)), "application/json"); + Versions = JsonSerializer.Deserialize>(response.Body) ?? []; } - - public void LoadLatestVersion() + catch { - string Version = HttpQuery.Get($"{Endpoint}/{Prefix}/versions/latest@text/plain"); - - if (!string.IsNullOrEmpty(Version) && Version.Contains(".")) - _latestVersion = new(Version); - else - _latestVersion = new(); + LogManager.Debug( + $"Failed to load the version list from the UCS cloud ({response.Reason}): '{response.Body}'"); + Versions = []; + return; } - public void LoadCreditTags() + foreach (var version in Versions) { - Credits = new(); - try - { - Dictionary> Data = JsonSerializer.Deserialize>>(HttpQuery.Get($"https://api.ucserver.it/credits.json")); + if (!Version.TryParse(version.Name, out var parsed)) + continue; - if (Data is null) - { - LogManager.Warn("Failed to connect to the UCS Central Server to get the credit tags informations!"); - return; - } + if (CompareReleases(parsed, LatestVersion) > 0) + LatestVersion = parsed; - 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); - } + if (version.PreRelease == 0) + { + if (CompareReleases(parsed, LatestStableVersion) > 0) + LatestStableVersion = parsed; } - catch (Exception e) + else if (CompareReleases(parsed, LatestPreRelease) > 0) { - 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}"); + LatestPreRelease = parsed; } } + } + + /// + /// Loads the latest version from the single-value endpoint, used when the version list is unavailable. + /// + private void LoadLatestVersionFallback(HttpResponse response) + { + var answer = response.Body; - public Triplet GetCreditTag(Player player) + try { - if (Credits.TryGetValue(player.UserId, out var tag)) - return tag; + if (string.IsNullOrEmpty(answer) || !answer.Contains(".")) + { + LogManager.Debug($"The UCS cloud gave us no latest version to fall back on ({response.Reason})"); + return; + } - return new(null, null, false); + LatestVersion = new Version(answer.Trim()); + + if (LatestVersion.Revision <= 0) + LatestStableVersion = LatestVersion; + else + LatestPreRelease = LatestVersion; + } + catch + { + 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) && CompareReleases(parsed, version) is 0); + 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 + /// nothing newer to install. + /// + public Version GetUpdateTarget() + { + var target = ResolveChannelTarget(); + return CompareReleases(target, Plugin.Instance.Version) > 0 ? target : null; + } + + public string GetDownloadHint(Version version) + { + TryGetVersionInfo(version, out var info); - public void ApplyCreditTag(Player player) + var link = string.IsNullOrWhiteSpace(info?.SourceLink) ? null : info.SourceLink.Trim(); + + return info?.Source?.Trim().ToLowerInvariant() switch { - if (!Plugin.Instance.Config.EnableCreditTags) - return; + "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)}" + }; + } - if (_alreadyManaged) - return; + public void LoadCreditTags() + { + Credits = new Dictionary>(); + IsJobRole.Clear(); - Triplet Tag = GetCreditTag(player); - - if (!string.IsNullOrEmpty(player.ReferenceHub.serverRoles.Network_myText)) - { - if (Credits.Any(k => k.Value.First == player.ReferenceHub.serverRoles.Network_myText && k.Value.Second == player.ReferenceHub.serverRoles.Network_myColor)) - _alreadyManaged = true; + WebQuery.Get(CreditsEndpoint, LoadCreditTagList); + } + + private void LoadCreditTagList(HttpResponse response) + { + try + { + var Data = JsonSerializer.Deserialize>>(response.Body); - if (!Tag.Third) - return; // Do not override + if (Data is null) + { + 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[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::LoadCreditTagList() ({response.Reason}) - {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 bool IsLatestVersion() - { - if (LatestVersion.CompareTo(Plugin.Instance.Version) > 0) - return false; + public void ApplyCreditTag(Player player) + { + if (!Plugin.Instance.Config.EnableCreditTags) + return; - return true; - } + var tag = GetCreditTag(player); - internal HttpStatusCode ShareLogs(string data, out string content) + if (!string.IsNullOrEmpty(player.ReferenceHub.serverRoles.Network_myText)) { - content = HttpQuery.Post($"{Endpoint}/{Prefix}/logs", JsonSerializer.Serialize(new ShareLogMessage(data)), "application/json"); - return content.GetStatusCode(out _); + if (Credits.Any(k => + k.Value.First == player.ReferenceHub.serverRoles.Network_myText && + k.Value.Second == player.ReferenceHub.serverRoles.Network_myColor)) + return; + + if (!tag.Third) + return; // Do not override } -#nullable enable - internal string VersionInfo() + if (tag.First is not null && tag.Second is not null) { - return HttpQuery.Get($"{Endpoint}/{Prefix}/versions/{Plugin.Instance.Version.ToString(4)}"); + player.ReferenceHub.serverRoles.SetText(tag.First); + 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 new file mode 100644 index 00000000..6a96013b --- /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"); +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Manager/NET/SpawnPointApiCommunicator.cs b/UncomplicatedCustomRoles/Manager/NET/SpawnPointApiCommunicator.cs deleted file mode 100644 index 785ddbfa..00000000 --- a/UncomplicatedCustomRoles/Manager/NET/SpawnPointApiCommunicator.cs +++ /dev/null @@ -1,163 +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.Text.Json; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -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 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 (!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) - { - TryLoadSpawnPoints(File.ReadAllText(FilePath)); - 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 - { - 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}"); - } - } - - /// - /// 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() - { - 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!"); - } - - private static void TryLoadSpawnPoints(string json) - { - List List = JsonSerializer.Deserialize>(json); - - foreach (SpawnPoint 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 2c2d274b..7a3e7909 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/NET/WebQuery.cs b/UncomplicatedCustomRoles/Manager/NET/WebQuery.cs new file mode 100644 index 00000000..f42d5d9b --- /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}"); + } + } +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Manager/PlaceholderManager.cs b/UncomplicatedCustomRoles/Manager/PlaceholderManager.cs index f604c5d6..6970f5cd 100644 --- a/UncomplicatedCustomRoles/Manager/PlaceholderManager.cs +++ b/UncomplicatedCustomRoles/Manager/PlaceholderManager.cs @@ -1,43 +1,59 @@ /* * 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.Features; 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.BulkReplace(new() + return (origin ?? string.Empty).BulkReplace(new Dictionary + { + { "nick", player.Nickname }, + { "displayname", ResolveDisplayName(player) }, + { "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 ?? string.Empty }, + { "customroleid", role?.Id ?? 0 }, + { "customrolebadge", role?.BadgeName ?? string.Empty }, + { "health", player.Health }, + { "max_health", player.MaxHealth }, + { "ahp", player.ArtificialHealth }, + { "max_ahp", player.MaxArtificialHealth }, + { "hume", player.HumeShield }, + { "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/PluginImportManager.cs b/UncomplicatedCustomRoles/Manager/PluginImportManager.cs index 420024db..72001db8 100644 --- a/UncomplicatedCustomRoles/Manager/PluginImportManager.cs +++ b/UncomplicatedCustomRoles/Manager/PluginImportManager.cs @@ -6,56 +6,59 @@ 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 && (type.IsSubclassOf(typeof(ICustomRole)) || type.IsSubclassOf(typeof(CustomRole)) || type.IsSubclassOf(typeof(EventCustomRole)))) - { - 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) + { + if (!ImportManager.AvailableAssemblies.Contains(assembly)) + ImportManager.AvailableAssemblies.Add(assembly); } -} +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Manager/RoleValidator.cs b/UncomplicatedCustomRoles/Manager/RoleValidator.cs new file mode 100644 index 00000000..fde634fa --- /dev/null +++ b/UncomplicatedCustomRoles/Manager/RoleValidator.cs @@ -0,0 +1,580 @@ +/* + * 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 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.Extensions; +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)) + { + 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); + + 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 not RoleTypeId.None && role.RoleAppearance.GetTeam() is Team.Dead) + warnings.Add( + $"'role_appearance' '{role.RoleAppearance}' is not a valid alive role; the role will keep the appearance of '{role.Role}'."); + } + + 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.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 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."); + 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 => 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.{(closest is null ? string.Empty : $" Did you mean '{closest}'?")} 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(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) + { + 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})."); + + 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) + { + 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.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."); + + if (role.IgnoreSpawnSystem) + return; + + ValidateSpawnEligibility(role, warnings); + } + + private static void ValidateSpawnEligibility(ICustomRole role, List warnings) + { + if (role.SpawnSettings.SpawnChance <= 0) + warnings.Add( + $"'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' 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; + + 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 (role.SpawnSettings.CanReplaceRoles is not { } canReplaceRoles || !canReplaceRoles.Any()) + { + 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 (!delayed) + foreach (var replace in 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."); + } + + 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") + 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( + $"'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; + + List>> flags; + try + { + flags = YamlFlagsHandler.Decode(role.CustomFlags) ?? []; + } + 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.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."); + 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 e669e392..45ddf568 100644 --- a/UncomplicatedCustomRoles/Manager/SpawnManager.cs +++ b/UncomplicatedCustomRoles/Manager/SpawnManager.cs @@ -1,576 +1,678 @@ /* * 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.Events; +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 void SummonCustomSubclass(Player player, int id, bool doBypassRoleOverwrite = true) + 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) + { + 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; - } + var spawningArgs = new CustomRoleSpawningEventArgs(player, Role); + 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); + // 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, true); + } + catch (Exception ex) + { + LogManager.Error(ex.ToString(), "SP0002"); } + finally + { + Spawn.Spawning.Remove(player.PlayerId); + } + } + + public static void SummonSubclassApplier(Player Player, ICustomRole Role) + { + SummonSubclassApplier(Player, Role, false); + } - public static void SummonSubclassApplier(Player Player, ICustomRole Role) + internal static void SummonSubclassApplier(Player Player, ICustomRole Role, bool spawningEventAlreadyFired) + { + try { - try + if (!spawningEventAlreadyFired) { - 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 + var spawningArgs = new CustomRoleSpawningEventArgs(Player, Role); + 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); + + if (Role.CustomItemsInventory is { Count: > 0 }) + 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(); + Player.ClearAmmo(); - if (Role.Ammo is not null && Role.Ammo.GetType() == typeof(Dictionary) && Role.Ammo.Any()) - foreach (KeyValuePair Ammo in Role.Ammo) - { - if (Ammo.Value > Player.GetAmmoLimit(Ammo.Key)) - Player.SetAmmoLimit(Ammo.Key, Ammo.Value); + if (Role.Ammo is { Count: > 0 }) + 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); - } + Player.AddAmmo(Ammo.Key, Ammo.Value); + } - // Reset the inventory if we need to add the old one - if (PlayerEventHandler.RespawnInventoryQueue.TryGetValue(Player.PlayerId, out Tuple, Dictionary, bool> oldInventory)) + if (PlayerEventHandler.RespawnInventoryQueue.TryRemove(Player.PlayerId, out var oldInventory)) + { + if (!oldInventory.Item3) { 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.Item1) + Player.AddItem(item); - 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(); - } + 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(); } - - PlayerInfoArea 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); + var InfoArea = Player.ReferenceHub.nicknameSync.Network_playerInfoToShow; - if (Role.Scale != Vector3.zero && Role.Scale != Vector3.one) - Player.Scale = Role.Scale; + // Apply every required stats + Role.Health?.Apply(Player); + Role.Ahp?.Apply(Player); + Role.HumeShield?.Apply(Player); + Role.Stamina?.Apply(Player); - List PermanentEffects = new(); - if (Role.Effects != null && Role.Effects.Any()) + if (Role.Scale != Vector3.zero && Role.Scale != Vector3.one) + Player.Scale = Role.Scale; + + 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 (!string.IsNullOrEmpty(Role.SpawnBroadcast)) + { + Player.ClearBroadcasts(); + Player.SendBroadcast(Role.SpawnBroadcast, Role.SpawnBroadcastDuration); + } - Player.ReferenceHub.serverRoles.SetText(Role.BadgeName.Replace("@hidden", "")); - Player.ReferenceHub.serverRoles.SetColor(Role.BadgeColor); + if (!string.IsNullOrEmpty(Role.SpawnHint)) + 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; - - 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; - }); + // 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); - ChangedNick = true; - } + appliedNick = Role.Nickname.Contains(",") ? Nick.Split(',').RandomItem().Trim() : Nick; + Player.DisplayName = appliedNick; - // Roll out custom info - CustomInfo customInfo = new(Player, Role); + ChangedNick = true; + } - LogManager.Debug($"{Player} successfully spawned as {Role.Name} ({Role.Id})!"); + // Roll out custom info + CustomInfo customInfo = new(Player, Role); - SummonedCustomRole roleInstance = new(Player, Role, Badge, PermanentEffects, InfoArea, customInfo, ChangedNick); + LogManager.Debug($"{Player} successfully spawned as {Role.Name} ({Role.Id})!"); - customInfo.UpdateInfo(Player); - - EscapeController escapeController = Player.GameObject.AddComponent(); - escapeController.Init(roleInstance); - - if (Spawn.Spawning.Contains(Player.PlayerId)) - Spawn.Spawning.Remove(Player.PlayerId); + SummonedCustomRole roleInstance = + new(Player, Role, Badge, PermanentEffects, InfoArea, customInfo, ChangedNick); - if (API.Features.Escape.Bucket.Contains(Player.PlayerId)) - API.Features.Escape.Bucket.Remove(Player.PlayerId); + roleInstance.AppliedNickname = appliedNick; - LogManager.Debug($"{Player} successfully spawned as {Role.Name} ({Role.Id})! [2VDS]"); - } - catch (Exception ex) - { - LogManager.Error(ex.ToString(), "SP0001"); - } - } + if (appliedNick is not null) + customInfo.Nickname = appliedNick; - 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 (KeyValuePair kvp in roleAfterEscape) - { - KeyValuePair? Data = ParseEscapeString(kvp.Value); - if (kvp.Key is "default") - Default = Data; - else + customInfo.UpdateInfo(Player); + + if (appliedNick is not null && Plugin.Instance.Config.OverrideRpNames) + roleInstance.NicknameReapplyCoroutine = Timing.CallDelayed(3f, () => { - List Elements = kvp.Key.Split(' ').ToList(); + if (roleInstance.IsValid && SummonedCustomRole.Get(roleInstance.Player) == roleInstance) + roleInstance.Player.DisplayName = appliedNick; + }); - 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); - } + var escapeController = Player.GameObject.AddComponent(); + escapeController.Init(roleInstance); - 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 (Spawn.Spawning.Contains(Player.PlayerId)) + Spawn.Spawning.Remove(Player.PlayerId); - 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 (API.Features.Escape.Bucket.Contains(Player.PlayerId)) + API.Features.Escape.Bucket.Remove(Player.PlayerId); - 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]}"); - } - } + CustomRoleEvents.OnSpawned(new CustomRoleSpawnedEventArgs(roleInstance)); - // 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}"); - return Default; + LogManager.Debug($"{Player} successfully spawned as {Role.Name} ({Role.Id})! [2VDS]"); } - - public static KeyValuePair? ParseEscapeString(string escape) + catch (Exception ex) { - if (escape is "Deny" or "deny" or "DENY") - return null; + LogManager.Error(ex.ToString(), "SP0001"); + } + } - List Elements = escape.Split(' ').ToList(); - if (Elements.Count != 2) + 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) + { + var Data = ParseEscapeString(kvp.Value); + if (kvp.Key is "default") { - LogManager.Warn($"Failed to parse an EscapeString[value]: syntax should be (2 args), found {Elements.Count} args!\nSource: {escape}"); - return new(false, RoleTypeId.Spectator); + Default = Data; } + else + { + var Elements = kvp.Key.Split(' '); - 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.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 ' (4 args), found {Elements.Length} args!\nSource: {kvp.Key}"); + continue; + } - return new(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]}"); + } } -#nullable enable - public static ICustomRole? DoEvaluateSpawnForPlayer(Player player, RoleTypeId? role = null) + // 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; + + var Elements = escape.Split(' ').ToList(); + if (Elements.Count != 2) { - role ??= player.Role; + 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 (role is null) - return null; + 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]}"); - RoleTypeId NewRole = (RoleTypeId)role; + return new KeyValuePair(false, RoleTypeId.Spectator); + } - Dictionary> RolePercentage = new() - { - { 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) - { - static bool CheckPermission(Player player, string permission) - { - if (Enum.TryParse(permission, out PlayerPermissions playerPermissions)) - return player.HasPermission(playerPermissions); +#nullable enable + public static ICustomRole? DoEvaluateSpawnForPlayer(Player player, RoleTypeId? role = null) + { + role ??= player.Role; - return player.HasAnyPermission(permission); - } + if (role is null) + return null; - static IEnumerable ExtractPermissions(object obj) - { - switch (obj) - { - case string s when !string.IsNullOrWhiteSpace(s): - return new[] { s }; - case System.Collections.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 Array.Empty(); - } - } + var NewRole = (RoleTypeId)role; - var permsList = ExtractPermissions(Role.SpawnSettings.RequiredPermission).ToList(); - if (permsList.Any()) - { - 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; - } - } - } + if (player.HasCustomRole()) + { + LogManager.Debug("Was evalutating role select for an already custom role player, stopping"); + return null; + } - foreach (RoleTypeId RoleType in Role.SpawnSettings.CanReplaceRoles) - for (int a = 0; a < Role.SpawnSettings.SpawnChance; a++) - RolePercentage[RoleType].Add(Role); - } + if (!SpawnEvaluatedRoles.Contains(NewRole)) + return null; + + var readyPlayers = Player.ReadyList.Count(); + List candidates = []; - if (player.HasCustomRole()) + foreach (var Role in CustomRole.CustomRoles.Values) + 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) { - LogManager.Debug("Was evalutating role select for an already custom role player, stopping"); - return null; + if (!HasRequiredPermission(player, Role)) + continue; + + for (var a = 0; a < Role.SpawnSettings.SpawnChance; a++) + candidates.Add(Role); } - if (RolePercentage.ContainsKey(NewRole)) - if (UnityEngine.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; + 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); } - - public static void AnnounceScpTermination(ReferenceHub scp, DamageHandlerBase hit) + + static IEnumerable ExtractPermissions(object obj) { - string announcement1 = hit.CassieDeathAnnouncement.Announcement; - SubtitlePart[] subtitleParts1 = hit.CassieDeathAnnouncement.SubtitleParts; - if (string.IsNullOrEmpty(announcement1)) - return; - foreach (CassieAnnouncement cassieAnnouncement in CassieAnnouncementDispatcher.AllAnnouncementsPreview) + switch (obj) { - if (cassieAnnouncement is CassieScpTerminationAnnouncement terminationAnnouncement && terminationAnnouncement._announcementTts == announcement1 && SubtitlePart.CheckEqualValues(terminationAnnouncement._subtitles, subtitleParts1)) + case string s when !string.IsNullOrWhiteSpace(s): + return [s]; + case IEnumerable enumerable: { - terminationAnnouncement._victims.Add(new Footprint(scp)); - terminationAnnouncement._remainingWait = 1f; - return; + 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 []; } - 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; - } - } -} + 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; + 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/SpawnPointManager.cs b/UncomplicatedCustomRoles/Manager/SpawnPointManager.cs new file mode 100644 index 00000000..8911982c --- /dev/null +++ b/UncomplicatedCustomRoles/Manager/SpawnPointManager.cs @@ -0,0 +1,125 @@ +/* + * 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; + +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!"); + } +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Manager/VersionManager.cs b/UncomplicatedCustomRoles/Manager/VersionManager.cs index a6d5e4db..155db9d6 100644 --- a/UncomplicatedCustomRoles/Manager/VersionManager.cs +++ b/UncomplicatedCustomRoles/Manager/VersionManager.cs @@ -1,96 +1,156 @@ /* * 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.IO; +using System.Net; 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 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 { - try + var data = response.Body; + + if (string.IsNullOrWhiteSpace(data)) { - 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( + $"The UCS cloud gave us an empty answer while asking for the version info ({response.Reason})."); + 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 + 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) + { + LogManager.Silent( + $"Failed to convert API endpoint answer to VersionInfo.\nContent: {msg ?? "Message is null"}"); + return; + } + + 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: {(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($"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; + CheckForUpdates(); - if (VersionInfo.Message is not null) - LogManager.Info(VersionInfo.Message); + var hash = HashFile(Plugin.Instance.FilePath); + if (hash != VersionInfo.Hash) + HashNotMatchMessageSender(hash); - 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) + else + CorrectHash = true; + + 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 CheckForUpdates() + { + try + { + UpdateTarget = Plugin.HttpManager.GetUpdateTarget(); - 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!"); + if (UpdateTarget is null) + return; - public static string HashFile(string path) + 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) { - FileStream file = new(path, FileMode.Open) - { - Position = 0 - }; - byte[] bytes = SHA256Managed.Create().ComputeHash(file); + LogManager.Error("An error occurred while checking for a newer version of the plugin."); + LogManager.Debug(e.ToString()); + } + } - file.Close(); + 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"); + } - return BitConverter.ToString(bytes).Replace("-", string.Empty); - } + 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}!{download}\nThis version CONTAINS IMPORTANT BUGS and for that reason SWITCHING TO THE NEWER ONE IS ESSENTIAL!"); + } + + 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); } } \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Manager/YamlFlagsHandler.cs b/UncomplicatedCustomRoles/Manager/YamlFlagsHandler.cs index 1896ae9c..121fab03 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,70 @@ 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; - } + var cached = _modules; + + if (cached is not null) + return cached; + + cached = GetModules(); + _modules = cached; + + return cached; } + } - 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 List?>>? Decode(List flags) + { + if (flags is null) + return null; - Dictionary?> result = new(); + List?>> result = []; - foreach (object flag in flags) - { - 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; - } + 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(), + 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.Add(new KeyValuePair?>(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 d2ab8586..b69bfa96 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/CategoryLimitPatch.cs b/UncomplicatedCustomRoles/Patches/CategoryLimitPatch.cs new file mode 100644 index 00000000..3888e8e7 --- /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 diff --git a/UncomplicatedCustomRoles/Patches/ChangeCustomPlayerInfoPatch.cs b/UncomplicatedCustomRoles/Patches/ChangeCustomPlayerInfoPatch.cs index 64e3c77b..49920d6c 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 c95c3046..57509a8c 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 a28b6eca..55a4c10a 100644 --- a/UncomplicatedCustomRoles/Patches/PlayerEventPrefix.cs +++ b/UncomplicatedCustomRoles/Patches/PlayerEventPrefix.cs @@ -1,67 +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 List _patchedMethods = []; + + 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 (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)) { - string name = ev.GetType().Name.Replace("EventArgs", string.Empty).Replace("Player", string.Empty); - - 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()); + } + } - PatchedMethods = typeof(PlayerEvents).GetMethods().Where(m => m.Name.StartsWith("On") && m.GetParameters().Length > 0 && typeof(IPlayerEvent).IsAssignableFrom(m.GetParameters()[0].ParameterType)); + internal static void Patch(Harmony harmony) + { + HarmonyMethod prefixMethod = + new(typeof(PlayerEventPrefix).GetMethod("Prefix", BindingFlags.Static | BindingFlags.NonPublic)); - foreach (MethodInfo method in PatchedMethods) - harmony.Patch(method, prefix: prefixMethod); - } + _patchedMethods = typeof(PlayerEvents).GetMethods().Where(m => + m.Name.StartsWith("On") && m.GetParameters().Length > 0 && + typeof(IPlayerEvent).IsAssignableFrom(m.GetParameters()[0].ParameterType)).ToList(); - internal static void Unpatch(Harmony harmony) - { - foreach (MethodInfo method in PatchedMethods) - harmony.Unpatch(method, HarmonyPatchType.All); - } + foreach (var method in _patchedMethods) + harmony.Patch(method, prefixMethod); + } + + 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 561ba755..98b3c501 100644 --- a/UncomplicatedCustomRoles/Patches/PlayerInfoSyncPatch.cs +++ b/UncomplicatedCustomRoles/Patches/PlayerInfoSyncPatch.cs @@ -12,43 +12,39 @@ 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 SummonedCustomRole _)) - { - value |= PlayerInfoArea.CustomInfo; - value &= ~PlayerInfoArea.Role; - value &= ~PlayerInfoArea.Nickname; - value &= ~PlayerInfoArea.UnitName; - } - } + if (CustomInfo.SuppressExternalSync) + return; + + 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 diff --git a/UncomplicatedCustomRoles/Patches/Scp3114StranglePrefix.cs b/UncomplicatedCustomRoles/Patches/Scp3114StranglePrefix.cs index 6dcbacde..979b9832 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 19289bd6..0a50bbad 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 ac1403bf..7b761a2d 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 . @@ -12,46 +12,46 @@ using HarmonyLib; using Mirror; using PlayerRoles; -using Respawning.NamingRules; using UncomplicatedCustomRoles.API.Features; +using UncomplicatedCustomRoles.Extensions; -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 { UsesUnitNames: true } humanRole + && humanRole.Team.TryGetLatestUnitNameId(out var unitNameId)) + humanRole.UnitNameId = unitNameId; + } +} \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Patches/StaminaUsagePatch.cs b/UncomplicatedCustomRoles/Patches/StaminaUsagePatch.cs index eb9bae89..7e7f506b 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 d2f869a1..45e77aa2 100644 --- a/UncomplicatedCustomRoles/Patches/TeamPatch.cs +++ b/UncomplicatedCustomRoles/Patches/TeamPatch.cs @@ -8,285 +8,408 @@ * If not, see . */ -using Achievements.Handlers; -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.API.Features.CustomModules; 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) - { - if (__instance.Hub == null || __instance.Hub.netId == 0) - return true; - - if (RoleSerializationContext.Active) - return true; + var hub = __instance.Hub; + if (hub is null || !DisguiseTeam.RoleBaseList.TryGetValue(hub.PlayerId, out var role) || role is null) + 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!"); + if (RoleSerializationContext.Active) + return true; - __result = role; + __result = role; + return false; + } +} - return false; - } +internal static class TeamFakeContext +{ + [ThreadStatic] private static int _depth; - return true; - } + 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 class RoleSerializationContext + + internal static void Exit() { - [ThreadStatic] private static int _depth; + if (_depth > 0) + _depth--; + } +} - internal static bool Active => _depth > 0; +[HarmonyPatchCategory(TeamPatchManager.Category)] +[HarmonyPatch(typeof(RoleSyncInfo), MethodType.Constructor, typeof(ReferenceHub), typeof(RoleTypeId), + typeof(ReferenceHub), typeof(NetworkWriter))] +internal class RoleSyncInfoCtorPatch +{ + private static void Prefix() + { + RoleSerializationContext.Enter(); + } - internal static void Enter() => _depth++; + private static void Finalizer() + { + RoleSerializationContext.Exit(); + } +} - internal static void Exit() +[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; + + 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 code; + } +} - return true; - } +[HarmonyPatchCategory(TeamPatchManager.Category)] +[HarmonyPatch] +internal class TeamFakeContextPatch +{ + private static IEnumerable TargetMethods() + { + 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))); } - [HarmonyPatchCategory(TeamPatchManager.Category)] - [HarmonyPatch] - internal class TeamFakeContextPatch + private static IEnumerable Declared(Type type, string name) { - 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(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(); + return type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | + BindingFlags.Static | BindingFlags.DeclaredOnly) + .Where(m => m.Name == name && !m.IsAbstract && !m.ContainsGenericParameters); } - [HarmonyPatchCategory(TeamPatchManager.Category)] - [HarmonyPatch(typeof(ExplosionGrenade), nameof(ExplosionGrenade.ExplodeDestructible))] - internal class GrenadeTranspiler + private static void Prefix() { - static IEnumerable Transpiler(IEnumerable instructions) - { - List newInstructions = new(instructions); - int index = -1; + TeamFakeContext.Enter(); + } + + private static void Finalizer() + { + TeamFakeContext.Exit(); + } +} - for (int i = 0; i < newInstructions.Count; i++) +[HarmonyPatchCategory(TeamPatchManager.Category)] +[HarmonyPatch(typeof(Footprint), MethodType.Constructor, typeof(ReferenceHub))] +internal class FootprintContextPatch +{ + private static void Prefix() + { + TeamFakeContext.Enter(); + } + + private static void Finalizer() + { + TeamFakeContext.Exit(); + } +} + +[HarmonyPatchCategory(TeamPatchManager.Category)] +[HarmonyPatch(typeof(ExplosionGrenade), nameof(ExplosionGrenade.ExplodeDestructible))] +internal class GrenadeTranspiler +{ + private static IEnumerable Transpiler(IEnumerable instructions) + { + List newInstructions = new(instructions); + var index = -1; + + 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] +public class DoorPermissionsPolicyPatch +{ + private static MethodBase TargetMethod() + { + return Method(typeof(DoorPermissionsPolicy), "CheckPermissions", [ + typeof(ReferenceHub), typeof(IDoorPermissionRequester), typeof(PermissionUsed).MakeByRefType() + ]); } - [HarmonyPatchCategory(TeamPatchManager.Category)] - [HarmonyPatch(typeof(PickupSearchCompletor), nameof(PickupSearchCompletor.ValidateAny))] - public class PickupSearchCompletorPatch + private static bool Prefix(DoorPermissionsPolicy __instance, ReferenceHub hub, IDoorPermissionRequester requester, + out PermissionUsed callback, ref bool __result) { - static bool Prefix(PickupSearchCompletor __instance, ref bool __result) + callback = null; + if (__instance.RequiredPermissions == DoorPermissionFlags.None || hub.serverRoles.BypassMode) { - 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); + __result = true; return false; } + + 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; + } + + var curInstance = hub.inventory.CurInstance; + __result = curInstance != null && curInstance is IDoorPermissionProvider provider && + __instance.CheckPermissions(provider, requester, out callback); + return false; } - - [HarmonyPatchCategory(TeamPatchManager.Category)] - [HarmonyPatch] - public class DoorPermissionsPolicyPatch +} + +[HarmonyPatchCategory(TeamPatchManager.Category)] +[HarmonyPatch(typeof(DoorPermissionsPolicyExtensions), nameof(DoorPermissionsPolicyExtensions.GetCombinedPermissions))] +public class DoorPermissionsPolicyExtensionsPatch +{ + private static bool Prefix(ReferenceHub hub, IDoorPermissionRequester requester, ref DoorPermissionFlags __result) { - static MethodBase TargetMethod() + if (hub == null) { - return Method(typeof(DoorPermissionsPolicy), "CheckPermissions", new[] { typeof(ReferenceHub), typeof(IDoorPermissionRequester), typeof(PermissionUsed).MakeByRefType() }); + __result = DoorPermissionFlags.None; + return false; } - static bool Prefix(DoorPermissionsPolicy __instance, ReferenceHub hub, IDoorPermissionRequester requester, out PermissionUsed callback, ref bool __result) + if (hub.serverRoles.BypassMode) { - 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 = DoorPermissionFlags.All; 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 && !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; } - - [HarmonyPatchCategory(TeamPatchManager.Category)] - [HarmonyPatch(typeof(DoorPermissionsPolicyExtensions), nameof(DoorPermissionsPolicyExtensions.GetCombinedPermissions))] - public class DoorPermissionsPolicyExtensionsPatch +} + +[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, IDoorPermissionRequester requester, ref DoorPermissionFlags __result) - { - if (hub == null) - { - __result = DoorPermissionFlags.None; - return false; - } + if (hub == null || !DisguiseTeam.List.TryGetValue(hub.PlayerId, out var team)) + return true; - if (hub.serverRoles.BypassMode) - { - __result = DoorPermissionFlags.All; - return false; - } + __result = team == Team.SCPs; + return false; + } +} - DoorPermissionFlags combinedPermissions = DoorPermissionFlags.None; +[HarmonyPatchCategory(TeamPatchManager.Category)] +[HarmonyPatch(typeof(PlayerRolesUtils), nameof(PlayerRolesUtils.IsHuman), typeof(ReferenceHub))] +internal class IsHumanPatch +{ + private static bool Prefix(ReferenceHub hub, ref bool __result) + { + 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; + } +} - if (hub.roleManager.CurrentRole is IDoorPermissionProvider currentRole && - (!DisguiseTeam.List.TryGetValue(hub.PlayerId, out Team team) || team != Team.SCPs)) - combinedPermissions |= currentRole.GetPermissions(requester); +[HarmonyPatchCategory(TeamPatchManager.Category)] +[HarmonyPatch(typeof(Scp079Recontainer), nameof(Scp079Recontainer.OnServerRoleChanged))] +public class Scp079RecontainerPatch +{ + private static bool Prefix(Scp079Recontainer __instance, ReferenceHub hub, RoleTypeId newRole, + RoleChangeReason reason) + { + 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; - ItemBase curInstance = hub.inventory.CurInstance; - if (curInstance != null && curInstance is IDoorPermissionProvider permissionProvider) - combinedPermissions |= permissionProvider.GetPermissions(requester); + var effectiveTeam = x.GetRoleId().GetTeam(); + if (DisguiseTeam.List.TryGetValue(x.PlayerId, out var fakeTeam)) + effectiveTeam = fakeTeam; - __result = combinedPermissions; + 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; } - [HarmonyPatchCategory(TeamPatchManager.Category)] - [HarmonyPatch(typeof(Scp079Recontainer), nameof(Scp079Recontainer.OnServerRoleChanged))] - public class Scp079RecontainerPatch + private static bool IsScpButNot079(RoleTypeId roleTypeId, Team team) { - 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; - return false; - } + return team == Team.SCPs && roleTypeId != RoleTypeId.Scp079; + } +} - 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 diff --git a/UncomplicatedCustomRoles/Patches/TeamPatchManager.cs b/UncomplicatedCustomRoles/Patches/TeamPatchManager.cs index 91b4ba24..ba9b7b29 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 235de390..b6543deb 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,133 @@ using System; using System.Collections.Generic; -using UncomplicatedCustomRoles.Integrations; -using UncomplicatedCustomRoles.Manager; -using UncomplicatedCustomRoles.API.Features; +using System.Reflection; using HarmonyLib; -using UncomplicatedCustomRoles.Manager.NET; -using UncomplicatedCustomRoles.Patches; -using System.Threading.Tasks; +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; -namespace UncomplicatedCustomRoles +internal class Plugin : Plugin { - internal class Plugin : Plugin - { - public override string Name => "UncomplicatedCustomRoles"; + internal static Plugin Instance; - public override string Description => "Customize your SCP:SL server with Custom Roles!"; + internal static HttpManager HttpManager; - public override string Author => "FoxWorn3365, Dr.Agenda, MedveMarci"; + private Harmony _harmony; - public override Version Version { get; } = new(9, 5, 1, 0); + private bool _welcomeShown; + public override string Name => "UncomplicatedCustomRoles"; - public override Version RequiredApiVersion => new(LabApiProperties.CompiledVersion); + public override string Description => "Customize your SCP:SL server with Custom Roles!"; - public override LoadPriority Priority => LoadPriority.High; + public override string Author => "FoxWorn3365, Dr.Agenda, MedveMarci"; - public static Assembly Assembly => Assembly.GetExecutingAssembly(); + public override Version Version { get; } = new(9, 6, 0); - internal static Plugin Instance; + public override Version RequiredApiVersion => new(LabApiProperties.CompiledVersion); - internal static HttpManager HttpManager; + public override LoadPriority Priority => LoadPriority.High; - private Harmony _harmony; + public static Assembly Assembly => Assembly.GetExecutingAssembly(); - public override void Enable() - { - Instance = this; + public override void Enable() + { + Instance = this; - // QoL things - LogManager.History.Clear(); - API.Features.Escape.Bucket.Clear(); + // QoL things + LogManager.History.Clear(); + API.Features.Escape.Bucket.Clear(); - HttpManager = new("ucr"); + HttpManager = new HttpManager("ucr"); - CustomRole.CustomRoles.Clear(); - CustomRole.NotLoadedRoles.Clear(); + CustomRole.CustomRoles.Clear(); + CustomRole.NotLoadedRoles.Clear(); + FlagMigrator.Migrated.Clear(); - EventHandlerBase.Register(new List() - { - new ServerEventHandler(), - new PlayerEventHandler(), - new ScpEventHandler() - }); + EventHandlerBase.Register(new List + { + new ServerEventHandler(), + new PlayerEventHandler(), + new ScpEventHandler() + }); - 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"); + Timing.RunCoroutine(VersionManager.Init(), "UCR_Http"); - VersionManager.Init(); - }); + ImportManager.Unload(); - ImportManager.Unload(); + FileConfigs.Welcome(); + FileConfigs.Welcome(Server.Port.ToString()); + FileConfigs.LoadAll(); + FileConfigs.LoadAll(Server.Port.ToString()); - FileConfigs.Welcome(); - FileConfigs.Welcome(Server.Port.ToString()); - FileConfigs.LoadAll(); - FileConfigs.LoadAll(Server.Port.ToString()); + SpawnPointManager.Init(); - SpawnPointApiCommunicator.Init(); + DisguiseTeam.Clear(); - DisguiseTeam.Clear(); - - TeamPatchManager.Initialize(); + TeamPatchManager.Initialize(); - _harmony = new($"com.ucs.ucr_labapi-{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}"); - _harmony.PatchAllUncategorized(); + _harmony = new Harmony($"com.ucs.ucr_labapi-{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}"); + _harmony.PatchAllUncategorized(); - PlayerEventPrefix.Patch(_harmony); + PlayerEventPrefix.Patch(_harmony); - // Add presence - if (Config.EnableTelemetry) - Timing.RunCoroutine(Presence.PresenceCoroutine(), "UCR_Presence"); - } + // Add presence + if (Config.EnableTelemetry) + Timing.RunCoroutine(Presence.PresenceCoroutine(), "UCR_Presence"); + } - public override void Disable() - { - Timing.KillCoroutines("UCR_Presence"); + public override void Disable() + { + Timing.KillCoroutines("UCR_Presence"); + Timing.KillCoroutines("UCR_Http"); - ScriptedEvents.UnregisterCustomActions(); + ScriptedEvents.UnregisterCustomActions(); + if (_harmony is not null) + { PlayerEventPrefix.Unpatch(_harmony); + _harmony.UnpatchAll(_harmony.Id); + _harmony = null; + } - _harmony.UnpatchAll(); - - TeamPatchManager.Shutdown(); + TeamPatchManager.Shutdown(); - EventHandlerBase.UnregisterAll(); + EventHandlerBase.UnregisterAll(); - HttpManager.UnregisterEvents(); + HttpManager?.UnregisterEvents(); + HttpManager = null; - Instance = null; - } - - /// - /// Invoked after the server finish to load every plugin - /// - public void OnFinishedLoadingPlugins() - { - // Register ScriptedEvents integration - ScriptedEvents.RegisterCustomActions(); - - // 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 manager + ImportManager.Init(); + + if (_welcomeShown || Config is not { EnableBasicLogs: true }) return; + _welcomeShown = true; + 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", + ConsoleColor.DarkYellow); } } \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Properties/AssemblyInfo.cs b/UncomplicatedCustomRoles/Properties/AssemblyInfo.cs index 689d43cd..cf2b58b0 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")] +[assembly: AssemblyVersion("9.6.0")] +[assembly: AssemblyFileVersion("9.6.0")] \ No newline at end of file diff --git a/UncomplicatedCustomRoles/UncomplicatedCustomRoles.csproj b/UncomplicatedCustomRoles/UncomplicatedCustomRoles.csproj index 72cf8654..e3871e07 100644 --- a/UncomplicatedCustomRoles/UncomplicatedCustomRoles.csproj +++ b/UncomplicatedCustomRoles/UncomplicatedCustomRoles.csproj @@ -30,11 +30,9 @@ - - - - - + + + @@ -45,6 +43,7 @@ + diff --git a/UncomplicatedCustomRoles/app.config b/UncomplicatedCustomRoles/app.config index 71c40342..a0d1004b 100644 --- a/UncomplicatedCustomRoles/app.config +++ b/UncomplicatedCustomRoles/app.config @@ -1,15 +1,18 @@ - - - - - - - - - - - - - + + + + + + + + + + + + + + + +