From a2ca59faf5bb2dc57572b2875c74519d7859547b Mon Sep 17 00:00:00 2001 From: Ahmad <55556ahmad@gmail.com> Date: Mon, 13 Jul 2026 12:15:28 +0300 Subject: [PATCH] feat: introduce UcrEvents system and baseline CustomRole implementation for plugin integration --- .../API/Features/CustomRole.cs | 9 +- .../API/Features/EventCustomRole.cs | 9 +- .../API/Features/SummonedCustomRole.cs | 9 +- .../API/Features/UcrEvents.cs | 111 ++++++++++++++++++ .../API/Interfaces/ICustomRole.cs | 9 +- .../Events/PlayerEventHandler.cs | 16 ++- 6 files changed, 158 insertions(+), 5 deletions(-) create mode 100644 UncomplicatedCustomRoles/API/Features/UcrEvents.cs diff --git a/UncomplicatedCustomRoles/API/Features/CustomRole.cs b/UncomplicatedCustomRoles/API/Features/CustomRole.cs index 8c91f10b..b678d4ef 100644 --- a/UncomplicatedCustomRoles/API/Features/CustomRole.cs +++ b/UncomplicatedCustomRoles/API/Features/CustomRole.cs @@ -1,4 +1,4 @@ -/* +/* * This file is a part of the UncomplicatedCustomRoles project. * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) @@ -231,6 +231,13 @@ public class CustomRole : ICustomRole /// public virtual bool IgnoreSpawnSystem { get; set; } = false; + /// + /// Gets or sets an optional custom team/faction identifier (e.g. "SerpentHand", "UIU").

+ /// Used by EndConditionsExtension for custom win conditions and escape-based scoring. + /// When null or empty, the role uses the standard for round-end evaluation. + ///
+ public virtual string CustomTeamId { get; set; } = null; + /// /// Invoked when the custom role is spawned /// diff --git a/UncomplicatedCustomRoles/API/Features/EventCustomRole.cs b/UncomplicatedCustomRoles/API/Features/EventCustomRole.cs index 76bef164..145a2916 100644 --- a/UncomplicatedCustomRoles/API/Features/EventCustomRole.cs +++ b/UncomplicatedCustomRoles/API/Features/EventCustomRole.cs @@ -1,4 +1,4 @@ -/* +/* * This file is a part of the UncomplicatedCustomRoles project. * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) @@ -217,6 +217,13 @@ public class EventCustomRole : ICustomRole /// public virtual bool IgnoreSpawnSystem { get; set; } = false; + /// + /// Gets or sets an optional custom team/faction identifier (e.g. "SerpentHand", "UIU").

+ /// Used by EndConditionsExtension for custom win conditions and escape-based scoring. + /// When null or empty, the role uses the standard for round-end evaluation. + ///
+ public virtual string CustomTeamId { get; set; } = null; + public override string ToString() => $"{Regex.Replace(Name, "(.*?)", "$1")} ({Id})"; /// diff --git a/UncomplicatedCustomRoles/API/Features/SummonedCustomRole.cs b/UncomplicatedCustomRoles/API/Features/SummonedCustomRole.cs index 61de6d54..1bbcf4d3 100644 --- a/UncomplicatedCustomRoles/API/Features/SummonedCustomRole.cs +++ b/UncomplicatedCustomRoles/API/Features/SummonedCustomRole.cs @@ -1,4 +1,4 @@ -/* +/* * This file is a part of the UncomplicatedCustomRoles project. * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) @@ -202,6 +202,9 @@ internal SummonedCustomRole(Player player, ICustomRole role, Triplet @@ -294,6 +297,10 @@ private bool EvaluateCustomActions() public void Destroy() { LogManager.Silent($"Destroying instance {Id} of CR {Role.Id} of PL {Player}"); + + // Raise the public event before cleanup so extensions can still read state + UcrEvents.RaiseCustomRoleRemoved(Player, Role, this); + Remove(); List.TryRemove(Id, out _); _cachedListByPlayerId.TryRemove(Player.PlayerId, out _); diff --git a/UncomplicatedCustomRoles/API/Features/UcrEvents.cs b/UncomplicatedCustomRoles/API/Features/UcrEvents.cs new file mode 100644 index 00000000..1872cd0f --- /dev/null +++ b/UncomplicatedCustomRoles/API/Features/UcrEvents.cs @@ -0,0 +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 LabApi.Features.Wrappers; +using System; +using UncomplicatedCustomRoles.API.Interfaces; + +namespace UncomplicatedCustomRoles.API.Features +{ + /// + /// Public static event bus for external plugins/extensions to subscribe to UCR lifecycle events. + /// This is the primary integration point for addons like EndConditionsExtension. + /// + public static class UcrEvents + { + /// + /// Data passed along with every UCR event. + /// + public class CustomRoleEventData : EventArgs + { + /// + /// Gets the involved in the event. + /// + public Player Player { get; } + + /// + /// Gets the definition of the role. + /// + public ICustomRole Role { get; } + + /// + /// Gets the instance, if available. + /// May be null in removal events if the instance was already destroyed. + /// + public SummonedCustomRole SummonedRole { get; } + + public CustomRoleEventData(Player player, ICustomRole role, SummonedCustomRole summonedRole) + { + Player = player; + Role = role; + SummonedRole = summonedRole; + } + } + + /// + /// Data passed when a custom role player escapes. + /// + public class CustomRoleEscapeEventData : CustomRoleEventData + { + /// + /// Gets whether the player was cuffed/disarmed when escaping. + /// + public bool IsCuffed { get; } + + /// + /// Gets the new role the player is being assigned to after escaping. + /// May be null if the player receives a natural role change. + /// + public ICustomRole NewRole { get; } + + /// + /// Gets the of the escaping role, for convenience. + /// + public string CustomTeamId => Role?.CustomTeamId; + + public CustomRoleEscapeEventData(Player player, ICustomRole role, SummonedCustomRole summonedRole, bool isCuffed, ICustomRole newRole = null) + : base(player, role, summonedRole) + { + IsCuffed = isCuffed; + NewRole = newRole; + } + } + + /// + /// Fired after a player has been assigned a custom role and fully initialized. + /// + public static event EventHandler CustomRoleAssigned; + + /// + /// Fired just before a player's custom role is destroyed/removed. + /// + public static event EventHandler CustomRoleRemoved; + + /// + /// Fired when a player with a custom role successfully escapes. + /// + public static event EventHandler CustomRoleEscaped; + + internal static void RaiseCustomRoleAssigned(Player player, ICustomRole role, SummonedCustomRole summonedRole) + { + CustomRoleAssigned?.Invoke(null, new CustomRoleEventData(player, role, summonedRole)); + } + + internal static void RaiseCustomRoleRemoved(Player player, ICustomRole role, SummonedCustomRole summonedRole) + { + CustomRoleRemoved?.Invoke(null, new CustomRoleEventData(player, role, summonedRole)); + } + + internal static void RaiseCustomRoleEscaped(Player player, ICustomRole role, SummonedCustomRole summonedRole, bool isCuffed, ICustomRole newRole = null) + { + CustomRoleEscaped?.Invoke(null, new CustomRoleEscapeEventData(player, role, summonedRole, isCuffed, newRole)); + } + } +} diff --git a/UncomplicatedCustomRoles/API/Interfaces/ICustomRole.cs b/UncomplicatedCustomRoles/API/Interfaces/ICustomRole.cs index 75c14d6a..e14890d8 100644 --- a/UncomplicatedCustomRoles/API/Interfaces/ICustomRole.cs +++ b/UncomplicatedCustomRoles/API/Interfaces/ICustomRole.cs @@ -1,4 +1,4 @@ -/* +/* * This file is a part of the UncomplicatedCustomRoles project. * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) @@ -82,5 +82,12 @@ public interface ICustomRole public abstract List? CustomFlags { get; set; } public abstract bool IgnoreSpawnSystem { get; set; } + + /// + /// Gets or sets an optional custom team/faction identifier (e.g. "SerpentHand", "UIU"). + /// Used by EndConditionsExtension for custom win conditions and escape-based scoring. + /// When null or empty, the role uses the standard for round-end evaluation. + /// + public abstract string CustomTeamId { get; set; } } } \ No newline at end of file diff --git a/UncomplicatedCustomRoles/Events/PlayerEventHandler.cs b/UncomplicatedCustomRoles/Events/PlayerEventHandler.cs index ea4d41b2..dee5def6 100644 --- a/UncomplicatedCustomRoles/Events/PlayerEventHandler.cs +++ b/UncomplicatedCustomRoles/Events/PlayerEventHandler.cs @@ -1,4 +1,4 @@ -/* +/* * This file is a part of the UncomplicatedCustomRoles project. * * Copyright (c) 2023-present FoxWorn3365 (Federico Cosma) @@ -325,10 +325,15 @@ public void OnEscaping(PlayerEscapingEventArgs Escaping) return; } + bool isCuffed = Escaping.Player.IsDisarmed; + 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; + + // Raise escape event for extensions (natural escape, no new custom role) + UcrEvents.RaiseCustomRoleEscaped(Escaping.Player, summoned.Role, summoned, isCuffed); return; } @@ -346,6 +351,9 @@ public void OnEscaping(PlayerEscapingEventArgs Escaping) if (NewRole.Value is null) { Escaping.IsAllowed = true; + + // Raise escape event for extensions (natural escape) + UcrEvents.RaiseCustomRoleEscaped(Escaping.Player, summoned.Role, summoned, isCuffed); return; } @@ -358,6 +366,9 @@ public void OnEscaping(PlayerEscapingEventArgs Escaping) { Escaping.NewRole = role; Escaping.IsAllowed = true; + + // Raise escape event for extensions (escape to internal role) + UcrEvents.RaiseCustomRoleEscaped(Escaping.Player, summoned.Role, summoned, isCuffed); } } } @@ -378,6 +389,9 @@ public void OnEscaping(PlayerEscapingEventArgs Escaping) LogManager.Silent($"Successfully activated the call to method SpawnManager::SummonCustomSubclass(<...>) as the player is not inside the Escape::Bucket bucket! - Adding it..."); API.Features.Escape.Bucket.Add(Escaping.Player.PlayerId); SpawnManager.SummonCustomSubclass(Escaping.Player, role.Id); + + // Raise escape event for extensions (escape to custom role) + UcrEvents.RaiseCustomRoleEscaped(Escaping.Player, summoned.Role, summoned, isCuffed, role); } else LogManager.Silent($"Canceled call to method SpawnManager::SummonCustomSubclass(<...>) due to the presence of the player inside the Escape::Bucket! - Event already fired!");