diff --git a/GamblingCoin/GamblingCoinConfig.cs b/GamblingCoin/GamblingCoinConfig.cs index a6fba76..a8d7c07 100644 --- a/GamblingCoin/GamblingCoinConfig.cs +++ b/GamblingCoin/GamblingCoinConfig.cs @@ -23,6 +23,7 @@ public class GamblingCoinChancesConfig public int AdvancedPositiveEffectChance { get; set; } = 150; public int AdvancedNegativeEffectChance { get; set; } = 250; public int RemoveCoinChance { get; set; } = 300; + public int SpawnZombieChance { get; set; } = 100; } public class GamblingCoinMessages @@ -44,6 +45,7 @@ public class GamblingCoinMessages 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 string SpawnZombieMessage { get; set; } = "You spawned as a Zombie!"; } public class GamblingCoinGameplayConfig diff --git a/GamblingCoin/GamblingCoinEventHandler.cs b/GamblingCoin/GamblingCoinEventHandler.cs index 83504ad..3995e8c 100644 --- a/GamblingCoin/GamblingCoinEventHandler.cs +++ b/GamblingCoin/GamblingCoinEventHandler.cs @@ -187,7 +187,23 @@ public class GamblingCoinEventHandler x.Player.ClearInventory(); foreach (var randomPlayerItem in randomPlayerItems) x.Player.AddItem(randomPlayerItem.Type); }, configChances.SwitchInventoryChance) - .AddAction(x => { x.Player.CurrentItem?.DropItem().Destroy(); }, configChances.RemoveCoinChance); + .AddAction(x => { x.Player.CurrentItem?.DropItem().Destroy(); }, configChances.RemoveCoinChance) + .AddAction(x => + { + var spectators = Player.List.Where(player => player.Role == RoleTypeId.Spectator).ToArray(); + var spectator = spectators[Random.Range(0, spectators.Length)]; + + spectator.SendBroadcast(configMessages.SpawnZombieMessage, configGameplay.BroadcastDuration); + spectator.SetRole(RoleTypeId.Scp0492); + + var spawnRoom = Map.Rooms.First(room => room.Name == RoomName.HczWarhead); + if (Warhead.IsDetonated) + { + spawnRoom = Map.Rooms.First(room => room.Name == RoomName.Outside); + } + + spectator.Position = spawnRoom.Position + new Vector3(0, 1, 0); + }, configChances.SpawnZombieChance); return; diff --git a/GrowingZombies/GrowingZombies.cs b/GrowingZombies/GrowingZombies.cs index 7a15341..d51a98d 100644 --- a/GrowingZombies/GrowingZombies.cs +++ b/GrowingZombies/GrowingZombies.cs @@ -1,4 +1,5 @@ using CustomPlayerEffects; +using HintServiceMeow.Core.Models.Hints; using InventorySystem.Items.Usables.Scp330; using LabApi.Events.Arguments.PlayerEvents; using LabApi.Events.Arguments.Scp0492Events; @@ -12,18 +13,22 @@ namespace GrowingZombies; public class GrowingZombies : Plugin { - private readonly Dictionary _zombieCorpseCount = new(); + public readonly Dictionary ZombieCorpseCount = new(); + public static GrowingZombies Instance { get; set; } + public override string Name => "GrowingZombies"; public override string Author => "Code002Lover"; public override Version Version { get; } = new(1, 0, 0); public override string Description => "Makes zombies grow stronger as they eat more"; public override Version RequiredApiVersion { get; } = new(LabApiProperties.CompiledVersion); + public override void Enable() { Scp0492Events.ConsumedCorpse += OnZombieEat; ServerEvents.RoundEnded += OnRoundEnd; PlayerEvents.Left += OnPlayerLeave; + Instance = this; } public override void Disable() @@ -31,17 +36,18 @@ public class GrowingZombies : Plugin Scp0492Events.ConsumedCorpse -= OnZombieEat; ServerEvents.RoundEnded -= OnRoundEnd; PlayerEvents.Left -= OnPlayerLeave; - _zombieCorpseCount.Clear(); + ZombieCorpseCount.Clear(); + Instance = null; } private void OnRoundEnd(RoundEndedEventArgs ev) { - _zombieCorpseCount.Clear(); + ZombieCorpseCount.Clear(); } private void OnPlayerLeave(PlayerLeftEventArgs ev) { - _zombieCorpseCount.Remove(ev.Player); + ZombieCorpseCount.Remove(ev.Player); } private void OnZombieEat(Scp0492ConsumedCorpseEventArgs ev) @@ -50,21 +56,21 @@ public class GrowingZombies : Plugin return; // Increment corpse count for this zombie - if (!_zombieCorpseCount.ContainsKey(ev.Player)) - _zombieCorpseCount[ev.Player] = 0; - _zombieCorpseCount[ev.Player]++; + if (!ZombieCorpseCount.ContainsKey(ev.Player)) + ZombieCorpseCount[ev.Player] = 0; + ZombieCorpseCount[ev.Player]++; - var corpsesEaten = _zombieCorpseCount[ev.Player]; + var corpsesEaten = ZombieCorpseCount[ev.Player]; - ev.Player.MaxHealth += 50; + ev.Player.MaxHealth = Math.Min(1000, ev.Player.MaxHealth + 50); + ev.Player.MaxHumeShield += 10; - var movementBoostIntensity = (byte)Math.Min(1 + corpsesEaten * 0.1f, 3f); - ev.Player.ReferenceHub.playerEffectsController.ChangeState(movementBoostIntensity, 30); + var movementBoostIntensity = (byte)Math.Min(1 + corpsesEaten * 0.5f, 5f); + ev.Player.ReferenceHub.playerEffectsController.ChangeState(movementBoostIntensity, 120); // Add damage resistance after eating multiple corpses - var damageResistance = (byte)Math.Min(0.5 - corpsesEaten * 0.5f, 2f); if (corpsesEaten >= 3) - ev.Player.ReferenceHub.playerEffectsController.ChangeState(damageResistance, 20); + ev.Player.ReferenceHub.playerEffectsController.ChangeState((byte)(corpsesEaten*2), float.MaxValue); // Add regeneration effect after eating multiple corpses if (corpsesEaten < 5) return; diff --git a/GrowingZombies/GrowingZombies.csproj b/GrowingZombies/GrowingZombies.csproj index 753ceeb..1b7af69 100644 --- a/GrowingZombies/GrowingZombies.csproj +++ b/GrowingZombies/GrowingZombies.csproj @@ -23,6 +23,9 @@ ..\dependencies\Assembly-CSharp.dll + + ..\dependencies\HintServiceMeow-LabAPI.dll + ..\dependencies\Mirror.dll diff --git a/ModInfo/ModInfo.cs b/ModInfo/ModInfo.cs new file mode 100644 index 0000000..19ac947 --- /dev/null +++ b/ModInfo/ModInfo.cs @@ -0,0 +1,69 @@ +using HintServiceMeow.Core.Enum; +using HintServiceMeow.Core.Models.Hints; +using HintServiceMeow.Core.Utilities; +using LabApi.Features; +using LabApi.Loader.Features.Plugins; +using LabApi.Features.Wrappers; +using MEC; + +namespace ModInfo +{ + public class ModInfo : Plugin + { + public override string Name => "ModInfo"; + public override string Author => "Code002Lover"; + public override Version Version { get; } = new(1, 0, 0); + public override string Description => "Shows some extra info for moderators"; + public override Version RequiredApiVersion { get; } = new(LabApiProperties.CompiledVersion); + + private readonly Dictionary _spectatorHints = new(); + + public override void Enable() + { + Timing.RunCoroutine(GodmodeHintLoop()); + } + + public override void Disable() + { + + } + + private IEnumerator GodmodeHintLoop() + { + while(true) + { + yield return Timing.WaitForSeconds(1); + UpdateHints(); + } + // ReSharper disable once IteratorNeverReturns + } + + private void UpdateHints() + { + foreach (var player in Player.ReadyList) UpdateHint(player); + } + + private void UpdateHint(Player player) + { + var hint = _spectatorHints.TryGetValue(player, out var hintValue) ? hintValue : AddPlayerHint(player); + hint.Hide = !player.IsGodModeEnabled; + } + + private Hint AddPlayerHint(Player player) + { + var hint = new Hint + { + Text = "GODMODE", + Alignment = HintAlignment.Left, + YCoordinate = 800, + Hide = true + }; + + var playerDisplay = PlayerDisplay.Get(player); + playerDisplay.AddHint(hint); + + _spectatorHints[player] = hint; + return hint; + } + } +} \ No newline at end of file diff --git a/ModInfo/ModInfo.csproj b/ModInfo/ModInfo.csproj new file mode 100644 index 0000000..bb92484 --- /dev/null +++ b/ModInfo/ModInfo.csproj @@ -0,0 +1,62 @@ + + + + net48 + enable + disable + latest + + + + true + true + full + + + + true + false + none + + + + + + + + + + + ..\dependencies\0Harmony.dll + + + ..\dependencies\Assembly-CSharp.dll + + + ..\dependencies\Assembly-CSharp-firstpass.dll + + + ..\dependencies\CommandSystem.Core.dll + + + ..\dependencies\HintServiceMeow-LabAPI.dll + + + ..\dependencies\Mirror.dll + + + ..\dependencies\NorthwoodLib.dll + + + ..\dependencies\Pooling.dll + + + ..\dependencies\UnityEngine.CoreModule.dll + + + + + + + + diff --git a/SCPTeamHint/SCPTeamHint.cs b/SCPTeamHint/SCPTeamHint.cs index f4a3375..7bded00 100644 --- a/SCPTeamHint/SCPTeamHint.cs +++ b/SCPTeamHint/SCPTeamHint.cs @@ -1,4 +1,5 @@ -using HintServiceMeow.Core.Enum; +using System.Drawing; +using HintServiceMeow.Core.Enum; using HintServiceMeow.Core.Models.Hints; using HintServiceMeow.Core.Utilities; using LabApi.Events.Arguments.PlayerEvents; @@ -11,6 +12,8 @@ using PlayerRoles.PlayableScps.Scp079; using PlayerRoles.PlayableScps.Scp096; using PlayerRoles.PlayableScps.Scp3114; using Timer = System.Timers.Timer; +using MEC; +using PlayerRoles.PlayableScps.Scp049.Zombies; namespace SCPTeamHint; @@ -19,7 +22,6 @@ public class Plugin : LabApi.Loader.Features.Plugins.Plugin private readonly object _hintsLock = new(); private readonly Dictionary _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); @@ -28,22 +30,33 @@ public class Plugin : LabApi.Loader.Features.Plugins.Plugin public override void Enable() { - Logger.Debug("Apple juice"); PlayerEvents.Joined += OnJoin; PlayerEvents.Left += OnLeft; - _timer = new Timer(1000); - _timer.Elapsed += (_, _) => UpdateHints(); - _timer.Start(); + Timing.RunCoroutine(ContinuouslyUpdateHints()); + } + + private IEnumerator ContinuouslyUpdateHints() + { + while (true) + { + yield return Timing.WaitForSeconds(1); + try + { + UpdateHints(); + } + catch (Exception e) + { + Logger.Error(e); + } + } + // ReSharper disable once IteratorNeverReturns } public override void Disable() { PlayerEvents.Joined -= OnJoin; PlayerEvents.Left -= OnLeft; - _timer?.Stop(); - _timer?.Dispose(); - _timer = null; } private void UpdateHints() @@ -52,11 +65,11 @@ public class Plugin : LabApi.Loader.Features.Plugins.Plugin lock (_hintsLock) { - foreach (var player in Player.ReadyList.Where(x => !x.IsHost && !x.IsDummy && x.IsSCP)) + foreach (var player in Player.ReadyList.Where(x => !x.IsHost && !x.IsDummy && (x.IsSCP || x.Role is RoleTypeId.Scp0492))) { var text = $" {player.RoleBase.RoleName} | {player.HumeShield} | {player.Health} | {player.Zone} "; - + switch (player.RoleBase) { case Scp096Role scp: @@ -66,17 +79,23 @@ public class Plugin : LabApi.Loader.Features.Plugins.Plugin if (!tracker) break; - text += $"Targets: {tracker.Targets.Count}"; + var targetColor = tracker.Targets.Count > 0 ? "red" : "grey"; + text += $"Targets: {tracker.Targets.Count}"; break; case Scp3114Role scp3114: - { text += "\n"; var stolenRole = scp3114.CurIdentity.StolenRole; - text += $" {stolenRole}"; + if (scp3114.Disguised) + { + text += $" {stolenRole}"; + } + else + { + text += " None"; + } break; - } case Scp079Role scp079: text = $" {player.RoleBase.RoleName} | {scp079.CurrentCamera.Room.Zone} "; @@ -88,7 +107,16 @@ public class Plugin : LabApi.Loader.Features.Plugins.Plugin if (!auxManager || !tierManager) break; text += - $" AUX: {auxManager.CurrentAuxFloored} / {auxManager.MaxAux} | Level {tierManager.AccessTierLevel}"; + $" AUX: {auxManager.CurrentAuxFloored} / {auxManager.MaxAux} | Level {tierManager.AccessTierLevel}"; + break; + case ZombieRole: + var count = GrowingZombies.GrowingZombies.Instance.ZombieCorpseCount[player]; + + const string corpseColor = "E68A8A"; + + text += "\n"; + + text += $" Corpses eaten: {count}"; break; } @@ -114,8 +142,8 @@ public class Plugin : LabApi.Loader.Features.Plugins.Plugin } Logger.Debug( - $"Player {player.Nickname} is on team {player.RoleBase.Team} | hide: {player.RoleBase.Team != Team.SCPs}"); - hint.Hide = player.RoleBase.Team != Team.SCPs; + $"Player {player.Nickname} is on team {player.RoleBase.Team} with Role {player.Role} | hide: {player.RoleBase.Team != Team.SCPs}"); + hint.Hide = player.RoleBase.Team != Team.SCPs && player.Role != RoleTypeId.Scp0492 && player.Role != RoleTypeId.Overwatch; if (!hint.Hide) hint.Text = hintText; } diff --git a/SCPTeamHint/SCPTeamHint.csproj b/SCPTeamHint/SCPTeamHint.csproj index 3c133ee..343957d 100644 --- a/SCPTeamHint/SCPTeamHint.csproj +++ b/SCPTeamHint/SCPTeamHint.csproj @@ -45,4 +45,9 @@ ..\dependencies\UnityEngine.CoreModule.dll + + + + + diff --git a/SecretPluginLaboratories.sln b/SecretPluginLaboratories.sln index b2b5f72..88ab551 100644 --- a/SecretPluginLaboratories.sln +++ b/SecretPluginLaboratories.sln @@ -38,6 +38,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TemplateProject", "Template EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LobbyGame", "LobbyGame\LobbyGame.csproj", "{E02243D5-0229-47BB-88A7-252EC753C8CC}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StatsTracker", "StatsTracker\StatsTracker.csproj", "{DA17C0F1-9C99-4F80-9871-38D6EB00EA95}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModInfo", "ModInfo\ModInfo.csproj", "{8C55C629-FFB9-41AC-8F5C-1BF715110766}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -120,5 +124,13 @@ Global {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 + {DA17C0F1-9C99-4F80-9871-38D6EB00EA95}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DA17C0F1-9C99-4F80-9871-38D6EB00EA95}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DA17C0F1-9C99-4F80-9871-38D6EB00EA95}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DA17C0F1-9C99-4F80-9871-38D6EB00EA95}.Release|Any CPU.Build.0 = Release|Any CPU + {8C55C629-FFB9-41AC-8F5C-1BF715110766}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8C55C629-FFB9-41AC-8F5C-1BF715110766}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8C55C629-FFB9-41AC-8F5C-1BF715110766}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8C55C629-FFB9-41AC-8F5C-1BF715110766}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/StatsTracker/FodyWeavers.xml b/StatsTracker/FodyWeavers.xml new file mode 100644 index 0000000..37fe15b --- /dev/null +++ b/StatsTracker/FodyWeavers.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/StatsTracker/FodyWeavers.xsd b/StatsTracker/FodyWeavers.xsd new file mode 100644 index 0000000..f2dbece --- /dev/null +++ b/StatsTracker/FodyWeavers.xsd @@ -0,0 +1,176 @@ + + + + + + + + + + + + A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks + + + + + A list of assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks. + + + + + A list of runtime assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks + + + + + A list of runtime assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks. + + + + + Obsolete, use UnmanagedWinX86Assemblies instead + + + + + A list of unmanaged X86 (32 bit) assembly names to include, delimited with line breaks. + + + + + Obsolete, use UnmanagedWinX64Assemblies instead. + + + + + A list of unmanaged X64 (64 bit) assembly names to include, delimited with line breaks. + + + + + A list of unmanaged Arm64 (64 bit) assembly names to include, delimited with line breaks. + + + + + The order of preloaded assemblies, delimited with line breaks. + + + + + + This will copy embedded files to disk before loading them into memory. This is helpful for some scenarios that expected an assembly to be loaded from a physical file. + + + + + Controls if .pdbs for reference assemblies are also embedded. + + + + + Controls if runtime assemblies are also embedded. + + + + + Controls whether the runtime assemblies are embedded with their full path or only with their assembly name. + + + + + Embedded assemblies are compressed by default, and uncompressed when they are loaded. You can turn compression off with this option. + + + + + As part of Costura, embedded assemblies are no longer included as part of the build. This cleanup can be turned off. + + + + + The attach method no longer subscribes to the `AppDomain.AssemblyResolve` (.NET 4.x) and `AssemblyLoadContext.Resolving` (.NET 6.0+) events. + + + + + Costura by default will load as part of the module initialization. This flag disables that behavior. Make sure you call CosturaUtility.Initialize() somewhere in your code. + + + + + Costura will by default use assemblies with a name like 'resources.dll' as a satellite resource and prepend the output path. This flag disables that behavior. + + + + + A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with | + + + + + A list of assembly names to include from the default action of "embed all Copy Local references", delimited with |. + + + + + A list of runtime assembly names to exclude from the default action of "embed all Copy Local references", delimited with | + + + + + A list of runtime assembly names to include from the default action of "embed all Copy Local references", delimited with |. + + + + + Obsolete, use UnmanagedWinX86Assemblies instead + + + + + A list of unmanaged X86 (32 bit) assembly names to include, delimited with |. + + + + + Obsolete, use UnmanagedWinX64Assemblies instead + + + + + A list of unmanaged X64 (64 bit) assembly names to include, delimited with |. + + + + + A list of unmanaged Arm64 (64 bit) assembly names to include, delimited with |. + + + + + The order of preloaded assemblies, delimited with |. + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/StatsTracker/StatsTracker.cs b/StatsTracker/StatsTracker.cs new file mode 100644 index 0000000..92983ad --- /dev/null +++ b/StatsTracker/StatsTracker.cs @@ -0,0 +1,162 @@ +using LabApi.Features; +using LabApi.Loader.Features.Plugins; +using System.Collections.Concurrent; +using System.Data.SQLite; +using LabApi.Events.Arguments.PlayerEvents; +using LabApi.Events.Handlers; +using LabApi.Loader; + +namespace StatsTracker +{ + public class StatsTracker : Plugin + { + public override string Name => "StatsTracker"; + public override string Author => "Code002Lover"; + public override Version Version { get; } = new(1, 0, 0); + public override string Description => "Tracks stats for players."; + public override Version RequiredApiVersion { get; } = new(LabApiProperties.CompiledVersion); + + private string _dbPath; + private readonly ConcurrentDictionary _currentSessionStats = new(); + + private class PlayerStats + { + public int Kills { get; set; } + public int Deaths { get; set; } + public Dictionary ItemUsage { get; set; } = new(); + } + + public override void Enable() + { + _dbPath = Path.Combine(this.GetConfigDirectory().FullName, "stats.db"); + InitializeDatabase(); + + PlayerEvents.Death += OnPlayerDied; + PlayerEvents.UsedItem += OnItemUsed; + PlayerEvents.Left += OnPlayerLeft; + } + + public override void Disable() + { + PlayerEvents.Death -= OnPlayerDied; + PlayerEvents.UsedItem -= OnItemUsed; + PlayerEvents.Left -= OnPlayerLeft; + + // Save any remaining stats + foreach (var player in _currentSessionStats) + { + SavePlayerStats(player.Key, player.Value); + } + _currentSessionStats.Clear(); + } + + private void InitializeDatabase() + { + using var connection = new SQLiteConnection($"Data Source={_dbPath}"); + connection.Open(); + + using var command = connection.CreateCommand(); + command.CommandText = """ + + CREATE TABLE IF NOT EXISTS PlayerStats ( + UserId TEXT PRIMARY KEY, + Kills INTEGER DEFAULT 0, + Deaths INTEGER DEFAULT 0 + ); + + CREATE TABLE IF NOT EXISTS ItemUsage ( + UserId TEXT, + ItemType INTEGER, + UsageCount INTEGER DEFAULT 0, + PRIMARY KEY (UserId, ItemType) + ); + """; + command.ExecuteNonQuery(); + } + + private void OnPlayerDied(PlayerDeathEventArgs ev) + { + if (ev.Attacker != null) + { + var killerStats = _currentSessionStats.GetOrAdd(ev.Attacker.UserId, _ => new PlayerStats()); + killerStats.Kills++; + } + + var victimStats = _currentSessionStats.GetOrAdd(ev.Player.UserId, _ => new PlayerStats()); + victimStats.Deaths++; + } + + private void OnItemUsed(PlayerUsedItemEventArgs ev) + { + var stats = _currentSessionStats.GetOrAdd(ev.Player.UserId, _ => new PlayerStats()); + + if (!stats.ItemUsage.ContainsKey(ev.UsableItem.Type)) + stats.ItemUsage[ev.UsableItem.Type] = 0; + + stats.ItemUsage[ev.UsableItem.Type]++; + } + + private void OnPlayerLeft(PlayerLeftEventArgs ev) + { + if (_currentSessionStats.TryRemove(ev.Player.UserId, out var stats)) + { + SavePlayerStats(ev.Player.UserId, stats); + } + } + + private void SavePlayerStats(string userId, PlayerStats stats) + { + using var connection = new SQLiteConnection($"Data Source={_dbPath}"); + connection.Open(); + using var transaction = connection.BeginTransaction(); + + try + { + // Update player stats + using (var command = connection.CreateCommand()) + { + command.CommandText = """ + + INSERT INTO PlayerStats (UserId, Kills, Deaths) + VALUES (@userId, @kills, @deaths) + ON CONFLICT(UserId) DO UPDATE SET + Kills = Kills + @kills, + Deaths = Deaths + @deaths; + """; + + command.Parameters.AddWithValue("@userId", userId); + command.Parameters.AddWithValue("@kills", stats.Kills); + command.Parameters.AddWithValue("@deaths", stats.Deaths); + command.ExecuteNonQuery(); + } + + // Update item usage + foreach (var itemInfoPair in stats.ItemUsage) + { + var itemType = itemInfoPair.Key; + var count = itemInfoPair.Value; + using var command = connection.CreateCommand(); + command.CommandText = """ + + INSERT INTO ItemUsage (UserId, ItemType, UsageCount) + VALUES (@userId, @itemType, @count) + ON CONFLICT(UserId, ItemType) DO UPDATE SET + UsageCount = UsageCount + @count; + """; + + command.Parameters.AddWithValue("@userId", userId); + command.Parameters.AddWithValue("@itemType", (int)itemType); + command.Parameters.AddWithValue("@count", count); + command.ExecuteNonQuery(); + } + + transaction.Commit(); + } + catch + { + transaction.Rollback(); + throw; + } + } + } +} \ No newline at end of file diff --git a/StatsTracker/StatsTracker.csproj b/StatsTracker/StatsTracker.csproj new file mode 100644 index 0000000..22c47cb --- /dev/null +++ b/StatsTracker/StatsTracker.csproj @@ -0,0 +1,62 @@ + + + + net48 + enable + disable + latest + + + + true + true + full + + + + true + false + none + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + ..\dependencies\0Harmony.dll + + + ..\dependencies\Assembly-CSharp.dll + + + ..\dependencies\Assembly-CSharp-firstpass.dll + + + ..\dependencies\CommandSystem.Core.dll + + + ..\dependencies\HintServiceMeow-LabAPI.dll + + + ..\dependencies\Mirror.dll + + + ..\dependencies\NorthwoodLib.dll + + + ..\dependencies\Pooling.dll + + + ..\dependencies\UnityEngine.CoreModule.dll + + + diff --git a/VisibleSpectators/PlayerDisplayUtil.cs b/VisibleSpectators/PlayerDisplayUtil.cs new file mode 100644 index 0000000..1621a95 --- /dev/null +++ b/VisibleSpectators/PlayerDisplayUtil.cs @@ -0,0 +1,75 @@ +using PlayerRoles; +using LabApi.Features; +using LabApi.Features.Wrappers; + +namespace VisibleSpectators; + +/// +/// Utility for formatting player display names and color mapping. +/// +public static class PlayerDisplayUtil +{ + private static readonly Dictionary ColorMap = new() + { + { "DEFAULT", "FFFFFF" }, + { "PUMPKIN", "EE7600" }, + { "ARMY_GREEN", "4B5320" }, + { "MINT", "98FB98" }, + { "NICKEL", "727472" }, + { "CARMINE", "960018" }, + { "EMERALD", "50C878" }, + { "GREEN", "228B22" }, + { "LIME", "BFFF00" }, + { "POLICE_BLUE", "002DB3" }, + { "ORANGE", "FF9966" }, + { "SILVER_BLUE", "666699" }, + { "BLUE_GREEN", "4DFFB8" }, + { "MAGENTA", "FF0090" }, + { "YELLOW", "FAFF86" }, + { "TOMATO", "FF6448" }, + { "DEEP_PINK", "FF1493" }, + { "AQUA", "00FFFF" }, + { "CYAN", "00B7EB" }, + { "CRIMSON", "DC143C" }, + { "LIGHT_GREEN", "32CD32" }, + { "SILVER", "A0A0A0" }, + { "BROWN", "944710" }, + { "RED", "C50000" }, + { "PINK", "FF96DE" }, + { "LIGHT_RED", "FD8272" }, + { "PURPLE", "8137CE" }, + { "BLUE", "005EBC" }, + { "TEAL", "008080" }, + { "GOLD", "EFC01A" } + }; + + /// + /// Returns a formatted display string for a player, with color. + /// + public static string PlayerToDisplay(Player player) + { + if (player is not { IsReady: true }) return string.Empty; + const string defaultColor = "FFFFFF"; + try + { + var groupColor = player.GroupColor; + if (string.IsNullOrEmpty(groupColor)) + return $"{player.DisplayName}"; + return ColorMap.TryGetValue(groupColor.ToUpper(), out var color) + ? $"{player.DisplayName}" + : $"{player.DisplayName}"; + } + catch + { + return $"{player.DisplayName}"; + } + } + + /// + /// Returns true if the player is not Overwatch. + /// + public static bool IsNotOverwatch(Player player) + { + return player != null && player.Role != RoleTypeId.Overwatch; + } +} \ No newline at end of file diff --git a/VisibleSpectators/Plugin.cs b/VisibleSpectators/Plugin.cs new file mode 100644 index 0000000..6150018 --- /dev/null +++ b/VisibleSpectators/Plugin.cs @@ -0,0 +1,38 @@ +using LabApi.Loader.Features.Plugins; +using LabApi.Features; +using LabApi.Events.Handlers; +using LabApi.Events.Arguments.PlayerEvents; +using LabApi.Features.Console; +using MEC; + +namespace VisibleSpectators; + +/// +/// Main entry point for the VisibleSpectators plugin. +/// +public class Plugin : Plugin +{ + private SpectatorManager _spectatorManager; + 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); + + public override void Enable() + { + Logger.Debug("starting..."); + _spectatorManager = new SpectatorManager(Config); + PlayerEvents.ChangedSpectator += _spectatorManager.OnSpectate; + PlayerEvents.Joined += _spectatorManager.OnJoin; + Timing.RunCoroutine(_spectatorManager.KeepUpdatingSpectators()); + } + + public override void Disable() + { + Logger.Debug("unloading..."); + PlayerEvents.Joined -= _spectatorManager.OnJoin; + PlayerEvents.ChangedSpectator -= _spectatorManager.OnSpectate; + _spectatorManager = null; + } +} \ No newline at end of file diff --git a/VisibleSpectators/SpectatorConfig.cs b/VisibleSpectators/SpectatorConfig.cs new file mode 100644 index 0000000..60cc7d3 --- /dev/null +++ b/VisibleSpectators/SpectatorConfig.cs @@ -0,0 +1,16 @@ +namespace VisibleSpectators; + +/// +/// Configuration for the VisibleSpectators plugin. +/// +public class SpectatorConfig +{ + /// + /// Header message shown above the spectator list. + /// + public string HeaderMessage { get; set; } = "Spectators:"; + /// + /// Message shown when there are no spectators. + /// + public string NoSpectatorsMessage { get; set; } = "No spectators"; +} \ No newline at end of file diff --git a/VisibleSpectators/SpectatorManager.cs b/VisibleSpectators/SpectatorManager.cs new file mode 100644 index 0000000..039a218 --- /dev/null +++ b/VisibleSpectators/SpectatorManager.cs @@ -0,0 +1,98 @@ +using HintServiceMeow.Core.Enum; +using HintServiceMeow.Core.Models.Hints; +using HintServiceMeow.Core.Utilities; +using LabApi.Events.Arguments.PlayerEvents; +using LabApi.Features; +using LabApi.Features.Console; +using LabApi.Features.Wrappers; +using MEC; +using PlayerRoles; + +namespace VisibleSpectators; + +/// +/// Handles spectator hint management and updates for players. +/// +public class SpectatorManager +{ + private readonly SpectatorConfig _config; + private readonly Dictionary _spectatorHints = new(); + public int YCoordinate { get; set; } = 100; + + public SpectatorManager(SpectatorConfig config) + { + _config = config; + } + + public IEnumerator KeepUpdatingSpectators() + { + while (true) + { + UpdateSpectators(); + yield return Timing.WaitForSeconds(1); + } + // ReSharper disable once IteratorNeverReturns + } + + public void OnSpectate(PlayerChangedSpectatorEventArgs ev) + { + UpdateSpectators(ev.OldTarget); + UpdateSpectators(ev.NewTarget); + UpdateSpectators(ev.Player); + } + + public void OnJoin(PlayerJoinedEventArgs ev) + { + AddPlayerHint(ev.Player); + } + + private void UpdateSpectators() + { + foreach (var player in GetPlayers()) + UpdateSpectators(player); + } + + private void AddPlayerHint(Player player) + { + var hint = new Hint + { + Text = $"{_config.HeaderMessage}\n{_config.NoSpectatorsMessage}", + Alignment = HintAlignment.Right, + YCoordinate = YCoordinate, + Hide = true + }; + var playerDisplay = PlayerDisplay.Get(player); + playerDisplay.AddHint(hint); + _spectatorHints[player] = hint; + } + + private void UpdateSpectators(Player player) + { + if (player == null) return; + if (!_spectatorHints.ContainsKey(player)) AddPlayerHint(player); + var spectators = _config.NoSpectatorsMessage; + try + { + spectators = string.Join("\n", player.CurrentSpectators.Where(PlayerDisplayUtil.IsNotOverwatch).Select(PlayerDisplayUtil.PlayerToDisplay)); + if (player.Role == RoleTypeId.Spectator) + spectators = player.CurrentlySpectating == null + ? _config.NoSpectatorsMessage + : string.Join("\n", + player.CurrentlySpectating?.CurrentSpectators.Where(PlayerDisplayUtil.IsNotOverwatch) + .Select(PlayerDisplayUtil.PlayerToDisplay) ?? Array.Empty()); + } + catch (Exception e) + { + Logger.Error(e); + } + if (spectators.Length < 2) spectators = _config.NoSpectatorsMessage; + _spectatorHints[player].Text = $"{_config.HeaderMessage}\n{spectators}"; + _spectatorHints[player].Hide = player.Role is RoleTypeId.Destroyed or RoleTypeId.None; + _spectatorHints[player].YCoordinate = YCoordinate + player.CurrentSpectators.Count * 10; + } + + private static Player[] GetPlayers() + { + return Player.ReadyList.Where(PlayerDisplayUtil.IsNotOverwatch).Where(x => x != null).ToArray(); + } +} \ No newline at end of file diff --git a/VisibleSpectators/VisibleSpectators.cs b/VisibleSpectators/VisibleSpectators.cs deleted file mode 100644 index 5a77a8e..0000000 --- a/VisibleSpectators/VisibleSpectators.cs +++ /dev/null @@ -1,194 +0,0 @@ -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 -{ - private static Plugin _singleton; - - private static readonly Dictionary GetColorMap = new() - { - { "DEFAULT", "FFFFFF" }, - { "PUMPKIN", "EE7600" }, - { "ARMY_GREEN", "4B5320" }, - { "MINT", "98FB98" }, - { "NICKEL", "727472" }, - { "CARMINE", "960018" }, - { "EMERALD", "50C878" }, - { "GREEN", "228B22" }, - { "LIME", "BFFF00" }, - { "POLICE_BLUE", "002DB3" }, - { "ORANGE", "FF9966" }, - { "SILVER_BLUE", "666699" }, - { "BLUE_GREEN", "4DFFB8" }, - { "MAGENTA", "FF0090" }, - { "YELLOW", "FAFF86" }, - { "TOMATO", "FF6448" }, - { "DEEP_PINK", "FF1493" }, - { "AQUA", "00FFFF" }, - { "CYAN", "00B7EB" }, - { "CRIMSON", "DC143C" }, - { "LIGHT_GREEN", "32CD32" }, - { "SILVER", "A0A0A0" }, - { "BROWN", "944710" }, - { "RED", "C50000" }, - { "PINK", "FF96DE" }, - { "LIGHT_RED", "FD8272" }, - { "PURPLE", "8137CE" }, - { "BLUE", "005EBC" }, - { "TEAL", "008080" }, - { "GOLD", "EFC01A" } - }; - - private readonly Dictionary _spectatorHints = new(); - private Timer _timer; - 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); - - public int YCoordinate { get; set; } = 100; - - 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 AddPlayerHint(Player player) - { - var hint = new Hint - { - Text = $"{Config!.HeaderMessage}\n{Config!.NoSpectatorsMessage}", - Alignment = HintAlignment.Right, - YCoordinate = YCoordinate, - Hide = true - }; - - var playerDisplay = PlayerDisplay.Get(player); - playerDisplay.AddHint(hint); - - _spectatorHints[player] = hint; - } - - private static string PlayerToDisplay(Player player) - { - if (player == null) return ""; - if (!player.IsReady) return ""; - - // Default color if GroupColor is null or not found in the map - const string defaultColor = "FFFFFF"; - - try - { - var groupColor = player.GroupColor; - if (string.IsNullOrEmpty(groupColor)) - return $"{player.DisplayName}"; - - return GetColorMap.TryGetValue(groupColor.ToUpper(), out var color) - ? $"{player.DisplayName}" - : $"{player.DisplayName}"; - } - catch - { - return $"{player.DisplayName}"; - } - } - - private static bool IsNotOverwatch(Player player) - { - return player != null && player.Role != RoleTypeId.Overwatch; - } - - private void UpdateSpectators(Player player) - { - // Safety check - if player doesn't have a hint, create one - if (!_spectatorHints.ContainsKey(player)) AddPlayerHint(player); - - var spectators = Config!.NoSpectatorsMessage; - - try - { - spectators = string.Join("\n", player.CurrentSpectators.Where(IsNotOverwatch).Select(PlayerToDisplay)); - if (player.Role == RoleTypeId.Spectator) - spectators = player.CurrentlySpectating == null - ? Config!.NoSpectatorsMessage - : string.Join("\n", - player.CurrentlySpectating?.CurrentSpectators.Where(IsNotOverwatch) - .Select(PlayerToDisplay) ?? Array.Empty()); - } - catch (Exception e) - { - Logger.Error(e); - } - - if (spectators.Length < 2) spectators = Config!.NoSpectatorsMessage; - - - _spectatorHints[player].Text = $"{Config!.HeaderMessage}\n{spectators}"; - - _spectatorHints[player].Hide = player.Role is RoleTypeId.Destroyed or RoleTypeId.None; - - _spectatorHints[player].YCoordinate = YCoordinate + player.CurrentSpectators.Count * 10; - } - - private static Player[] GetPlayers() - { - return Player.ReadyList.Where(IsNotOverwatch).ToArray(); - } - - private static void OnSpectate(PlayerChangedSpectatorEventArgs ev) - { - _singleton.UpdateSpectators(ev.OldTarget); - _singleton.UpdateSpectators(ev.NewTarget); - _singleton.UpdateSpectators(ev.Player); - } - - private void OnJoin(PlayerJoinedEventArgs ev) - { - AddPlayerHint(ev.Player); - } -} - -public class SpectatorConfig -{ - public string HeaderMessage { get; set; } = "Spectators:"; - public string NoSpectatorsMessage { get; set; } = "No spectators"; -} \ No newline at end of file