This commit is contained in:
2025-06-09 22:13:12 +02:00
parent 326b99c464
commit 0aee847089
11 changed files with 583 additions and 389 deletions
-5
View File
@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<Costura>
</Costura>
</Weavers>
-176
View File
@@ -1,176 +0,0 @@
<?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>
+97
View File
@@ -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"
+18
View File
@@ -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"
+98
View File
@@ -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<Player> = 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<Player> = 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,
}
}
+183 -116
View File
@@ -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<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();
}
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();
}
}
}
+11 -6
View File
@@ -5,6 +5,7 @@
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>disable</Nullable>
<LangVersion>latest</LangVersion>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
@@ -21,14 +22,18 @@
<ItemGroup>
<PackageReference Include="Costura.Fody" Version="6.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<EmbeddedResource Include="Rust/target/x86_64-pc-windows-gnu/release/stats_tracker.dll" />
<EmbeddedResource Include="Rust/target/x86_64-unknown-linux-gnu/release/libstats_tracker.so" />
<PackageReference Include="Northwood.LabAPI" Version="1.0.2"/>
<PackageReference Include="System.Data.SQLite" Version="1.0.119" />
</ItemGroup>
<Target Name="RustBuild" BeforeTargets="PrepareForBuild">
<Exec Command="echo 'Configuration: $(Configuration)'"/>
<Exec Command="cargo build -r --target x86_64-pc-windows-gnu" WorkingDirectory="./Rust"/>
<Exec Command="cargo build -r --target x86_64-unknown-linux-gnu" WorkingDirectory="./Rust"/>
</Target>
<ItemGroup>
<Reference Include="0Harmony">
@@ -59,4 +64,4 @@
<HintPath>..\dependencies\UnityEngine.CoreModule.dll</HintPath>
</Reference>
</ItemGroup>
</Project>
</Project>