various thingies

This commit is contained in:
code002lover 2025-06-08 00:53:52 +02:00
parent 67e3d6ceaa
commit 326b99c464
18 changed files with 867 additions and 226 deletions

View File

@ -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

View File

@ -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;

View File

@ -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<Player, int> _zombieCorpseCount = new();
public readonly Dictionary<Player, int> 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<MovementBoost>(movementBoostIntensity, 30);
var movementBoostIntensity = (byte)Math.Min(1 + corpsesEaten * 0.5f, 5f);
ev.Player.ReferenceHub.playerEffectsController.ChangeState<MovementBoost>(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<DamageReduction>(damageResistance, 20);
ev.Player.ReferenceHub.playerEffectsController.ChangeState<DamageReduction>((byte)(corpsesEaten*2), float.MaxValue);
// Add regeneration effect after eating multiple corpses
if (corpsesEaten < 5) return;

View File

@ -23,6 +23,9 @@
<Reference Include="Assembly-CSharp">
<HintPath>..\dependencies\Assembly-CSharp.dll</HintPath>
</Reference>
<Reference Include="HintServiceMeow">
<HintPath>..\dependencies\HintServiceMeow-LabAPI.dll</HintPath>
</Reference>
<Reference Include="Mirror">
<HintPath>..\dependencies\Mirror.dll</HintPath>
</Reference>

69
ModInfo/ModInfo.cs Normal file
View File

@ -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<Player, Hint> _spectatorHints = new();
public override void Enable()
{
Timing.RunCoroutine(GodmodeHintLoop());
}
public override void Disable()
{
}
private IEnumerator<float> 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 = "<size=40><color=#50C878>GODMODE</color></size>",
Alignment = HintAlignment.Left,
YCoordinate = 800,
Hide = true
};
var playerDisplay = PlayerDisplay.Get(player);
playerDisplay.AddHint(hint);
_spectatorHints[player] = hint;
return hint;
}
}
}

62
ModInfo/ModInfo.csproj Normal file
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

@ -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<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);
@ -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<float> 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,7 +65,7 @@ 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 =
$" <size=25><color=red>{player.RoleBase.RoleName}</color> | <color=#6761cd>{player.HumeShield}</color> | <color=#da0101>{player.Health}</color> | <color=grey>{player.Zone}</color></size> ";
@ -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 += $"<color=grey>Targets:</color> <color={targetColor}>{tracker.Targets.Count}</color>";
break;
case Scp3114Role scp3114:
{
text += "\n";
var stolenRole = scp3114.CurIdentity.StolenRole;
if (scp3114.Disguised)
{
text += $" {stolenRole}";
break;
}
else
{
text += " None";
}
break;
case Scp079Role scp079:
text =
$" <size=25><color=red>{player.RoleBase.RoleName}</color> | <color=grey>{scp079.CurrentCamera.Room.Zone}</color></size> ";
@ -88,7 +107,16 @@ public class Plugin : LabApi.Loader.Features.Plugins.Plugin
if (!auxManager || !tierManager) break;
text +=
$" <color=grey>AUX: {auxManager.CurrentAuxFloored} / {auxManager.MaxAux} | Level {tierManager.AccessTierLevel}</color>";
$" <color=#FFEF00>AUX: {auxManager.CurrentAuxFloored}</color> / {auxManager.MaxAux} | <color=#FFD700>Level {tierManager.AccessTierLevel}</color>";
break;
case ZombieRole:
var count = GrowingZombies.GrowingZombies.Instance.ZombieCorpseCount[player];
const string corpseColor = "E68A8A";
text += "\n";
text += $" <color=#{corpseColor}>Corpses eaten: {count}</color>";
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;
}

View File

@ -45,4 +45,9 @@
<HintPath>..\dependencies\UnityEngine.CoreModule.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\GrowingZombies\GrowingZombies.csproj" />
</ItemGroup>
</Project>

View File

@ -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

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<Costura>
</Costura>
</Weavers>

View File

@ -0,0 +1,176 @@
<?xml version="1.0" encoding="utf-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. -->
<xs:element name="Weavers">
<xs:complexType>
<xs:all>
<xs:element name="Costura" minOccurs="0" maxOccurs="1">
<xs:complexType>
<xs:all>
<xs:element minOccurs="0" maxOccurs="1" name="ExcludeAssemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element minOccurs="0" maxOccurs="1" name="IncludeAssemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element minOccurs="0" maxOccurs="1" name="ExcludeRuntimeAssemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of runtime assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element minOccurs="0" maxOccurs="1" name="IncludeRuntimeAssemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of runtime assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element minOccurs="0" maxOccurs="1" name="Unmanaged32Assemblies" type="xs:string">
<xs:annotation>
<xs:documentation>Obsolete, use UnmanagedWinX86Assemblies instead</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element minOccurs="0" maxOccurs="1" name="UnmanagedWinX86Assemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of unmanaged X86 (32 bit) assembly names to include, delimited with line breaks.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element minOccurs="0" maxOccurs="1" name="Unmanaged64Assemblies" type="xs:string">
<xs:annotation>
<xs:documentation>Obsolete, use UnmanagedWinX64Assemblies instead.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element minOccurs="0" maxOccurs="1" name="UnmanagedWinX64Assemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of unmanaged X64 (64 bit) assembly names to include, delimited with line breaks.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element minOccurs="0" maxOccurs="1" name="UnmanagedWinArm64Assemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of unmanaged Arm64 (64 bit) assembly names to include, delimited with line breaks.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element minOccurs="0" maxOccurs="1" name="PreloadOrder" type="xs:string">
<xs:annotation>
<xs:documentation>The order of preloaded assemblies, delimited with line breaks.</xs:documentation>
</xs:annotation>
</xs:element>
</xs:all>
<xs:attribute name="CreateTemporaryAssemblies" type="xs:boolean">
<xs:annotation>
<xs:documentation>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.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="IncludeDebugSymbols" type="xs:boolean">
<xs:annotation>
<xs:documentation>Controls if .pdbs for reference assemblies are also embedded.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="IncludeRuntimeReferences" type="xs:boolean">
<xs:annotation>
<xs:documentation>Controls if runtime assemblies are also embedded.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="UseRuntimeReferencePaths" type="xs:boolean">
<xs:annotation>
<xs:documentation>Controls whether the runtime assemblies are embedded with their full path or only with their assembly name.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="DisableCompression" type="xs:boolean">
<xs:annotation>
<xs:documentation>Embedded assemblies are compressed by default, and uncompressed when they are loaded. You can turn compression off with this option.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="DisableCleanup" type="xs:boolean">
<xs:annotation>
<xs:documentation>As part of Costura, embedded assemblies are no longer included as part of the build. This cleanup can be turned off.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="DisableEventSubscription" type="xs:boolean">
<xs:annotation>
<xs:documentation>The attach method no longer subscribes to the `AppDomain.AssemblyResolve` (.NET 4.x) and `AssemblyLoadContext.Resolving` (.NET 6.0+) events.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="LoadAtModuleInit" type="xs:boolean">
<xs:annotation>
<xs:documentation>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.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="IgnoreSatelliteAssemblies" type="xs:boolean">
<xs:annotation>
<xs:documentation>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.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="ExcludeAssemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with |</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="IncludeAssemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of assembly names to include from the default action of "embed all Copy Local references", delimited with |.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="ExcludeRuntimeAssemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of runtime assembly names to exclude from the default action of "embed all Copy Local references", delimited with |</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="IncludeRuntimeAssemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of runtime assembly names to include from the default action of "embed all Copy Local references", delimited with |.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="Unmanaged32Assemblies" type="xs:string">
<xs:annotation>
<xs:documentation>Obsolete, use UnmanagedWinX86Assemblies instead</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="UnmanagedWinX86Assemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of unmanaged X86 (32 bit) assembly names to include, delimited with |.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="Unmanaged64Assemblies" type="xs:string">
<xs:annotation>
<xs:documentation>Obsolete, use UnmanagedWinX64Assemblies instead</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="UnmanagedWinX64Assemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of unmanaged X64 (64 bit) assembly names to include, delimited with |.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="UnmanagedWinArm64Assemblies" type="xs:string">
<xs:annotation>
<xs:documentation>A list of unmanaged Arm64 (64 bit) assembly names to include, delimited with |.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="PreloadOrder" type="xs:string">
<xs:annotation>
<xs:documentation>The order of preloaded assemblies, delimited with |.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
</xs:element>
</xs:all>
<xs:attribute name="VerifyAssembly" type="xs:boolean">
<xs:annotation>
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="VerifyIgnoreCodes" type="xs:string">
<xs:annotation>
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="GenerateXsd" type="xs:boolean">
<xs:annotation>
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
</xs:element>
</xs:schema>

View File

@ -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<string, PlayerStats> _currentSessionStats = new();
private class PlayerStats
{
public int Kills { get; set; }
public int Deaths { get; set; }
public Dictionary<ItemType, int> 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;
}
}
}
}

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="Costura.Fody" Version="6.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Northwood.LabAPI" Version="1.0.2"/>
<PackageReference Include="System.Data.SQLite" Version="1.0.119" />
</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

@ -0,0 +1,75 @@
using PlayerRoles;
using LabApi.Features;
using LabApi.Features.Wrappers;
namespace VisibleSpectators;
/// <summary>
/// Utility for formatting player display names and color mapping.
/// </summary>
public static class PlayerDisplayUtil
{
private static readonly Dictionary<string, string> 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" }
};
/// <summary>
/// Returns a formatted display string for a player, with color.
/// </summary>
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 $"<color=#{defaultColor}FF>{player.DisplayName}</color>";
return ColorMap.TryGetValue(groupColor.ToUpper(), out var color)
? $"<color=#{color}FF>{player.DisplayName}</color>"
: $"<color=#{defaultColor}FF>{player.DisplayName}</color>";
}
catch
{
return $"<color=#{defaultColor}FF>{player.DisplayName}</color>";
}
}
/// <summary>
/// Returns true if the player is not Overwatch.
/// </summary>
public static bool IsNotOverwatch(Player player)
{
return player != null && player.Role != RoleTypeId.Overwatch;
}
}

View File

@ -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;
/// <summary>
/// Main entry point for the VisibleSpectators plugin.
/// </summary>
public class Plugin : Plugin<SpectatorConfig>
{
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;
}
}

View File

@ -0,0 +1,16 @@
namespace VisibleSpectators;
/// <summary>
/// Configuration for the VisibleSpectators plugin.
/// </summary>
public class SpectatorConfig
{
/// <summary>
/// Header message shown above the spectator list.
/// </summary>
public string HeaderMessage { get; set; } = "Spectators:";
/// <summary>
/// Message shown when there are no spectators.
/// </summary>
public string NoSpectatorsMessage { get; set; } = "No spectators";
}

View File

@ -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;
/// <summary>
/// Handles spectator hint management and updates for players.
/// </summary>
public class SpectatorManager
{
private readonly SpectatorConfig _config;
private readonly Dictionary<Player, Hint> _spectatorHints = new();
public int YCoordinate { get; set; } = 100;
public SpectatorManager(SpectatorConfig config)
{
_config = config;
}
public IEnumerator<float> 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<string>());
}
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();
}
}

View File

@ -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<SpectatorConfig>
{
private static Plugin _singleton;
private static readonly Dictionary<string, string> 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<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);
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 $"<color=#{defaultColor}FF>{player.DisplayName}</color>";
return GetColorMap.TryGetValue(groupColor.ToUpper(), out var color)
? $"<color=#{color}FF>{player.DisplayName}</color>"
: $"<color=#{defaultColor}FF>{player.DisplayName}</color>";
}
catch
{
return $"<color=#{defaultColor}FF>{player.DisplayName}</color>";
}
}
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<string>());
}
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";
}