Add Template, more custom classes & fix issues

This commit is contained in:
code002lover 2025-06-06 12:22:27 +02:00
parent c9bee028d9
commit 73a4da1edd
9 changed files with 466 additions and 53 deletions

View File

@ -1,5 +1,6 @@
using Interactables.Interobjects.DoorUtils;
using InventorySystem.Items;
using InventorySystem.Items.Firearms.Modules;
using LabApi.Events.Arguments.PlayerEvents;
using LabApi.Events.Arguments.ServerEvents;
using LabApi.Events.Handlers;
@ -28,11 +29,30 @@ public class CustomClasses : Plugin
public JanitorConfig JanitorConfig { get; set; } = new();
public ResearchSubjectConfig ResearchSubjectConfig { get; set; } = new();
public HeadGuardConfig HeadGuardConfig { get; set; } = new();
public MedicConfig MedicConfig { get; set; } = new();
public GamblerConfig GamblerConfig { get; set; } = new();
public override void Enable()
{
PlayerEvents.Spawned += OnPlayerSpawned;
ServerEvents.RoundEnded += OnRoundEnded;
Scp914Events.ProcessingPickup += ev =>
{
if (ev.Pickup.Type < ItemType.KeycardCustomTaskForce) return;
//process custom upgrade
var keycard = (KeycardPickup)ev.Pickup;
var pickup = Pickup.Create(ItemType.KeycardMTFCaptain, keycard.Position);
keycard.Destroy();
pickup!.Spawn();
};
Scp914Events.ProcessingInventoryItem += ev =>
{
if (ev.Item.Type < ItemType.KeycardCustomTaskForce) return;
//process custom upgrade
var keycard = (KeycardItem)ev.Item;
ev.Player.RemoveItem(keycard);
ev.Player.AddItem(ItemType.KeycardMTFCaptain, ItemAddReason.Scp914Upgrade);
};
}
public override void Disable()
@ -51,6 +71,8 @@ public class CustomClasses : Plugin
if (_classManager.TryHandleSpawn(ev.Player, JanitorConfig, typeof(JanitorConfig))) return;
if (_classManager.TryHandleSpawn(ev.Player, ResearchSubjectConfig, typeof(ResearchSubjectConfig))) return;
if (_classManager.TryHandleSpawn(ev.Player, HeadGuardConfig, typeof(HeadGuardConfig))) return;
if (_classManager.TryHandleSpawn(ev.Player, MedicConfig, typeof(MedicConfig))) return;
if (_classManager.TryHandleSpawn(ev.Player, GamblerConfig, typeof(GamblerConfig))) return;
}
}
@ -66,7 +88,9 @@ public class CustomClassManager
// Register handlers
RegisterHandler<JanitorConfig>(new JanitorHandler(this));
RegisterHandler<ResearchSubjectConfig>(new ResearchSubjectHandler(this));
RegisterHandler<HeadGuardConfig>(new HeadGuardHandler(this));
RegisterHandler<HeadGuardConfig>(new HeadGuardHandler());
RegisterHandler<MedicConfig>(new MedicHandler());
RegisterHandler<GamblerConfig>(new GamblerHandler());
}
public void TeleportPlayerToAround(Player player, Vector3 position)
@ -117,90 +141,121 @@ public class CustomClassManager
Logger.Debug($"Player spawning {configType} - {player.Nickname} - {state.Spawns} / {config.MaxSpawns}");
if (_handlers.TryGetValue(configType, out var handler)) return handler.HandleSpawn(player, config, _random);
if (!_handlers.TryGetValue(configType, out var handler)) return false;
Timing.CallDelayed(0.5f, () => { handler.HandleSpawn(player, config, _random); });
return true;
}
return false;
}
}
public interface ICustomClassHandler
{
bool HandleSpawn(Player player, CustomClassConfig config, Random random);
void HandleSpawn(Player player, CustomClassConfig config, Random random);
}
public class JanitorHandler(CustomClassManager manager) : ICustomClassHandler
{
public bool HandleSpawn(Player player, CustomClassConfig config, Random random)
public void HandleSpawn(Player player, CustomClassConfig config, Random random)
{
var scp914 = Map.Rooms.First(r => r.Name == RoomName.Lcz914);
Timing.CallDelayed(0.5f, () =>
manager.TeleportPlayerToAround(player, scp914.Position);
foreach (var spawnItem in config.Items)
{
manager.TeleportPlayerToAround(player, scp914.Position);
player.AddItem(spawnItem, ItemAddReason.StartingItem);
Logger.Debug($"Gave player {player.Nickname} spawn item {spawnItem}");
}
foreach (var spawnItem in config.Items)
{
player.AddItem(spawnItem, ItemAddReason.StartingItem);
Logger.Debug($"Gave player {player.Nickname} spawn item {spawnItem}");
}
player.SendBroadcast("You're a <color=#A0A0A0>Janitor</color>!", 3);
});
return true;
player.SendBroadcast("You're a <color=#A0A0A0>Janitor</color>!", 3);
}
}
public class ResearchSubjectHandler(CustomClassManager manager) : ICustomClassHandler
{
public bool HandleSpawn(Player player, CustomClassConfig config, Random random)
public void HandleSpawn(Player player, CustomClassConfig config, Random random)
{
var scientist = Player.ReadyList.First(p => p.Role == RoleTypeId.Scientist);
Timing.CallDelayed(0.5f, () =>
manager.TeleportPlayerToAround(player, scientist.Position);
foreach (var spawnItem in config.Items)
{
manager.TeleportPlayerToAround(player, scientist.Position);
player.AddItem(spawnItem, ItemAddReason.StartingItem);
Logger.Debug($"Gave player {player.Nickname} spawn item {spawnItem}");
}
foreach (var spawnItem in config.Items)
{
player.AddItem(spawnItem, ItemAddReason.StartingItem);
Logger.Debug($"Gave player {player.Nickname} spawn item {spawnItem}");
}
player.SendBroadcast("You're a <color=#944710>Research Subject</color>!", 3);
});
return true;
player.SendBroadcast("You're a <color=#944710>Research Subject</color>!", 3);
}
}
public class HeadGuardHandler(CustomClassManager manager) : ICustomClassHandler
public class HeadGuardHandler : ICustomClassHandler
{
public bool HandleSpawn(Player player, CustomClassConfig config, Random random)
public void HandleSpawn(Player player, CustomClassConfig config, Random random)
{
Timing.CallDelayed(0.5f, () =>
player.RemoveItem(ItemType.KeycardGuard);
KeycardItem.CreateCustomKeycardTaskForce(player, "Head Guard Keycard", $"HG. {player.Nickname}",
new KeycardLevels(1, 1, 2), Color.blue, Color.cyan, "1", 0);
player.AddItem(ItemType.Adrenaline, ItemAddReason.StartingItem);
player.RemoveItem(ItemType.ArmorLight);
player.AddItem(ItemType.ArmorCombat, ItemAddReason.StartingItem);
player.RemoveItem(ItemType.GunFSP9);
var pickup = Pickup.Create(ItemType.GunCrossvec, Vector3.one);
var firearm = (FirearmPickup)pickup;
if (firearm != null)
{
player.RemoveItem(ItemType.KeycardGuard);
firearm.Base.Template.TryGetModule(out MagazineModule magazine);
magazine.ServerSetInstanceAmmo(firearm.Base.Template.ItemSerial, magazine.AmmoMax);
}
else
{
Logger.Error("Failed to get firearm from pickup");
}
KeycardItem.CreateCustomKeycardTaskForce(player, "Head Guard Keycard", $"HG. {player.Nickname}",
new KeycardLevels(1, 1, 2), Color.blue, Color.cyan, "1", 0);
if (pickup != null) player.AddItem(pickup);
player.AddItem(ItemType.Adrenaline, ItemAddReason.StartingItem);
player.SetAmmo(ItemType.Ammo9x19, 120);
player.RemoveItem(ItemType.ArmorLight);
player.AddItem(ItemType.ArmorCombat, ItemAddReason.StartingItem);
player.SendBroadcast("You're a <color=#00B7EB>Head Guard</color>!", 3);
}
}
player.RemoveItem(ItemType.GunFSP9);
public class MedicHandler : ICustomClassHandler
{
public void HandleSpawn(Player player, CustomClassConfig config, Random random)
{
foreach (var spawnItem in config.Items)
{
player.AddItem(spawnItem, ItemAddReason.StartingItem);
Logger.Debug($"Gave player {player.Nickname} spawn item {spawnItem}");
}
var pickup = Pickup.Create(ItemType.GunCrossvec, Vector3.one);
player.SendBroadcast("You're a <color=#727472>Medic</color>!", 3);
}
}
if (pickup != null) player.AddItem(pickup);
public abstract class SimpleAddItemHandler : ICustomClassHandler
{
public virtual void HandleSpawn(Player player, CustomClassConfig config, Random random)
{
foreach (var spawnItem in config.Items)
{
player.AddItem(spawnItem, ItemAddReason.StartingItem);
Logger.Debug($"Gave player {player.Nickname} spawn item {spawnItem}");
}
}
}
player.SetAmmo(ItemType.Ammo9x19, 120);
player.SendBroadcast("You're a <color=#00B7EB>Head Guard</color>!", 3);
});
return true;
public class GamblerHandler : SimpleAddItemHandler
{
public override void HandleSpawn(Player player, CustomClassConfig config, Random random)
{
base.HandleSpawn(player, config, random);
player.SendBroadcast("You're a <color=#FF9966>Gambler</color>!", 3);
}
}
@ -233,3 +288,19 @@ public class HeadGuardConfig : CustomClassConfig
public override int MinPlayers { get; set; } = 9;
public override RoleTypeId RequiredRole { get; set; } = RoleTypeId.FacilityGuard;
}
public class MedicConfig : CustomClassConfig
{
public override int MinPlayers { get; set; } = 5;
public override double ChancePerPlayer { get; set; } = 0.3;
public override int MaxSpawns { get; set; } = 1;
public override ItemType[] Items { get; set; } = [ItemType.Medkit, ItemType.Adrenaline];
public override RoleTypeId RequiredRole { get; set; } = RoleTypeId.Scientist;
}
public class GamblerConfig : CustomClassConfig
{
public override double ChancePerPlayer { get; set; } = 0.3;
public override int MaxSpawns { get; set; } = 5;
public override ItemType[] Items { get; set; } = [ItemType.Coin, ItemType.Coin];
}

161
LobbyGame/LobbyGame.cs Normal file
View File

@ -0,0 +1,161 @@
using CommandSystem.Commands.RemoteAdmin;
using GameCore;
using LabApi.Events.Arguments.ServerEvents;
using LabApi.Events.Handlers;
using LabApi.Features;
using LabApi.Loader.Features.Plugins;
using MEC;
using Interactables.Interobjects.DoorUtils;
using LabApi.Events.Arguments.PlayerEvents;
using LabApi.Features.Wrappers;
using MapGeneration;
using PlayerRoles;
using UnityEngine;
using Logger = LabApi.Features.Console.Logger;
using Version = System.Version;
namespace LobbyGame
{
public class LobbyGame: Plugin
{
public override string Name => "LobbyGame";
public override string Author => "Code002Lover";
public override Version Version { get; } = new(1, 0, 0);
public override string Description => "Adds a lobby minigame";
public override Version RequiredApiVersion { get; } = new(LabApiProperties.CompiledVersion);
public int RoundTimer { get; set; } = 20;
public static LobbyGame Singleton { get; private set; }
private bool _isStarted;
private Room _randomRoom;
public override void Enable()
{
ServerEvents.WaitingForPlayers += WaitingForPlayers;
PlayerEvents.Joined += PlayerJoined;
Singleton = this;
}
private static void PlayerJoined(PlayerJoinedEventArgs ev)
{
Timing.RunCoroutine(ContinuouslyTrySpawning());
return;
IEnumerator<float> ContinuouslyTrySpawning()
{
while (!RoundStart.RoundStarted)
{
GameObject.Find("StartRound").transform.localScale = Vector3.zero;
yield return Timing.WaitForSeconds(0.5f);
if(Singleton._randomRoom == null) continue;
if(!Singleton._isStarted) continue;
ev.Player.SetRole(RoleTypeId.ChaosRifleman, RoleChangeReason.None, RoleSpawnFlags.None);
ev.Player.Position = Singleton._randomRoom.Position + new Vector3(0, 1, 0);
break;
}
}
}
private static void WaitingForPlayers()
{
Timing.CallDelayed(15, () =>
{
if (Singleton._isStarted) return;
var randomRoom = Map.GetRandomRoom(FacilityZone.Entrance);
while (randomRoom is not { Zone: FacilityZone.Entrance })
{
randomRoom = Map.GetRandomRoom(FacilityZone.Entrance);
}
Logger.Debug($"Random entrance room: {randomRoom.Name}");
RoundStart.LobbyLock = true;
Singleton._randomRoom = randomRoom;
Singleton._isStarted = true;
foreach (var player in Player.ReadyList)
{
player.SetRole(RoleTypeId.ChaosRifleman, RoleChangeReason.None, RoleSpawnFlags.None);
try
{
player.Position = randomRoom.Position + new Vector3(0, 1, 0);
}
catch (Exception _)
{
player.SetRole(RoleTypeId.Spectator);
}
}
Timing.RunCoroutine(ContinuouslyUpdateRoundTimer());
GameObject.Find("StartRound").transform.localScale = Vector3.zero;
foreach (var door in Map.Doors)
{
door.IsOpened = false;
door.Lock(DoorLockReason.Lockdown2176, true);
}
return;
IEnumerator<float> ContinuouslyUpdateRoundTimer()
{
while (true)
{
yield return Timing.WaitForSeconds(1);
foreach (var player in Player.ReadyList)
{
player.SendBroadcast(
$"<size=30><color=grey>Round starts in</color> <color=red>{Singleton.RoundTimer}</color> <color=grey>seconds</color></size>",
10, Broadcast.BroadcastFlags.Normal, true);
}
Logger.Debug($"Round starts in {Singleton.RoundTimer} seconds");
if (Player.ReadyList.Count() <= 1)
{
Singleton.RoundTimer = 20;
continue;
}
Singleton.RoundTimer--;
if (Singleton.RoundTimer >= -1) continue;
Singleton.RoundTimer = 20;
foreach (var player in Player.ReadyList)
{
player.ClearInventory();
player.SetRole(RoleTypeId.Spectator);
}
foreach (var door in Map.Doors)
{
door.Lock(DoorLockReason.Lockdown2176, false);
}
foreach (var player in Player.ReadyList)
{
player.SendBroadcast(
"",
1, Broadcast.BroadcastFlags.Normal, true);
}
CharacterClassManager.ForceRoundStart();
break;
}
}
});
}
public override void Disable()
{
ServerEvents.WaitingForPlayers -= WaitingForPlayers;
PlayerEvents.Joined -= PlayerJoined;
Singleton = null;
}
}
}

View File

@ -0,0 +1,57 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net48</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>disable</Nullable>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<Optimize>true</Optimize>
<CheckForOverflowUnderflow>true</CheckForOverflowUnderflow>
<DebugType>full</DebugType>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<Optimize>true</Optimize>
<CheckForOverflowUnderflow>false</CheckForOverflowUnderflow>
<DebugType>none</DebugType>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Northwood.LabAPI" Version="1.0.2"/>
</ItemGroup>
<ItemGroup>
<Reference Include="0Harmony">
<HintPath>..\dependencies\0Harmony.dll</HintPath>
</Reference>
<Reference Include="Assembly-CSharp">
<HintPath>..\dependencies\Assembly-CSharp.dll</HintPath>
</Reference>
<Reference Include="Assembly-CSharp-firstpass">
<HintPath>..\dependencies\Assembly-CSharp-firstpass.dll</HintPath>
</Reference>
<Reference Include="CommandSystem.Core">
<HintPath>..\dependencies\CommandSystem.Core.dll</HintPath>
</Reference>
<Reference Include="HintServiceMeow">
<HintPath>..\dependencies\HintServiceMeow-LabAPI.dll</HintPath>
</Reference>
<Reference Include="Mirror">
<HintPath>..\dependencies\Mirror.dll</HintPath>
</Reference>
<Reference Include="NorthwoodLib">
<HintPath>..\dependencies\NorthwoodLib.dll</HintPath>
</Reference>
<Reference Include="Pooling">
<HintPath>..\dependencies\Pooling.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.CoreModule">
<HintPath>..\dependencies\UnityEngine.CoreModule.dll</HintPath>
</Reference>
</ItemGroup>
</Project>

View File

@ -6,11 +6,11 @@ using LabApi.Events.Handlers;
using LabApi.Features;
using LabApi.Features.Console;
using LabApi.Features.Wrappers;
using MEC;
using PlayerRoles;
using PlayerRoles.PlayableScps.Scp079;
using PlayerRoles.PlayableScps.Scp096;
using PlayerRoles.PlayableScps.Scp3114;
using Timer = System.Timers.Timer;
namespace SCPTeamHint;
@ -19,6 +19,7 @@ public class Plugin : LabApi.Loader.Features.Plugins.Plugin
private readonly object _hintsLock = new();
private readonly Dictionary<Player, Hint> _spectatorHints = new();
private Timer _timer;
public override string Name => "SCPTeamHint";
public override string Author => "HoherGeist, Code002Lover";
public override Version Version { get; } = new(1, 0, 0);
@ -31,13 +32,18 @@ public class Plugin : LabApi.Loader.Features.Plugins.Plugin
PlayerEvents.Joined += OnJoin;
PlayerEvents.Left += OnLeft;
Timing.CallContinuously(1, UpdateHints);
_timer = new Timer(1000);
_timer.Elapsed += (_, _) => UpdateHints();
_timer.Start();
}
public override void Disable()
{
PlayerEvents.Joined -= OnJoin;
PlayerEvents.Left -= OnLeft;
_timer?.Stop();
_timer?.Dispose();
_timer = null;
}
private void UpdateHints()

View File

@ -34,6 +34,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ServerHints", "ServerHints\
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GrowingZombies", "GrowingZombies\GrowingZombies.csproj", "{5751F8D6-7A8D-4C2C-B7E9-A8A3DB324329}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TemplateProject", "TemplateProject\TemplateProject.csproj", "{E5A28D1C-638F-4849-9784-240D50A6DA29}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LobbyGame", "LobbyGame\LobbyGame.csproj", "{E02243D5-0229-47BB-88A7-252EC753C8CC}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -108,5 +112,13 @@ Global
{5751F8D6-7A8D-4C2C-B7E9-A8A3DB324329}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5751F8D6-7A8D-4C2C-B7E9-A8A3DB324329}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5751F8D6-7A8D-4C2C-B7E9-A8A3DB324329}.Release|Any CPU.Build.0 = Release|Any CPU
{E5A28D1C-638F-4849-9784-240D50A6DA29}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E5A28D1C-638F-4849-9784-240D50A6DA29}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E5A28D1C-638F-4849-9784-240D50A6DA29}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E5A28D1C-638F-4849-9784-240D50A6DA29}.Release|Any CPU.Build.0 = Release|Any CPU
{E02243D5-0229-47BB-88A7-252EC753C8CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E02243D5-0229-47BB-88A7-252EC753C8CC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E02243D5-0229-47BB-88A7-252EC753C8CC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E02243D5-0229-47BB-88A7-252EC753C8CC}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

View File

@ -0,0 +1,13 @@
{
"author": "Code002Lover",
"name": "SCP:SL LabAPI template",
"description": "LabAPI basic template for own use",
"identity": "Code002Lover.LabAPI.1.0",
"shortName": "labapi",
"tags": {
"language": "C#",
"type": "project"
},
"sourceName": "TemplateProject"
}

View File

@ -0,0 +1,24 @@
using LabApi.Features;
using LabApi.Loader.Features.Plugins;
namespace TemplateProject
{
public class TemplateProject: Plugin
{
public override string Name => "TemplateProject";
public override string Author => "Code002Lover";
public override Version Version { get; } = new(1, 0, 0);
public override string Description => "Is a template for creating plugins. It does nothing.";
public override Version RequiredApiVersion { get; } = new(LabApiProperties.CompiledVersion);
public override void Enable()
{
}
public override void Disable()
{
}
}
}

View File

@ -0,0 +1,62 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net48</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>disable</Nullable>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<Optimize>true</Optimize>
<CheckForOverflowUnderflow>true</CheckForOverflowUnderflow>
<DebugType>full</DebugType>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<Optimize>true</Optimize>
<CheckForOverflowUnderflow>false</CheckForOverflowUnderflow>
<DebugType>none</DebugType>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Northwood.LabAPI" Version="1.0.2"/>
</ItemGroup>
<ItemGroup>
<Reference Include="0Harmony">
<HintPath>..\dependencies\0Harmony.dll</HintPath>
</Reference>
<Reference Include="Assembly-CSharp">
<HintPath>..\dependencies\Assembly-CSharp.dll</HintPath>
</Reference>
<Reference Include="Assembly-CSharp-firstpass">
<HintPath>..\dependencies\Assembly-CSharp-firstpass.dll</HintPath>
</Reference>
<Reference Include="CommandSystem.Core">
<HintPath>..\dependencies\CommandSystem.Core.dll</HintPath>
</Reference>
<Reference Include="HintServiceMeow">
<HintPath>..\dependencies\HintServiceMeow-LabAPI.dll</HintPath>
</Reference>
<Reference Include="Mirror">
<HintPath>..\dependencies\Mirror.dll</HintPath>
</Reference>
<Reference Include="NorthwoodLib">
<HintPath>..\dependencies\NorthwoodLib.dll</HintPath>
</Reference>
<Reference Include="Pooling">
<HintPath>..\dependencies\Pooling.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.CoreModule">
<HintPath>..\dependencies\UnityEngine.CoreModule.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<Content Include=".template.config\template.json" />
</ItemGroup>
</Project>

View File

@ -7,8 +7,8 @@ using LabApi.Features;
using LabApi.Features.Console;
using LabApi.Features.Wrappers;
using LabApi.Loader.Features.Plugins;
using MEC;
using PlayerRoles;
using Timer = System.Timers.Timer;
namespace VisibleSpectators;
@ -51,6 +51,7 @@ public class Plugin : Plugin<SpectatorConfig>
};
private readonly Dictionary<Player, Hint> _spectatorHints = new();
private Timer _timer;
public override string Name => "VisibleSpectators";
public override string Author => "Code002Lover";
public override Version Version { get; } = new(1, 0, 0);
@ -67,13 +68,19 @@ public class Plugin : Plugin<SpectatorConfig>
PlayerEvents.ChangedSpectator += OnSpectate;
PlayerEvents.Joined += OnJoin;
Timing.CallContinuously(1, UpdateSpectators);
_timer = new Timer(1000);
_timer.Elapsed += (_, _) => UpdateSpectators();
_timer.Start();
}
public override void Disable()
{
Logger.Debug("unloading...");
_timer.Stop();
_timer.Dispose();
_timer = null;
PlayerEvents.Joined -= OnJoin;
PlayerEvents.ChangedSpectator -= OnSpectate;