Skip to content

Commit 1aeeb08

Browse files
authored
feat: Rework and Fix Custom Role Spawn Logic & Limits (#578)
* Update CustomRole.cs * Update CustomRoles.cs * Update PlayerHandlers.cs * Delete white space * Update PlayerHandlers.cs * Update CustomRole.cs * F' * Fix * Update PlayerHandlers.cs * Add Spawn reason Spawn reasons have been set to prevent custom role assignment in incorrect situation. * Update PlayerHandlers.cs * Update PlayerHandlers.cs * Update Extensions.cs * Update PlayerHandlers.cs * Update PlayerHandlers.cs * Update PlayerHandlers.cs * Update CustomRole.cs * Update PlayerHandlers.cs * Update CustomRole.cs * white space * Changed to thread safe * non blocking, thread safe implementation Refactored the player spawning logic to be lock free by using Interlocked for atomic counters and a ConcurrentDictionary for safe concurrent processing. * Update PlayerHandlers.cs * Update CustomRole.cs * Update CustomRole.cs * Delete log info * Update PlayerHandlers.cs * Update CustomRole.cs * rework real random and fair spawn position system * Update CustomRole.cs * Update CustomRole.cs * Update CustomRole.cs
1 parent f1d2007 commit 1aeeb08

4 files changed

Lines changed: 167 additions & 31 deletions

File tree

EXILED/Exiled.CustomRoles/API/Extensions.cs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,25 @@ public static ReadOnlyCollection<CustomRole> GetCustomRoles(this Player player)
4141
return roles.AsReadOnly();
4242
}
4343

44+
/// <summary>
45+
/// Checks whether the player has any custom role assigned.
46+
/// </summary>
47+
/// <param name="player">The <see cref="Player"/> to check.</param>
48+
/// <returns><c>true</c> if the player has at least one custom role; otherwise, <c>false</c>.</returns>
49+
public static bool HasAnyCustomRole(this Player player)
50+
{
51+
if (player == null)
52+
return false;
53+
54+
foreach (CustomRole role in CustomRole.Registered)
55+
{
56+
if (role.Check(player))
57+
return true;
58+
}
59+
60+
return false;
61+
}
62+
4463
/// <summary>
4564
/// Registers an <see cref="IEnumerable{T}"/> of <see cref="CustomRole"/>s.
4665
/// </summary>
@@ -105,4 +124,4 @@ public static void Unregister(this IEnumerable<CustomRole> customRoles)
105124
/// <returns>The <see cref="ActiveAbility"/> the <see cref="Player"/> has selected, or <see langword="null"/>.</returns>
106125
public static ActiveAbility? GetSelectedAbility(this Player player) => !ActiveAbility.AllActiveAbilities.TryGetValue(player, out HashSet<ActiveAbility> abilities) ? null : abilities.FirstOrDefault(a => a.Check(player, CheckType.Selected));
107126
}
108-
}
127+
}

EXILED/Exiled.CustomRoles/API/Features/CustomRole.cs

Lines changed: 50 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,14 @@ namespace Exiled.CustomRoles.API.Features
3737
/// </summary>
3838
public abstract class CustomRole
3939
{
40+
/// <summary>
41+
/// Gets or sets the number of players that naturally spawned with this custom role.
42+
/// </summary>
43+
[YamlIgnore]
44+
#pragma warning disable SA1401 // Fields should be private
45+
public int SpawnedPlayers = 0;
46+
#pragma warning restore SA1401 // Fields should be private
47+
4048
private const float AddRoleDelay = 0.25f;
4149

4250
private static Dictionary<Type, CustomRole?> typeLookupTable = new();
@@ -816,46 +824,68 @@ protected bool TryAddItem(Player player, string itemName)
816824
protected Vector3 GetSpawnPosition()
817825
{
818826
if (SpawnProperties is null || SpawnProperties.Count() == 0)
827+
{
819828
return Vector3.zero;
829+
}
830+
831+
float totalchance = 0f;
832+
List<(float chance, Vector3 pos)> spawnPointPool = new(4);
833+
834+
void Add(Vector3 pos, float chance)
835+
{
836+
if (chance <= 0f)
837+
return;
838+
839+
spawnPointPool.Add((chance, pos));
840+
totalchance += chance;
841+
}
842+
843+
if (!SpawnProperties.StaticSpawnPoints.IsEmpty())
844+
{
845+
foreach (StaticSpawnPoint sp in SpawnProperties.StaticSpawnPoints)
846+
{
847+
Add(sp.Position, sp.Chance);
848+
}
849+
}
820850

821-
if (SpawnProperties.StaticSpawnPoints.Count > 0)
851+
if (!SpawnProperties.DynamicSpawnPoints.IsEmpty())
822852
{
823-
foreach ((float chance, Vector3 pos) in SpawnProperties.StaticSpawnPoints)
853+
foreach (DynamicSpawnPoint sp in SpawnProperties.DynamicSpawnPoints)
824854
{
825-
double r = Loader.Random.NextDouble() * 100;
826-
if (r <= chance)
827-
return pos;
855+
Add(sp.Position, sp.Chance);
828856
}
829857
}
830858

831-
if (SpawnProperties.DynamicSpawnPoints.Count > 0)
859+
if (!SpawnProperties.RoleSpawnPoints.IsEmpty())
832860
{
833-
foreach ((float chance, Vector3 pos) in SpawnProperties.DynamicSpawnPoints)
861+
foreach (RoleSpawnPoint sp in SpawnProperties.RoleSpawnPoints)
834862
{
835-
double r = Loader.Random.NextDouble() * 100;
836-
if (r <= chance)
837-
return pos;
863+
Add(sp.Position, sp.Chance);
838864
}
839865
}
840866

841-
if (SpawnProperties.RoleSpawnPoints.Count > 0)
867+
if (!SpawnProperties.RoomSpawnPoints.IsEmpty())
842868
{
843-
foreach ((float chance, Vector3 pos) in SpawnProperties.RoleSpawnPoints)
869+
foreach (RoomSpawnPoint sp in SpawnProperties.RoomSpawnPoints)
844870
{
845-
double r = Loader.Random.NextDouble() * 100;
846-
if (r <= chance)
847-
return pos;
871+
Add(sp.Position, sp.Chance);
848872
}
849873
}
850874

851-
if (SpawnProperties.RoomSpawnPoints.Count > 0)
875+
if (spawnPointPool.Count == 0 || totalchance <= 0f)
876+
{
877+
return Vector3.zero;
878+
}
879+
880+
float randomRoll = (float)(Loader.Random.NextDouble() * totalchance);
881+
foreach ((float chance, Vector3 pos) in spawnPointPool)
852882
{
853-
foreach ((float chance, Vector3 pos) in SpawnProperties.RoomSpawnPoints)
883+
if (randomRoll < chance)
854884
{
855-
double r = Loader.Random.NextDouble() * 100;
856-
if (r <= chance)
857-
return pos;
885+
return pos;
858886
}
887+
888+
randomRoll -= chance;
859889
}
860890

861891
return Vector3.zero;
@@ -869,7 +899,6 @@ protected virtual void SubscribeEvents()
869899
Log.Debug($"{Name}: Loading events.");
870900
Exiled.Events.Handlers.Player.ChangingNickname += OnInternalChangingNickname;
871901
Exiled.Events.Handlers.Player.ChangingRole += OnInternalChangingRole;
872-
Exiled.Events.Handlers.Player.Spawned += OnInternalSpawned;
873902
Exiled.Events.Handlers.Player.SpawningRagdoll += OnSpawningRagdoll;
874903
Exiled.Events.Handlers.Player.Destroying += OnDestroying;
875904
}
@@ -885,7 +914,6 @@ protected virtual void UnsubscribeEvents()
885914
Log.Debug($"{Name}: Unloading events.");
886915
Exiled.Events.Handlers.Player.ChangingNickname -= OnInternalChangingNickname;
887916
Exiled.Events.Handlers.Player.ChangingRole -= OnInternalChangingRole;
888-
Exiled.Events.Handlers.Player.Spawned -= OnInternalSpawned;
889917
Exiled.Events.Handlers.Player.SpawningRagdoll -= OnSpawningRagdoll;
890918
Exiled.Events.Handlers.Player.Destroying -= OnDestroying;
891919
}
@@ -924,12 +952,6 @@ private void OnInternalChangingNickname(ChangingNicknameEventArgs ev)
924952
ev.Player.CustomInfo = $"{ev.NewName}\n{CustomInfo}";
925953
}
926954

927-
private void OnInternalSpawned(SpawnedEventArgs ev)
928-
{
929-
if (!IgnoreSpawnSystem && SpawnChance > 0 && !Check(ev.Player) && ev.Player.Role.Type == Role && Loader.Random.NextDouble() * 100 <= SpawnChance)
930-
AddRole(ev.Player);
931-
}
932-
933955
private void OnInternalChangingRole(ChangingRoleEventArgs ev)
934956
{
935957
if (ev.IsAllowed && ev.Reason != SpawnReason.Destroyed && Check(ev.Player) && ((ev.NewRole == RoleTypeId.Spectator && !KeepRoleOnDeath) || (ev.NewRole != RoleTypeId.Spectator && !KeepRoleOnChangingRole)))

EXILED/Exiled.CustomRoles/CustomRoles.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,16 +62,24 @@ public override void OnEnabled()
6262

6363
if (Config.UseKeypressActivation)
6464
keypressActivator = new();
65+
66+
Exiled.Events.Handlers.Player.Spawned += playerHandlers.OnSpawned;
6567
Exiled.Events.Handlers.Player.SpawningRagdoll += playerHandlers.OnSpawningRagdoll;
68+
69+
Exiled.Events.Handlers.Server.WaitingForPlayers += playerHandlers.OnWaitingForPlayers;
6670
base.OnEnabled();
6771
}
6872

6973
/// <inheritdoc/>
7074
public override void OnDisabled()
7175
{
76+
Exiled.Events.Handlers.Player.Spawned -= playerHandlers!.OnSpawned;
7277
Exiled.Events.Handlers.Player.SpawningRagdoll -= playerHandlers!.OnSpawningRagdoll;
78+
79+
Exiled.Events.Handlers.Server.WaitingForPlayers -= playerHandlers!.OnWaitingForPlayers;
80+
7381
keypressActivator = null;
7482
base.OnDisabled();
7583
}
7684
}
77-
}
85+
}

EXILED/Exiled.CustomRoles/Events/PlayerHandlers.cs

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@
77

88
namespace Exiled.CustomRoles.Events
99
{
10+
using System;
11+
using System.Collections.Generic;
12+
using System.Threading;
13+
14+
using Exiled.API.Enums;
15+
using Exiled.API.Features;
16+
using Exiled.CustomRoles.API;
17+
using Exiled.CustomRoles.API.Features;
1018
using Exiled.Events.EventArgs.Player;
1119

1220
/// <summary>
@@ -15,6 +23,15 @@ namespace Exiled.CustomRoles.Events
1523
public class PlayerHandlers
1624
{
1725
private readonly CustomRoles plugin;
26+
private readonly HashSet<SpawnReason> validSpawnReasons = new()
27+
{
28+
SpawnReason.RoundStart,
29+
SpawnReason.Respawn,
30+
SpawnReason.LateJoin,
31+
SpawnReason.Revived,
32+
SpawnReason.Escaped,
33+
SpawnReason.ItemUsage,
34+
};
1835

1936
/// <summary>
2037
/// Initializes a new instance of the <see cref="PlayerHandlers"/> class.
@@ -25,6 +42,15 @@ public PlayerHandlers(CustomRoles plugin)
2542
this.plugin = plugin;
2643
}
2744

45+
/// <inheritdoc cref="Exiled.Events.Handlers.Server.WaitingForPlayers"/>
46+
internal void OnWaitingForPlayers()
47+
{
48+
foreach (CustomRole role in CustomRole.Registered)
49+
{
50+
role.SpawnedPlayers = 0;
51+
}
52+
}
53+
2854
/// <inheritdoc cref="Exiled.Events.Handlers.Player.SpawningRagdoll"/>
2955
internal void OnSpawningRagdoll(SpawningRagdollEventArgs ev)
3056
{
@@ -34,5 +60,66 @@ internal void OnSpawningRagdoll(SpawningRagdollEventArgs ev)
3460
plugin.StopRagdollPlayers.Remove(ev.Player);
3561
}
3662
}
63+
64+
/// <inheritdoc cref="Exiled.Events.Handlers.Player.Spawning"/>
65+
internal void OnSpawned(SpawnedEventArgs ev)
66+
{
67+
if (!validSpawnReasons.Contains(ev.Reason) || ev.Player.HasAnyCustomRole())
68+
{
69+
return;
70+
}
71+
72+
float totalChance = 0f;
73+
List<CustomRole> eligibleRoles = new(8);
74+
75+
foreach (CustomRole role in CustomRole.Registered)
76+
{
77+
if (role.Role == ev.Player.Role.Type && !role.IgnoreSpawnSystem && role.SpawnChance > 0 && !role.Check(ev.Player) && (role.SpawnProperties is null || role.SpawnedPlayers < role.SpawnProperties.Limit))
78+
{
79+
eligibleRoles.Add(role);
80+
totalChance += role.SpawnChance;
81+
}
82+
}
83+
84+
if (eligibleRoles.Count == 0)
85+
{
86+
return;
87+
}
88+
89+
float lotterySize = Math.Max(100f, totalChance);
90+
float randomRoll = (float)Loader.Loader.Random.NextDouble() * lotterySize;
91+
92+
if (randomRoll >= totalChance)
93+
{
94+
return;
95+
}
96+
97+
foreach (CustomRole candidateRole in eligibleRoles)
98+
{
99+
if (randomRoll >= candidateRole.SpawnChance)
100+
{
101+
randomRoll -= candidateRole.SpawnChance;
102+
continue;
103+
}
104+
105+
if (candidateRole.SpawnProperties is null)
106+
{
107+
candidateRole.AddRole(ev.Player);
108+
break;
109+
}
110+
111+
int newSpawnCount = Interlocked.Increment(ref candidateRole.SpawnedPlayers);
112+
if (newSpawnCount <= candidateRole.SpawnProperties.Limit)
113+
{
114+
candidateRole.AddRole(ev.Player);
115+
break;
116+
}
117+
else
118+
{
119+
Interlocked.Decrement(ref candidateRole.SpawnedPlayers);
120+
randomRoll -= candidateRole.SpawnChance;
121+
}
122+
}
123+
}
37124
}
38-
}
125+
}

0 commit comments

Comments
 (0)