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