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