idk man
This commit is contained in:
+183
-116
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user