This commit is contained in:
code002lover 2025-05-22 18:51:47 +02:00
commit 840ddbcd47
12 changed files with 858 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
bin/
.idea/
obj/
*.user
*.dll

View File

@ -0,0 +1,13 @@
{
"version": 1,
"isRoot": true,
"tools": {
"csharpier": {
"version": "1.0.1",
"commands": [
"csharpier"
],
"rollForward": false
}
}
}

View File

@ -0,0 +1,48 @@
using LabApi.Events.Handlers;
using LabApi.Features;
using LabApi.Features.Console;
using LabApi.Loader;
namespace GamblingCoin
{
public class Plugin : LabApi.Loader.Features.Plugins.Plugin
{
public override string Name => "GamblingCoin";
public override string Author => "Code002Lover";
public override Version Version { get; } = new(1, 0, 0);
public override string Description => "Gamble your life away";
public override Version RequiredApiVersion { get; } = new (LabApiProperties.CompiledVersion);
public GamblingCoinGameplayConfig ConfigGameplay;
public GamblingCoinMessages ConfigMessages;
public GamblingCoinChancesConfig ConfigChances;
public override void LoadConfigs()
{
base.LoadConfigs();
ConfigGameplay = this.LoadConfig< GamblingCoinGameplayConfig > ("gameplay.yml");
ConfigMessages = this.LoadConfig< GamblingCoinMessages > ("messages.yml");
ConfigChances = this.LoadConfig< GamblingCoinChancesConfig > ("chances.yml");
}
public static Plugin Singleton;
private GamblingCoinEventHandler _eventHandler;
public override void Enable()
{
Logger.Debug("starting...");
Singleton = this;
_eventHandler = new GamblingCoinEventHandler();
PlayerEvents.FlippedCoin += _eventHandler.OnFlippedCoin;
}
public override void Disable()
{
Logger.Debug("unloading...");
Singleton = null;
PlayerEvents.FlippedCoin -= _eventHandler.OnFlippedCoin;
}
}
}

View File

@ -0,0 +1,37 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net48</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>disable</Nullable>
<LangVersion>10</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>
<Reference Include="Assembly-CSharp">
<HintPath>..\..\.local\share\Steam\steamapps\common\SCP Secret Laboratory Dedicated Server\SCPSL_Data\Managed\Assembly-CSharp.dll</HintPath>
</Reference>
<Reference Include="Mirror">
<HintPath>..\..\.local\share\Steam\steamapps\common\SCP Secret Laboratory Dedicated Server\SCPSL_Data\Managed\Mirror.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.CoreModule">
<HintPath>..\..\.local\share\Steam\steamapps\common\SCP Secret Laboratory Dedicated Server\SCPSL_Data\Managed\UnityEngine.CoreModule.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Northwood.LabAPI" Version="1.0.2" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,182 @@
using CustomPlayerEffects;
namespace GamblingCoin
{
public class GamblingCoinChancesConfig
{
public int NukeChance { get; set; } = 20;
public int SpawnWaveChance { get; set; } = 150;
public int CommonItemChance { get; set; } = 600;
public int UncommonItemChance { get; set; } = 400;
public int RareItemChance { get; set; } = 250;
public int EpicItemChance { get; set; } = 100;
public int LegendaryItemChance { get; set; } = 30;
public int RandomTeleportChance { get; set; } = 200;
public int StealItemChance { get; set; } = 100;
public int ExplosionChance { get; set; } = 15;
public int AntiMicroChance { get; set; } = 10;
public int GrenadeChance { get; set; } = 50;
public int PocketDimensionChance { get; set; } = 75;
public int SwitchInventoryChance { get; set; } = 150;
public int PositiveEffectChance { get; set; } = 300;
public int NegativeEffectChance { get; set; } = 250;
public int AdvancedPositiveEffectChance { get; set; } = 150;
public int AdvancedNegativeEffectChance { get; set; } = 250;
}
public class GamblingCoinMessages
{
public String SpawnWaveMessage { get; set; } = "Did someone just enter the Site...?";
public string ItemSpawnMessage { get; set; } = "*plop*";
public string UncommonItemSpawnMessage { get; set; } = "*bump*";
public string RareItemSpawnMessage { get; set; } = "*badunk*";
public string EpicItemSpawnMessage { get; set; } = "*katong*";
public string LegendaryItemSpawnMessage { get; set; } = "*sounds of monetary loss*";
public string RandomTeleportMessage { get; set; } = "Where did you go?";
public string StealItemMessage { get; set; } = "Be careful, the coin is slippery!";
public string ExplosionMessage { get; set; } = "How did you even die '-'";
public string AntiMicroMessage { get; set; } = "Where did all the micros go...";
public string GrenadeMessage { get; set; } = "Watch out!";
public string PocketDimensionMessage { get; set; } = "I hear he likes to see people suffer...";
public string PositiveEffectMessage { get; set; } = "You feel slightly better";
public string AdvancedPositiveEffectMessage { get; set; } = "You feel better";
public string NegativeEffectMessage { get; set; } = "You feel worse";
public string AdvancedNegativeEffectMessage { get; set; } = "You feel like you could die any second";
public string SwitchInventoryMessage { get; set; } = "Whoops... looks like something happened to your items!";
}
public class GamblingCoinGameplayConfig
{
public float WarheadTimeIncrease { get; set; } = 20f;
public ushort BroadcastDuration { get; set; } = 3;
public float TeleportHeightOffset { get; set; } = 1f;
public int MaxMicrosToRemove { get; set; } = 8;
public ItemPoolConfig Items { get; set; } = new();
public EffectConfig Effects { get; set; } = new();
}
public class ItemPoolConfig
{
public ItemType[] CommonItems { get; set; } = {
ItemType.KeycardJanitor,
ItemType.KeycardScientist,
ItemType.Medkit,
ItemType.Painkillers,
ItemType.Radio,
ItemType.Flashlight
};
public ItemType[] UncommonItems { get; set; } = {
ItemType.KeycardZoneManager,
ItemType.KeycardGuard,
ItemType.KeycardResearchCoordinator,
ItemType.Adrenaline,
ItemType.ArmorLight,
ItemType.GrenadeFlash
};
public ItemType[] RareItems { get; set; } = {
ItemType.KeycardMTFPrivate,
ItemType.KeycardContainmentEngineer,
ItemType.KeycardMTFOperative,
ItemType.ArmorCombat,
ItemType.ArmorHeavy,
ItemType.SCP330,
ItemType.Lantern,
ItemType.GrenadeHE
};
public ItemType[] EpicItems { get; set; } = {
ItemType.KeycardFacilityManager,
ItemType.KeycardChaosInsurgency,
ItemType.KeycardMTFCaptain,
ItemType.SCP500
};
public ItemType[] LegendaryItems { get; set; } = {
ItemType.KeycardO5,
ItemType.MicroHID,
ItemType.Jailbird,
ItemType.ParticleDisruptor,
ItemType.GunCom45,
ItemType.Coin
};
}
public class EffectConfig
{
public AdvancedEffectSettings AdvancedPositive { get; set; } = new()
{
Effects = new[] { nameof(Invisible), nameof(DamageReduction), nameof(MovementBoost) },
Settings = new Dictionary<string, EffectSettings>
{
{ nameof(Invisible), new EffectSettings(1, 5f, true) },
{ nameof(DamageReduction), new EffectSettings(100, 8f, false) },
{ nameof(MovementBoost), new EffectSettings(40, 2f, false) }
}
};
public AdvancedEffectSettings Positive { get; set; } = new()
{
Effects = new[] { nameof(Invigorated), nameof(RainbowTaste), nameof(Vitality), nameof(BodyshotReduction) },
Settings = new Dictionary<string, EffectSettings>
{
{ nameof(Invigorated), new EffectSettings(1, 5f, true) },
{ nameof(RainbowTaste), new EffectSettings(2, 10f, true) },
{ nameof(Vitality), new EffectSettings(1, 10f, true) },
{ nameof(BodyshotReduction), new EffectSettings(4, 5f, true) }
}
};
public AdvancedEffectSettings Negative { get; set; } = new()
{
Effects = new[] { nameof(Asphyxiated), nameof(AmnesiaVision), nameof(Bleeding), nameof(Blurred),
nameof(Concussed), nameof(Deafened), nameof(Disabled) },
Settings = new Dictionary<string, EffectSettings>
{
{ nameof(Asphyxiated), new EffectSettings(1, 10f, true) },
{ nameof(AmnesiaVision), new EffectSettings(1, 5f, true) },
{ nameof(Bleeding), new EffectSettings(1, 5f, true) },
{ nameof(Blurred), new EffectSettings(1, 5f, true) },
{ nameof(Concussed), new EffectSettings(1, 5f, true) },
{ nameof(Deafened), new EffectSettings(1, 5f, true) },
{ nameof(Disabled), new EffectSettings(1, 5f, true) }
}
};
public AdvancedEffectSettings AdvancedNegative { get; set; } = new()
{
Effects = new[] { nameof(InsufficientLighting) },
Settings = new Dictionary<string, EffectSettings>
{
{ nameof(InsufficientLighting), new EffectSettings(1, 20f, true) }
}
};
}
public class AdvancedEffectSettings
{
public String[] Effects { get; set; }
public Dictionary<string, EffectSettings> Settings { get; set; }
public AdvancedEffectSettings(){}
}
public class EffectSettings
{
public byte Intensity { get; set; }
public float Duration { get; set; }
public bool AddDuration { get; set; }
public EffectSettings(byte intensity, float duration, bool addDuration)
{
Intensity = intensity;
Duration = duration;
AddDuration = addDuration;
}
public EffectSettings(){}
}
}

View File

@ -0,0 +1,196 @@
using System.Numerics;
using LabApi.Events.Arguments.PlayerEvents;
using LabApi.Features.Wrappers;
using MapGeneration;
using Mirror;
using PlayerRoles;
using Respawning.Waves;
using Utils;
using Logger = LabApi.Features.Console.Logger;
using Random = UnityEngine.Random;
namespace GamblingCoin
{
public class GamblingCoinEventHandler
{
private readonly WeightedRandomExecutor<PlayerFlippedCoinEventArgs> _executor;
public GamblingCoinEventHandler()
{
var configMessages = Plugin.Singleton.ConfigMessages;
var configGameplay = Plugin.Singleton.ConfigGameplay;
var configChances = Plugin.Singleton.ConfigChances;
_executor = new WeightedRandomExecutor<PlayerFlippedCoinEventArgs>();
_executor
.AddAction(_ =>
{
Warhead.DetonationTime += configGameplay.WarheadTimeIncrease;
Warhead.Start(suppressSubtitles: true);
}, configChances.NukeChance)
.AddAction(x =>
{
x.Player.SendBroadcast(configMessages.SpawnWaveMessage, configGameplay.BroadcastDuration);
if(GetPlayers().Any(player=>player.Role == RoleTypeId.Spectator)) {
Respawning.WaveManager.InitiateRespawn(Respawning.WaveManager.Waves[Random.Range(0, Respawning.WaveManager.Waves.Count)]);
}
}, configChances.SpawnWaveChance)
.AddAction(x =>
{
x.Player.SendBroadcast(configMessages.ItemSpawnMessage, configGameplay.BroadcastDuration);
SpawnRandomItemAtPlayer(x.Player, configGameplay.Items.CommonItems);
}, configChances.CommonItemChance)
.AddAction(x =>
{
x.Player.SendBroadcast(configMessages.UncommonItemSpawnMessage, configGameplay.BroadcastDuration);
SpawnRandomItemAtPlayer(x.Player, configGameplay.Items.UncommonItems);
}, configChances.UncommonItemChance)
.AddAction(x =>
{
x.Player.SendBroadcast(configMessages.RareItemSpawnMessage, configGameplay.BroadcastDuration);
SpawnRandomItemAtPlayer(x.Player, configGameplay.Items.RareItems);
}, configChances.RareItemChance)
.AddAction(x =>
{
x.Player.SendBroadcast(configMessages.EpicItemSpawnMessage, configGameplay.BroadcastDuration);
SpawnRandomItemAtPlayer(x.Player, configGameplay.Items.EpicItems);
}, configChances.EpicItemChance)
.AddAction(x =>
{
x.Player.SendBroadcast(configMessages.LegendaryItemSpawnMessage, configGameplay.BroadcastDuration);
SpawnRandomItemAtPlayer(x.Player, configGameplay.Items.LegendaryItems);
}, configChances.LegendaryItemChance)
.AddAction(x =>
{
x.Player.SendBroadcast(configMessages.RandomTeleportMessage, configGameplay.BroadcastDuration);
var randomRoom = Map.GetRandomRoom();
if (randomRoom == null) return;
var newPos = randomRoom.Position;
x.Player.Position = newPos + new UnityEngine.Vector3(0, configGameplay.TeleportHeightOffset, 0);;
}, configChances.RandomTeleportChance)
.AddAction(x =>
{
x.Player.SendBroadcast(configMessages.StealItemMessage, configGameplay.BroadcastDuration);
if (x.Player.CurrentItem != null) x.Player.DropItem(x.Player.CurrentItem);
}, configChances.StealItemChance)
.AddAction(x =>
{
x.Player.SendBroadcast(configMessages.ExplosionMessage, configGameplay.BroadcastDuration);
x.Player.ClearInventory();
ExplosionUtils.ServerExplode(x.Player.ReferenceHub, ExplosionType.Custom);
}, configChances.ExplosionChance)
.AddAction(x =>
{
x.Player.SendBroadcast(configMessages.AntiMicroMessage, configGameplay.BroadcastDuration);
GetPlayers().ForEach(p => { p.RemoveItem(ItemType.MicroHID, configGameplay.MaxMicrosToRemove); });
//TODO: remove *all* micros
}, configChances.AntiMicroChance)
.AddAction(x =>
{
x.Player.SendBroadcast(configMessages.GrenadeMessage, configGameplay.BroadcastDuration);
var grenade = (TimedGrenadeProjectile)Pickup.Create(ItemType.GrenadeHE, x.Player.Position);
grenade?.Spawn();
grenade?.FuseEnd();
}, configChances.GrenadeChance)
.AddAction(x =>
{
x.Player.SendBroadcast(configMessages.PocketDimensionMessage, configGameplay.BroadcastDuration);
var newPos = Map.Rooms.First(roomIdentifier => roomIdentifier.Zone==FacilityZone.Other).Position;
x.Player.Position = newPos + new UnityEngine.Vector3(0, configGameplay.TeleportHeightOffset, 0);
},
configChances.PocketDimensionChance)
.AddAction(x =>
{
x.Player.SendBroadcast(configMessages.AdvancedPositiveEffectMessage, configGameplay.BroadcastDuration);
ApplyRandomEffect(x.Player, configGameplay.Effects.AdvancedPositive);
}, configChances.AdvancedPositiveEffectChance)
.AddAction(x =>
{
x.Player.SendBroadcast(configMessages.PositiveEffectMessage, configGameplay.BroadcastDuration);
ApplyRandomEffect(x.Player, configGameplay.Effects.Positive);
}, configChances.PositiveEffectChance)
.AddAction(x =>
{
x.Player.SendBroadcast(configMessages.NegativeEffectMessage, configGameplay.BroadcastDuration);
ApplyRandomEffect(x.Player, configGameplay.Effects.Negative);
}, configChances.NegativeEffectChance)
.AddAction(x =>
{
x.Player.SendBroadcast(configMessages.AdvancedNegativeEffectMessage, configGameplay.BroadcastDuration);
ApplyRandomEffect(x.Player, configGameplay.Effects.AdvancedNegative);
}, configChances.AdvancedNegativeEffectChance)
.AddAction(x =>
{
var players = GetPlayers();
var randomPlayer = players[Random.Range(0,GetPlayers().Length)];
while(randomPlayer.RoleBase.Team is Team.Dead or Team.SCPs) randomPlayer = players[Random.Range(0,GetPlayers().Length)];
x.Player.SendBroadcast(configMessages.SwitchInventoryMessage, configGameplay.BroadcastDuration);
randomPlayer.SendBroadcast(configMessages.SwitchInventoryMessage, configGameplay.BroadcastDuration);
var randomPlayerItems = new List<Item>();
randomPlayer.Items.CopyTo(randomPlayerItems);
var items = x.Player.Items;
randomPlayer.ClearInventory();
foreach (var itemBase in items)
{
randomPlayer.AddItem(itemBase.Type);
}
x.Player.ClearInventory();
foreach (var randomPlayerItem in randomPlayerItems)
{
x.Player.AddItem(randomPlayerItem.Type);
}
}, configChances.SwitchInventoryChance);
return;
void ApplyRandomEffect(Player player, AdvancedEffectSettings settings)
{
var effectChosen = settings.Effects[Random.Range(0, settings.Effects.Length)];
var effectSettings = settings.Settings[effectChosen];
player.ReferenceHub.playerEffectsController.ChangeState(effectChosen, effectSettings.Intensity, effectSettings.Duration, effectSettings.AddDuration);
}
void SpawnItemAtPlayer(Player player, ItemType item)
{
var pickup = Pickup.Create(item, player.Position + new UnityEngine.Vector3(0,1,0));
if (pickup == null) return;
pickup.Spawn();
}
void SpawnRandomItemAtPlayer(Player player, ItemType[] items)
{
var itemIndex = Random.Range(0, items.Length);
SpawnItemAtPlayer(player, items[itemIndex]);
}
Player[] GetPlayers()
{
return Player.Dictionary.Values.ToArray();
}
}
public void OnFlippedCoin(PlayerFlippedCoinEventArgs ev)
{
_executor.Execute(ev);
}
}
}

View File

@ -0,0 +1,82 @@
namespace GamblingCoin;
public class WeightedRandomExecutor<TEvent>
{
private class WeightedAction
{
public Action<TEvent> Action { get; }
public double Weight { get; }
public WeightedAction(Action<TEvent> action, double weight)
{
if (weight <= 0)
throw new ArgumentOutOfRangeException(
nameof(weight),
"Weight must be positive."
);
Action = action ?? throw new ArgumentNullException(nameof(action));
Weight = weight;
}
}
private readonly List<WeightedAction> _actions = new();
private readonly Random _random = new();
private double _totalWeight;
/// <summary>
/// Adds a function to be potentially executed, along with its weight.
/// </summary>
/// <param name="action">The function to add. It must accept an argument of type TEvent.</param>
/// <param name="weight">The positive weight for this function. Higher weights mean higher probability.</param>
public WeightedRandomExecutor<TEvent> AddAction(Action<TEvent> action, double weight)
{
var weightedAction = new WeightedAction(action, weight);
_actions.Add(weightedAction);
_totalWeight += weight;
return this;
}
/// <summary>
/// Executes one of the added functions randomly based on their weights.
/// The chosen function will be called with the provided 'ev' argument.
/// </summary>
/// <param name="ev">The event argument to pass to the chosen function.</param>
/// <exception cref="InvalidOperationException">Thrown if no actions have been added.</exception>
public void Execute(TEvent ev)
{
if (_actions.Count == 0)
{
throw new InvalidOperationException(
"No actions have been added to execute."
);
}
if (_totalWeight <= 0) // Should not happen if AddAction validates weight > 0
{
throw new InvalidOperationException(
"Total weight is zero or negative, cannot execute."
);
}
var randomNumber = _random.NextDouble() * _totalWeight;
double cumulativeWeight = 0;
foreach (var weightedAction in _actions)
{
cumulativeWeight += weightedAction.Weight;
if (!(randomNumber < cumulativeWeight)) continue;
weightedAction.Action(ev);
return; // Exit after executing one action
}
// Fallback in case of floating point inaccuracies,
// or if somehow randomNumber was exactly _totalWeight (NextDouble is < 1.0)
// This should ideally pick the last item if all weights were summed up.
if (_actions.Any())
{
_actions.Last().Action(ev);
}
}
}

View File

@ -0,0 +1,66 @@
using InventorySystem.Items.Keycards;
using LabApi.Events.Arguments.PlayerEvents;
using LabApi.Events.Handlers;
using LabApi.Features;
using LabApi.Features.Console;
namespace KeycardButModern
{
public class Plugin: LabApi.Loader.Features.Plugins.Plugin
{
public override string Name => "KeycardButModern";
public override string Description => "Ever thought you wanted your keycard implanted in your body? No? Same.";
public override string Author => "Code002Lover";
public override Version Version { get; } = new(1, 0, 0);
public override Version RequiredApiVersion { get; } = new (LabApiProperties.CompiledVersion);
private void OnInteractingDoor(PlayerInteractingDoorEventArgs ev)
{
if (ev.CanOpen)
{
Logger.Debug("Door can be opened, no need for implant check");
return;
}
if (ev.Door.IsLocked)
{
Logger.Debug("Door has active locks");
return;
}
var permissions = ev.Door.Permissions;
foreach (var playerItem in ev.Player.Items)
{
//is keycard?
if (playerItem.Type > ItemType.KeycardO5) continue;
if (playerItem.Base is not KeycardItem keycardItem)
{
continue;
}
var keycardPermissions = keycardItem.GetPermissions(ev.Door.Base);
Logger.Debug($"Item is a keycard: {keycardPermissions} vs {permissions} = {keycardPermissions & permissions}");
if ((keycardPermissions & permissions) != permissions) continue;
ev.Door.IsOpened = true;
Logger.Debug("Door can be opened");
return;
}
}
public override void Enable()
{
Logger.Debug("starting...");
PlayerEvents.InteractingDoor += OnInteractingDoor;
}
public override void Disable()
{
PlayerEvents.InteractingDoor -= OnInteractingDoor;
Logger.Debug("unloading...");
}
}
}

View File

@ -0,0 +1,37 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net48</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>disable</Nullable>
<LangVersion>10</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>
<Reference Include="Assembly-CSharp">
<HintPath>..\..\.local\share\Steam\steamapps\common\SCP Secret Laboratory Dedicated Server\SCPSL_Data\Managed\Assembly-CSharp.dll</HintPath>
</Reference>
<Reference Include="Mirror">
<HintPath>..\..\.local\share\Steam\steamapps\common\SCP Secret Laboratory Dedicated Server\SCPSL_Data\Managed\Mirror.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.CoreModule">
<HintPath>..\..\.local\share\Steam\steamapps\common\SCP Secret Laboratory Dedicated Server\SCPSL_Data\Managed\UnityEngine.CoreModule.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Northwood.LabAPI" Version="1.0.2" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,34 @@

Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GamblingCoin", "GamblingCoin\GamblingCoin.csproj", "{4EEC49AE-6037-4EC9-AC30-F38E7F394614}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KeycardButModern", "KeycardButModern\KeycardButModern.csproj", "{41731D20-3035-457B-ACFF-C1710D179FA5}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VisibleSpectators", "VisibleSpectators\VisibleSpectators.csproj", "{F320856D-6340-4DD5-89F7-EE44611DF8E8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SCPList", "SCPList\SCPList.csproj", "{2EFB3C10-A917-4840-97CD-B36733D666DE}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{4EEC49AE-6037-4EC9-AC30-F38E7F394614}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4EEC49AE-6037-4EC9-AC30-F38E7F394614}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4EEC49AE-6037-4EC9-AC30-F38E7F394614}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4EEC49AE-6037-4EC9-AC30-F38E7F394614}.Release|Any CPU.Build.0 = Release|Any CPU
{41731D20-3035-457B-ACFF-C1710D179FA5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{41731D20-3035-457B-ACFF-C1710D179FA5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{41731D20-3035-457B-ACFF-C1710D179FA5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{41731D20-3035-457B-ACFF-C1710D179FA5}.Release|Any CPU.Build.0 = Release|Any CPU
{F320856D-6340-4DD5-89F7-EE44611DF8E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F320856D-6340-4DD5-89F7-EE44611DF8E8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F320856D-6340-4DD5-89F7-EE44611DF8E8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F320856D-6340-4DD5-89F7-EE44611DF8E8}.Release|Any CPU.Build.0 = Release|Any CPU
{2EFB3C10-A917-4840-97CD-B36733D666DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2EFB3C10-A917-4840-97CD-B36733D666DE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2EFB3C10-A917-4840-97CD-B36733D666DE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2EFB3C10-A917-4840-97CD-B36733D666DE}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

View File

@ -0,0 +1,115 @@
using HintServiceMeow.Core.Enum;
using HintServiceMeow.Core.Models.Hints;
using HintServiceMeow.Core.Utilities;
using LabApi.Events.Arguments.PlayerEvents;
using LabApi.Events.Handlers;
using LabApi.Features;
using LabApi.Features.Console;
using LabApi.Features.Wrappers;
using LabApi.Loader.Features.Plugins;
using PlayerRoles;
using Timer = System.Timers.Timer;
namespace VisibleSpectators
{
public class Plugin : Plugin<SpectatorConfig>
{
public override string Name => "VisibleSpectators";
public override string Author => "Code002Lover";
public override Version Version { get; } = new(1, 0, 0);
public override string Description => "See your spectators";
public override Version RequiredApiVersion { get; } = new (LabApiProperties.CompiledVersion);
private static Plugin _singleton;
private Timer _timer;
private readonly Dictionary<Player,Hint> _spectatorHints = new();
public override void Enable()
{
Logger.Debug("starting...");
_singleton = this;
PlayerEvents.ChangedSpectator += OnSpectate;
PlayerEvents.Joined += OnJoin;
_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;
_singleton = null;
}
private void UpdateSpectators()
{
foreach (var player in GetPlayers())
{
UpdateSpectators(player);
}
}
private void UpdateSpectators(Player player)
{
var spectators = string.Join("\n",player.CurrentSpectators.Select(x => x.DisplayName));
if (player.Role == RoleTypeId.Spectator)
{
spectators = string.Join("\n",player.CurrentlySpectating?.CurrentSpectators.Select(x => x.DisplayName) ?? Array.Empty<string>());
}
if (spectators.Length < 2)
{
spectators = Config!.NoSpectatorsMessage;
}
_spectatorHints[player].Text = $"{Config!.HeaderMessage}\n{spectators}\n{DateTime.UtcNow:HH:mm:ss}";
_spectatorHints[player].Hide = player.Role is RoleTypeId.Overwatch or RoleTypeId.Destroyed or RoleTypeId.None;
}
private static Player[] GetPlayers()
{
return Player.Dictionary.Values.Where(x=>!x.IsHost).ToArray();
}
private static void OnSpectate(PlayerChangedSpectatorEventArgs ev)
{
_singleton.UpdateSpectators(ev.OldTarget);
_singleton.UpdateSpectators(ev.NewTarget);
}
private void OnJoin(PlayerJoinedEventArgs ev)
{
var hint = new Hint
{
Text = $"{Config!.HeaderMessage}\n{Config!.NoSpectatorsMessage}",
Alignment = HintAlignment.Right,
YCoordinate = 100,
Hide = true
};
var playerDisplay = PlayerDisplay.Get(ev.Player);
playerDisplay.AddHint(hint);
_spectatorHints[ev.Player] = hint;
}
}
public class SpectatorConfig
{
public string HeaderMessage => "Spectators:";
public string NoSpectatorsMessage => "No spectators";
}
}

View File

@ -0,0 +1,43 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net48</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>disable</Nullable>
<LangVersion>10</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>
<Reference Include="0Harmony">
<HintPath>..\dependencies\0Harmony.dll</HintPath>
</Reference>
<Reference Include="Assembly-CSharp">
<HintPath>..\..\.local\share\Steam\steamapps\common\SCP Secret Laboratory Dedicated Server\SCPSL_Data\Managed\Assembly-CSharp.dll</HintPath>
</Reference>
<Reference Include="HintServiceMeow">
<HintPath>..\dependencies\HintServiceMeow-LabAPI.dll</HintPath>
</Reference>
<Reference Include="Mirror">
<HintPath>..\..\.local\share\Steam\steamapps\common\SCP Secret Laboratory Dedicated Server\SCPSL_Data\Managed\Mirror.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.CoreModule">
<HintPath>..\..\.local\share\Steam\steamapps\common\SCP Secret Laboratory Dedicated Server\SCPSL_Data\Managed\UnityEngine.CoreModule.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Northwood.LabAPI" Version="1.0.2" />
</ItemGroup>
</Project>