diff --git a/.gitignore b/.gitignore index dbd3f10..6a179d2 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ obj/ *.user *.dll fuchsbau/ +**/target/ diff --git a/CustomClasses/CustomClasses.cs b/CustomClasses/CustomClasses.cs index 8c40172..9713a94 100644 --- a/CustomClasses/CustomClasses.cs +++ b/CustomClasses/CustomClasses.cs @@ -1,3 +1,5 @@ +using System.Diagnostics.CodeAnalysis; +using CustomPlayerEffects; using Interactables.Interobjects.DoorUtils; using InventorySystem.Items; using InventorySystem.Items.Firearms.Modules; @@ -61,6 +63,11 @@ public sealed class CustomClasses : Plugin /// public GamblerConfig GamblerConfig { get; private set; } = new(); + /// + /// Configuration for the ShadowStepper class. + /// + public ShadowStepperConfig ShadowStepperConfig { get; private set; } = new(); + /// public override void Enable() { @@ -68,6 +75,12 @@ public sealed class CustomClasses : Plugin ServerEvents.RoundEnded += OnRoundEnded; Scp914Events.ProcessingPickup += OnScp914ProcessingPickup; Scp914Events.ProcessingInventoryItem += OnScp914ProcessingInventoryItem; + PlayerEvents.Escaped += OnEscaped; + } + + private void OnEscaped(PlayerEscapedEventArgs ev) + { + } /// @@ -77,6 +90,7 @@ public sealed class CustomClasses : Plugin ServerEvents.RoundEnded -= OnRoundEnded; Scp914Events.ProcessingPickup -= OnScp914ProcessingPickup; Scp914Events.ProcessingInventoryItem -= OnScp914ProcessingInventoryItem; + PlayerEvents.Escaped -= OnEscaped; } private void OnRoundEnded(RoundEndedEventArgs ev) @@ -84,6 +98,7 @@ public sealed class CustomClasses : Plugin _classManager.ResetSpawnStates(); } + [SuppressMessage("ReSharper", "RedundantJumpStatement")] private void OnPlayerSpawned(PlayerSpawnedEventArgs ev) { if (_classManager.TryHandleSpawn(ev.Player, JanitorConfig, typeof(JanitorConfig))) return; @@ -91,6 +106,7 @@ public sealed class CustomClasses : Plugin if (_classManager.TryHandleSpawn(ev.Player, HeadGuardConfig, typeof(HeadGuardConfig))) return; if (_classManager.TryHandleSpawn(ev.Player, MedicConfig, typeof(MedicConfig))) return; if (_classManager.TryHandleSpawn(ev.Player, GamblerConfig, typeof(GamblerConfig))) return; + if (_classManager.TryHandleSpawn(ev.Player, ShadowStepperConfig, typeof(ShadowStepperConfig))) return; } private static void OnScp914ProcessingPickup(Scp914ProcessingPickupEventArgs ev) @@ -133,6 +149,7 @@ public class CustomClassManager RegisterHandler(new HeadGuardHandler()); RegisterHandler(new MedicHandler()); RegisterHandler(new GamblerHandler()); + RegisterHandler(new ShadowStepperHandler()); } /// @@ -219,14 +236,26 @@ public interface ICustomClassHandler /// The configuration for the custom class. /// A random number generator. void HandleSpawn(Player player, CustomClassConfig config, Random random); + + void HandleEscape(Player player, CustomClassConfig config); +} + +public abstract class CustomClassHandler: ICustomClassHandler +{ + public abstract void HandleSpawn(Player player, CustomClassConfig config, Random random); + + public virtual void HandleEscape(Player player, CustomClassConfig config) + { + //Intentionally left blank + } } /// /// Handler for the Janitor custom class. /// -public class JanitorHandler(CustomClassManager manager) : ICustomClassHandler +public class JanitorHandler(CustomClassManager manager) : CustomClassHandler { - public void HandleSpawn(Player player, CustomClassConfig config, Random random) + public override void HandleSpawn(Player player, CustomClassConfig config, Random random) { var scp914 = Map.Rooms.FirstOrDefault(r => r.Name == RoomName.Lcz914); if (scp914 == null) @@ -247,9 +276,9 @@ public class JanitorHandler(CustomClassManager manager) : ICustomClassHandler /// /// Handler for the Research Subject custom class. /// -public class ResearchSubjectHandler(CustomClassManager manager) : ICustomClassHandler +public class ResearchSubjectHandler(CustomClassManager manager) : CustomClassHandler { - public void HandleSpawn(Player player, CustomClassConfig config, Random random) + public override void HandleSpawn(Player player, CustomClassConfig config, Random random) { var scientist = Player.ReadyList.FirstOrDefault(p => p.Role == RoleTypeId.Scientist); if (scientist == null) @@ -270,9 +299,9 @@ public class ResearchSubjectHandler(CustomClassManager manager) : ICustomClassHa /// /// Handler for the Head Guard custom class. /// -public class HeadGuardHandler : ICustomClassHandler +public class HeadGuardHandler : CustomClassHandler { - public void HandleSpawn(Player player, CustomClassConfig config, Random random) + public override void HandleSpawn(Player player, CustomClassConfig config, Random random) { player.RemoveItem(ItemType.KeycardGuard); KeycardItem.CreateCustomKeycardTaskForce(player, "Head Guard Keycard", $"HG. {player.Nickname}", new KeycardLevels(1, 1, 2), Color.blue, Color.cyan, "1", 0); @@ -305,9 +334,9 @@ public class HeadGuardHandler : ICustomClassHandler /// /// Handler for the Medic custom class. /// -public class MedicHandler : ICustomClassHandler +public class MedicHandler : CustomClassHandler { - public void HandleSpawn(Player player, CustomClassConfig config, Random random) + public override void HandleSpawn(Player player, CustomClassConfig config, Random random) { foreach (var spawnItem in config.Items) { @@ -318,12 +347,38 @@ public class MedicHandler : ICustomClassHandler } } +/// +/// Handler for ShadowStepper custom class. +/// +public class ShadowStepperHandler : CustomClassHandler +{ + public override void HandleSpawn(Player player, CustomClassConfig config, Random random) + { + ApplyEffects(player); + + player.SendBroadcast("You're a ShadowStepper!", 3); + } + + public override void HandleEscape(Player player, CustomClassConfig config) + { + base.HandleEscape(player, config); + + ApplyEffects(player); + } + + private static void ApplyEffects(Player player) + { + player.ReferenceHub.playerEffectsController.ChangeState(100,float.MaxValue); + player.ReferenceHub.playerEffectsController.ChangeState(20,float.MaxValue); + } +} + /// /// Base handler for simple item-giving custom classes. /// -public abstract class SimpleAddItemHandler : ICustomClassHandler +public abstract class SimpleAddItemHandler : CustomClassHandler { - public virtual void HandleSpawn(Player player, CustomClassConfig config, Random random) + public override void HandleSpawn(Player player, CustomClassConfig config, Random random) { foreach (var spawnItem in config.Items) { @@ -375,7 +430,7 @@ public abstract class CustomClassConfig /// /// Configuration for the Research Subject class. /// -public sealed class ResearchSubjectConfig : CustomClassConfig { } +public sealed class ResearchSubjectConfig : CustomClassConfig; /// /// Configuration for the Janitor class. @@ -419,6 +474,14 @@ public sealed class GamblerConfig : CustomClassConfig public override ItemType[] Items { get; set; } = [ItemType.Coin, ItemType.Coin]; } +/// +/// Configuration for the Shadow Stepper class. +/// +public sealed class ShadowStepperConfig : CustomClassConfig +{ + +} + /// /// Tracks the spawn state for a custom class. /// diff --git a/GrowingZombies/GrowingZombies.cs b/GrowingZombies/GrowingZombies.cs index d51a98d..c9a6892 100644 --- a/GrowingZombies/GrowingZombies.cs +++ b/GrowingZombies/GrowingZombies.cs @@ -70,7 +70,11 @@ public class GrowingZombies : Plugin // Add damage resistance after eating multiple corpses if (corpsesEaten >= 3) - ev.Player.ReferenceHub.playerEffectsController.ChangeState((byte)(corpsesEaten*2), float.MaxValue); + { + var damageReductionIntensity = (byte)Math.Min(corpsesEaten * 2, 100); // Half-Percent + ev.Player.ReferenceHub.playerEffectsController.ChangeState(damageReductionIntensity, + float.MaxValue); + } // Add regeneration effect after eating multiple corpses if (corpsesEaten < 5) return; diff --git a/SCPTeamHint/SCPTeamHint.cs b/SCPTeamHint/SCPTeamHint.cs index 7bded00..96bbb8f 100644 --- a/SCPTeamHint/SCPTeamHint.cs +++ b/SCPTeamHint/SCPTeamHint.cs @@ -59,109 +59,131 @@ public class Plugin : LabApi.Loader.Features.Plugins.Plugin PlayerEvents.Left -= OnLeft; } - private void UpdateHints() + private static string CollectHint() { var hintTexts = new List(); - - lock (_hintsLock) + + foreach (var player in Player.ReadyList.Where(x => !x.IsDummy && (x.IsSCP || x.Role is RoleTypeId.Scp0492))) { - 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} "; + var text = + $" {player.RoleBase.RoleName} | {player.HumeShield} | {player.Health} | {player.Zone} "; - switch (player.RoleBase) - { - case Scp096Role scp: - text += "\n"; - - scp.SubroutineModule.TryGetSubroutine(out Scp096TargetsTracker tracker); - - if (!tracker) break; - - var targetColor = tracker.Targets.Count > 0 ? "red" : "grey"; - text += $"Targets: {tracker.Targets.Count}"; - break; - case Scp3114Role scp3114: - text += "\n"; - - var stolenRole = scp3114.CurIdentity.StolenRole; - - if (scp3114.Disguised) - { - text += $" {stolenRole}"; - } - else - { - text += " None"; - } - break; - case Scp079Role scp079: - text = - $" {player.RoleBase.RoleName} | {scp079.CurrentCamera.Room.Zone} "; - text += "\n"; - - scp079.SubroutineModule.TryGetSubroutine(out Scp079AuxManager auxManager); - scp079.SubroutineModule.TryGetSubroutine(out Scp079TierManager tierManager); - - if (!auxManager || !tierManager) break; - - text += - $" 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; - } - - hintTexts.Add(text); - } - - var hintText = string.Join("\n", hintTexts); - - foreach (var player in Player.ReadyList.Where(x => !x.IsHost && !x.IsDummy)) + switch (player.RoleBase) { - Logger.Debug($"Updating hint for {player.DisplayName}"); - UpdateHint(player, hintText); + case Scp096Role scp: + text += "\n"; + + scp.SubroutineModule.TryGetSubroutine(out Scp096TargetsTracker tracker); + + if (!tracker) break; + + var targetColor = tracker.Targets.Count > 0 ? "red" : "grey"; + text += $"Targets: {tracker.Targets.Count}"; + break; + case Scp3114Role scp3114: + text += "\n"; + + var stolenRole = scp3114.CurIdentity.StolenRole; + + if (scp3114.Disguised) + { + text += $" {stolenRole}"; + } + else + { + text += " None"; + } + break; + case Scp079Role scp079: + text = + $" {player.RoleBase.RoleName} | {scp079.CurrentCamera.Room.Zone} "; + text += "\n"; + + scp079.SubroutineModule.TryGetSubroutine(out Scp079AuxManager auxManager); + scp079.SubroutineModule.TryGetSubroutine(out Scp079TierManager tierManager); + + if (!auxManager || !tierManager) break; + + text += + $" 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; } + + hintTexts.Add(text); } + + return string.Join("\n", hintTexts); + } + + private void UpdateHints() + { + var hintText = CollectHint(); + + foreach (var player in Player.ReadyList.Where(x => !x.IsDummy)) + { + try + { + UpdateHint(player, hintText); + } catch (Exception e) + { + Logger.Warn("Caught exception while updating hint for player"); + Logger.Error(e); + } + } } private void UpdateHint(Player player, string hintText) { - if (!_spectatorHints.TryGetValue(player, out var hint)) + bool isContained; + lock (_hintsLock) { - Logger.Debug($"No hint found for player {player.DisplayName}"); - return; + isContained = _spectatorHints.ContainsKey(player); + } + + if (!isContained) + { + CreateHint(player); } - Logger.Debug( - $"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; + if (_spectatorHints == null) return; + lock (_hintsLock) + { + var hint = _spectatorHints[player]; + + hint.Hide = player.RoleBase.Team != Team.SCPs && player.Role != RoleTypeId.Scp0492 && player.Role != RoleTypeId.Overwatch; + if (!hint.Hide) hint.Text = hintText; + } } private void OnJoin(PlayerJoinedEventArgs ev) { if (ev.Player.IsDummy || ev.Player.IsHost) return; + CreateHint(ev.Player); + } + + private void CreateHint(Player player) + { var hint = new Hint { Text = "", Alignment = HintAlignment.Left, YCoordinate = 100, Hide = true }; - var playerDisplay = PlayerDisplay.Get(ev.Player); + var playerDisplay = PlayerDisplay.Get(player); playerDisplay.AddHint(hint); lock (_hintsLock) { - _spectatorHints[ev.Player] = hint; + _spectatorHints[player] = hint; } } diff --git a/StatsTracker/FodyWeavers.xml b/StatsTracker/FodyWeavers.xml deleted file mode 100644 index 37fe15b..0000000 --- a/StatsTracker/FodyWeavers.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/StatsTracker/FodyWeavers.xsd b/StatsTracker/FodyWeavers.xsd deleted file mode 100644 index f2dbece..0000000 --- a/StatsTracker/FodyWeavers.xsd +++ /dev/null @@ -1,176 +0,0 @@ - - - - - - - - - - - - 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/Rust/Cargo.lock b/StatsTracker/Rust/Cargo.lock new file mode 100644 index 0000000..fc45299 --- /dev/null +++ b/StatsTracker/Rust/Cargo.lock @@ -0,0 +1,97 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "bincode" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740" +dependencies = [ + "bincode_derive", + "serde", + "unty", +] + +[[package]] +name = "bincode_derive" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf95709a440f45e986983918d0e8a1f30a9b1df04918fc828670606804ac3c09" +dependencies = [ + "virtue", +] + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "stats_tracker" +version = "0.1.0" +dependencies = [ + "bincode", +] + +[[package]] +name = "syn" +version = "2.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "unty" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" + +[[package]] +name = "virtue" +version = "0.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1" diff --git a/StatsTracker/Rust/Cargo.toml b/StatsTracker/Rust/Cargo.toml new file mode 100644 index 0000000..0134587 --- /dev/null +++ b/StatsTracker/Rust/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "stats_tracker" +version = "0.1.0" +edition = "2024" + +[dependencies] +bincode = "2.0" + +[lib] +crate-type = ["cdylib"] +name = "stats_tracker" + +[profile.release] +opt-level = "z" +codegen-units = 1 +lto = "fat" +overflow-checks = false +panic = "abort" diff --git a/StatsTracker/Rust/src/lib.rs b/StatsTracker/Rust/src/lib.rs new file mode 100644 index 0000000..c4ab36a --- /dev/null +++ b/StatsTracker/Rust/src/lib.rs @@ -0,0 +1,98 @@ +use std::{ + ffi::{CStr, c_char}, + fs::File, +}; + +use bincode::{Decode, Encode}; + +#[derive(Decode, Encode, Clone)] +#[repr(C)] +pub struct PlayerStat { + kills: u32, + deaths: u32, + team_damage: u32, +} + +#[derive(Decode, Encode, Clone, Default)] +#[repr(C)] +pub struct ItemStat { + item_name: String, + item_count: u32, +} + +#[derive(Decode, Encode, Clone)] +#[repr(C)] +pub struct Player { + player_id: String, + player_stats: PlayerStat, + player_items: [ItemStat; 256], +} + +impl Default for Player { + fn default() -> Self { + Self { + player_id: String::new(), + player_stats: PlayerStat { + kills: 0, + deaths: 0, + team_damage: 0, + }, + player_items: std::array::from_fn(|_| ItemStat::default()), + } + } +} + +#[unsafe(no_mangle)] +/// # Safety +/// `player_id` must be a valid, null-terminated C string pointer. +pub unsafe extern "C" fn get_player_stats(player_id: *const c_char) -> *const Player { + let player_id = unsafe { CStr::from_ptr(player_id) } + .to_string_lossy() + .into_owned(); + + let db_location = "./stats.data"; + + let player_stats: Vec = match File::open(db_location) { + Ok(mut data) => bincode::decode_from_std_read(&mut data, bincode::config::standard()) + .unwrap_or_default(), + Err(_) => Vec::new(), + }; + + player_stats + .iter() + .find(|p| p.player_id == player_id) + .unwrap_or(&Player::default()) +} + +#[unsafe(no_mangle)] +/// # Safety +/// `player` must be a valid pointer to a `Player` struct. +pub unsafe extern "C" fn save_player_stats(player: *const Player) -> bool { + let player = unsafe { &*player }; + let db_location = "./stats.data"; + + let mut player_stats: Vec = match File::open(db_location) { + Ok(mut data) => bincode::decode_from_std_read(&mut data, bincode::config::standard()) + .unwrap_or_default(), + Err(_) => Vec::new(), + }; + + // Find and update existing player or add new player + if let Some(existing_player) = player_stats + .iter_mut() + .find(|p| p.player_id == player.player_id) + { + *existing_player = player.clone(); + } else { + player_stats.push(player.clone()); + } + + // Save updated stats + match File::create(db_location) { + Ok(mut file) => { + bincode::encode_into_std_write(&player_stats, &mut file, bincode::config::standard()) + .is_ok() + } + Err(_) => false, + } +} diff --git a/StatsTracker/StatsTracker.cs b/StatsTracker/StatsTracker.cs index 92983ad..3399a9c 100644 --- a/StatsTracker/StatsTracker.cs +++ b/StatsTracker/StatsTracker.cs @@ -1,13 +1,40 @@ using LabApi.Features; using LabApi.Loader.Features.Plugins; -using System.Collections.Concurrent; -using System.Data.SQLite; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using System.Runtime.InteropServices; using LabApi.Events.Arguments.PlayerEvents; using LabApi.Events.Handlers; -using LabApi.Loader; +using LabApi.Features.Console; namespace StatsTracker { + + + [SuppressMessage("ReSharper", "InconsistentNaming")] + public struct PlayerStat + { + public uint kills; + public uint deaths; + public uint team_damage; + } + + [SuppressMessage("ReSharper", "InconsistentNaming")] + public struct ItemStat + { + public string item_name; + public uint item_count; + } + + [SuppressMessage("ReSharper", "InconsistentNaming")] + public struct Player + { + public string player_id; + public PlayerStat player_stats; + public ItemStat[] player_items; + } + + [SuppressMessage("ReSharper", "InconsistentNaming")] public class StatsTracker : Plugin { public override string Name => "StatsTracker"; @@ -15,148 +42,188 @@ namespace StatsTracker 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(); - } + + private const string RustDllName = "stats_tracker"; + + [DllImport(RustDllName, EntryPoint="get_player_stats", CallingConvention = CallingConvention.Cdecl)] + private static extern ref Player GetPlayerStats(ref string player_id); + + [DllImport(RustDllName, EntryPoint="save_player_stats", CallingConvention = CallingConvention.Cdecl)] + private static extern bool SavePlayerStats(ref Player player); public override void Enable() { - _dbPath = Path.Combine(this.GetConfigDirectory().FullName, "stats.db"); - InitializeDatabase(); + var pathVariable = Environment.GetEnvironmentVariable("PATH"); + Logger.Debug($"PATH: {pathVariable}"); + + var extractedDllPath = ExtractRustDll(); // Call extraction + if (string.IsNullOrEmpty(extractedDllPath)) + { + Logger.Error("Failed to extract Rust DLL. Exiting."); + return; + } + var libDirectory = Path.GetDirectoryName(extractedDllPath); + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + SetDllDirectory(libDirectory); + Logger.Info($"Windows: Added '{libDirectory}' to DLL search path."); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + + Environment.SetEnvironmentVariable("LD_LIBRARY_PATH", libDirectory + ":" + Environment.GetEnvironmentVariable("LD_LIBRARY_PATH")); + + Logger.Info($"Linux: Extracted library to '{libDirectory}'. Relying on default search paths and manual LD_LIBRARY_PATH."); + } + else + { + Logger.Error("Only windows and linux are supported."); + return; + } + PlayerEvents.Death += OnPlayerDied; PlayerEvents.UsedItem += OnItemUsed; - PlayerEvents.Left += OnPlayerLeft; + PlayerEvents.Hurt += OnHurt; + } + + // Conditional P/Invoke for OS-specific functions (like SetDllDirectory on Windows) + [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true, ExactSpelling = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool SetDllDirectory(string lpPathName); + + + private static string ExtractRustDll() + { + string dllFilename; + string resourceSubPath; // Folder inside NativeLibs + string extractedLibFilename; + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + dllFilename = $"{RustDllName}.dll"; + resourceSubPath = "x86_64_pc_windows_gnu"; + extractedLibFilename = dllFilename; // On Windows, we keep the original name + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + dllFilename = $"lib{RustDllName}.so"; // Linux uses lib prefix and .so suffix + resourceSubPath = "x86_64_unknown_linux_gnu"; + extractedLibFilename = dllFilename; // On Linux, keep the lib*.so name + } + else + { + Logger.Error("Unsupported operating system detected."); + return null; + } + + // Determine where to extract the DLL. + var targetDirectory = AppDomain.CurrentDomain.BaseDirectory; + + var extractedLibPath = Path.Combine(targetDirectory, extractedLibFilename); + + // Check if the DLL already exists (e.g., from a previous run or development environment) + if (File.Exists(extractedLibPath)) + { + Logger.Warn($"Rust lib '{dllFilename}' already exists at '{extractedLibPath}'. Skipping extraction."); + return extractedLibPath; // Return the path directly if it exists + } + + try + { + // Adjust this resource name to match your actual embedded resource name. + // Based on your previous comment: + var resourceName = $"StatsTracker.Rust.target.{resourceSubPath}.release.{dllFilename}"; + + Logger.Info($"Attempting to load embedded resource: {resourceName}"); + + using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName)) + { + if (stream == null) + { + Logger.Error($"Error: Embedded resource '{resourceName}' not found."); + Logger.Info("Available resources:"); + foreach (var res in Assembly.GetExecutingAssembly().GetManifestResourceNames()) + { + Logger.Info($"- {res}"); + } + return null; // Return null to indicate failure + } + + using (var fileStream = File.Create(extractedLibPath)) + { + stream.CopyTo(fileStream); + } + } + Logger.Info($"Successfully extracted '{RustDllName}' to '{extractedLibPath}'."); + return extractedLibPath; // Return the path after successful extraction + } + catch (Exception ex) + { + Logger.Error($"Error extracting Rust DLL to {extractedLibPath}: {ex.Message}"); + return null; // Return null to indicate failure + } } 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() + private static void OnHurt(PlayerHurtEventArgs ev) { - 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) + switch (ev.Attacker) { - var killerStats = _currentSessionStats.GetOrAdd(ev.Attacker.UserId, _ => new PlayerStats()); - killerStats.Kills++; + case null: + case { DoNotTrack: true }: + return; } - 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(ev.Attacker.Team != ev.Player.Team) return; - if (!stats.ItemUsage.ContainsKey(ev.UsableItem.Type)) - stats.ItemUsage[ev.UsableItem.Type] = 0; - - stats.ItemUsage[ev.UsableItem.Type]++; + var userId = ev.Attacker.UserId; + var killerStats = GetPlayerStats(ref userId); + killerStats.player_stats.team_damage++; + SavePlayerStats(ref killerStats); } - private void OnPlayerLeft(PlayerLeftEventArgs ev) + private static void OnPlayerDied(PlayerDeathEventArgs ev) { - if (_currentSessionStats.TryRemove(ev.Player.UserId, out var stats)) + if (ev.Attacker != null && ev.Attacker.Nickname != "emeraldo" && !ev.Attacker.DoNotTrack) { - SavePlayerStats(ev.Player.UserId, stats); + var userId = ev.Attacker.UserId; + var killerStats = GetPlayerStats(ref userId); + killerStats.player_stats.kills++; + SavePlayerStats(ref killerStats); } + + if (ev.Player.DoNotTrack) + { + Logger.Debug($"Do Not Track: {ev.Player.Nickname}"); + return; + } + var victimId = ev.Player.UserId; + var victimStats = GetPlayerStats(ref victimId); + victimStats.player_stats.deaths++; + SavePlayerStats(ref victimStats); } - private void SavePlayerStats(string userId, PlayerStats stats) + private static void OnItemUsed(PlayerUsedItemEventArgs ev) { - using var connection = new SQLiteConnection($"Data Source={_dbPath}"); - connection.Open(); - using var transaction = connection.BeginTransaction(); - - try + if (ev.Player.DoNotTrack) { - // 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; + Logger.Debug($"Do Not Track: {ev.Player.Nickname}"); + return; } + var userId = ev.Player.UserId; + var stats = GetPlayerStats(ref userId); + + var stat = stats.player_items[(int)ev.UsableItem.Type]; + + stat.item_count++; + stat.item_name = ev.UsableItem.Type.ToString(); } } } \ No newline at end of file diff --git a/StatsTracker/StatsTracker.csproj b/StatsTracker/StatsTracker.csproj index 22c47cb..c197370 100644 --- a/StatsTracker/StatsTracker.csproj +++ b/StatsTracker/StatsTracker.csproj @@ -5,6 +5,7 @@ enable disable latest + true @@ -21,14 +22,18 @@ - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - + + + - + + + + + + @@ -59,4 +64,4 @@ ..\dependencies\UnityEngine.CoreModule.dll - + \ No newline at end of file