idk man
This commit is contained in:
parent
326b99c464
commit
0aee847089
1
.gitignore
vendored
1
.gitignore
vendored
@ -4,3 +4,4 @@ obj/
|
||||
*.user
|
||||
*.dll
|
||||
fuchsbau/
|
||||
**/target/
|
||||
|
@ -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
|
||||
/// </summary>
|
||||
public GamblerConfig GamblerConfig { get; private set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Configuration for the ShadowStepper class.
|
||||
/// </summary>
|
||||
public ShadowStepperConfig ShadowStepperConfig { get; private set; } = new();
|
||||
|
||||
/// <inheritdoc/>
|
||||
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)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@ -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<HeadGuardConfig>(new HeadGuardHandler());
|
||||
RegisterHandler<MedicConfig>(new MedicHandler());
|
||||
RegisterHandler<GamblerConfig>(new GamblerHandler());
|
||||
RegisterHandler<ShadowStepperConfig>(new ShadowStepperHandler());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -219,14 +236,26 @@ public interface ICustomClassHandler
|
||||
/// <param name="config">The configuration for the custom class.</param>
|
||||
/// <param name="random">A random number generator.</param>
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handler for the Janitor custom class.
|
||||
/// </summary>
|
||||
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
|
||||
/// <summary>
|
||||
/// Handler for the Research Subject custom class.
|
||||
/// </summary>
|
||||
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
|
||||
/// <summary>
|
||||
/// Handler for the Head Guard custom class.
|
||||
/// </summary>
|
||||
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
|
||||
/// <summary>
|
||||
/// Handler for the Medic custom class.
|
||||
/// </summary>
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handler for ShadowStepper custom class.
|
||||
/// </summary>
|
||||
public class ShadowStepperHandler : CustomClassHandler
|
||||
{
|
||||
public override void HandleSpawn(Player player, CustomClassConfig config, Random random)
|
||||
{
|
||||
ApplyEffects(player);
|
||||
|
||||
player.SendBroadcast("You're a <color=#000000>ShadowStepper</color>!", 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<SilentWalk>(100,float.MaxValue);
|
||||
player.ReferenceHub.playerEffectsController.ChangeState<Slowness>(20,float.MaxValue);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Base handler for simple item-giving custom classes.
|
||||
/// </summary>
|
||||
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
|
||||
/// <summary>
|
||||
/// Configuration for the Research Subject class.
|
||||
/// </summary>
|
||||
public sealed class ResearchSubjectConfig : CustomClassConfig { }
|
||||
public sealed class ResearchSubjectConfig : CustomClassConfig;
|
||||
|
||||
/// <summary>
|
||||
/// Configuration for the Janitor class.
|
||||
@ -419,6 +474,14 @@ public sealed class GamblerConfig : CustomClassConfig
|
||||
public override ItemType[] Items { get; set; } = [ItemType.Coin, ItemType.Coin];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configuration for the Shadow Stepper class.
|
||||
/// </summary>
|
||||
public sealed class ShadowStepperConfig : CustomClassConfig
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tracks the spawn state for a custom class.
|
||||
/// </summary>
|
||||
|
@ -70,7 +70,11 @@ public class GrowingZombies : Plugin
|
||||
|
||||
// Add damage resistance after eating multiple corpses
|
||||
if (corpsesEaten >= 3)
|
||||
ev.Player.ReferenceHub.playerEffectsController.ChangeState<DamageReduction>((byte)(corpsesEaten*2), float.MaxValue);
|
||||
{
|
||||
var damageReductionIntensity = (byte)Math.Min(corpsesEaten * 2, 100); // Half-Percent
|
||||
ev.Player.ReferenceHub.playerEffectsController.ChangeState<DamageReduction>(damageReductionIntensity,
|
||||
float.MaxValue);
|
||||
}
|
||||
|
||||
// Add regeneration effect after eating multiple corpses
|
||||
if (corpsesEaten < 5) return;
|
||||
|
@ -59,13 +59,11 @@ public class Plugin : LabApi.Loader.Features.Plugins.Plugin
|
||||
PlayerEvents.Left -= OnLeft;
|
||||
}
|
||||
|
||||
private void UpdateHints()
|
||||
private static string CollectHint()
|
||||
{
|
||||
var hintTexts = new List<string>();
|
||||
|
||||
lock (_hintsLock)
|
||||
{
|
||||
foreach (var player in Player.ReadyList.Where(x => !x.IsHost && !x.IsDummy && (x.IsSCP || x.Role is RoleTypeId.Scp0492)))
|
||||
foreach (var player in Player.ReadyList.Where(x => !x.IsDummy && (x.IsSCP || x.Role is RoleTypeId.Scp0492)))
|
||||
{
|
||||
var text =
|
||||
$" <size=25><color=red>{player.RoleBase.RoleName}</color> | <color=#6761cd>{player.HumeShield}</color> | <color=#da0101>{player.Health}</color> | <color=grey>{player.Zone}</color></size> ";
|
||||
@ -123,45 +121,69 @@ public class Plugin : LabApi.Loader.Features.Plugins.Plugin
|
||||
hintTexts.Add(text);
|
||||
}
|
||||
|
||||
var hintText = string.Join("\n", hintTexts);
|
||||
return string.Join("\n", hintTexts);
|
||||
}
|
||||
|
||||
foreach (var player in Player.ReadyList.Where(x => !x.IsHost && !x.IsDummy))
|
||||
private void UpdateHints()
|
||||
{
|
||||
var hintText = CollectHint();
|
||||
|
||||
foreach (var player in Player.ReadyList.Where(x => !x.IsDummy))
|
||||
{
|
||||
try
|
||||
{
|
||||
Logger.Debug($"Updating hint for {player.DisplayName}");
|
||||
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);
|
||||
}
|
||||
|
||||
Logger.Debug(
|
||||
$"Player {player.Nickname} is on team {player.RoleBase.Team} with Role {player.Role} | hide: {player.RoleBase.Team != Team.SCPs}");
|
||||
if (!isContained)
|
||||
{
|
||||
CreateHint(player);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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>
|
@ -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
StatsTracker/Rust/Cargo.lock
generated
Normal file
97
StatsTracker/Rust/Cargo.lock
generated
Normal 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
StatsTracker/Rust/Cargo.toml
Normal file
18
StatsTracker/Rust/Cargo.toml
Normal 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
StatsTracker/Rust/src/lib.rs
Normal file
98
StatsTracker/Rust/src/lib.rs
Normal 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,
|
||||
}
|
||||
}
|
@ -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";
|
||||
@ -16,147 +43,187 @@ namespace StatsTracker
|
||||
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 const string RustDllName = "stats_tracker";
|
||||
|
||||
private class PlayerStats
|
||||
{
|
||||
public int Kills { get; set; }
|
||||
public int Deaths { get; set; }
|
||||
public Dictionary<ItemType, int> ItemUsage { get; set; } = new();
|
||||
}
|
||||
[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();
|
||||
switch (ev.Attacker)
|
||||
{
|
||||
case null:
|
||||
case { DoNotTrack: true }:
|
||||
return;
|
||||
}
|
||||
|
||||
private void OnPlayerDied(PlayerDeathEventArgs ev)
|
||||
{
|
||||
if (ev.Attacker != null)
|
||||
{
|
||||
var killerStats = _currentSessionStats.GetOrAdd(ev.Attacker.UserId, _ => new PlayerStats());
|
||||
killerStats.Kills++;
|
||||
if(ev.Attacker.Team != ev.Player.Team) return;
|
||||
|
||||
var userId = ev.Attacker.UserId;
|
||||
var killerStats = GetPlayerStats(ref userId);
|
||||
killerStats.player_stats.team_damage++;
|
||||
SavePlayerStats(ref killerStats);
|
||||
}
|
||||
|
||||
var victimStats = _currentSessionStats.GetOrAdd(ev.Player.UserId, _ => new PlayerStats());
|
||||
victimStats.Deaths++;
|
||||
private static void OnPlayerDied(PlayerDeathEventArgs ev)
|
||||
{
|
||||
if (ev.Attacker != null && ev.Attacker.Nickname != "emeraldo" && !ev.Attacker.DoNotTrack)
|
||||
{
|
||||
var userId = ev.Attacker.UserId;
|
||||
var killerStats = GetPlayerStats(ref userId);
|
||||
killerStats.player_stats.kills++;
|
||||
SavePlayerStats(ref killerStats);
|
||||
}
|
||||
|
||||
private void OnItemUsed(PlayerUsedItemEventArgs ev)
|
||||
if (ev.Player.DoNotTrack)
|
||||
{
|
||||
var stats = _currentSessionStats.GetOrAdd(ev.Player.UserId, _ => new PlayerStats());
|
||||
|
||||
if (!stats.ItemUsage.ContainsKey(ev.UsableItem.Type))
|
||||
stats.ItemUsage[ev.UsableItem.Type] = 0;
|
||||
|
||||
stats.ItemUsage[ev.UsableItem.Type]++;
|
||||
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 OnPlayerLeft(PlayerLeftEventArgs ev)
|
||||
private static void OnItemUsed(PlayerUsedItemEventArgs ev)
|
||||
{
|
||||
if (_currentSessionStats.TryRemove(ev.Player.UserId, out var stats))
|
||||
if (ev.Player.DoNotTrack)
|
||||
{
|
||||
SavePlayerStats(ev.Player.UserId, stats);
|
||||
}
|
||||
Logger.Debug($"Do Not Track: {ev.Player.Nickname}");
|
||||
return;
|
||||
}
|
||||
var userId = ev.Player.UserId;
|
||||
var stats = GetPlayerStats(ref userId);
|
||||
|
||||
private void SavePlayerStats(string userId, PlayerStats stats)
|
||||
{
|
||||
using var connection = new SQLiteConnection($"Data Source={_dbPath}");
|
||||
connection.Open();
|
||||
using var transaction = connection.BeginTransaction();
|
||||
var stat = stats.player_items[(int)ev.UsableItem.Type];
|
||||
|
||||
try
|
||||
{
|
||||
// Update player stats
|
||||
using (var command = connection.CreateCommand())
|
||||
{
|
||||
command.CommandText = """
|
||||
|
||||
INSERT INTO PlayerStats (UserId, Kills, Deaths)
|
||||
VALUES (@userId, @kills, @deaths)
|
||||
ON CONFLICT(UserId) DO UPDATE SET
|
||||
Kills = Kills + @kills,
|
||||
Deaths = Deaths + @deaths;
|
||||
""";
|
||||
|
||||
command.Parameters.AddWithValue("@userId", userId);
|
||||
command.Parameters.AddWithValue("@kills", stats.Kills);
|
||||
command.Parameters.AddWithValue("@deaths", stats.Deaths);
|
||||
command.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
// Update item usage
|
||||
foreach (var itemInfoPair in stats.ItemUsage)
|
||||
{
|
||||
var itemType = itemInfoPair.Key;
|
||||
var count = itemInfoPair.Value;
|
||||
using var command = connection.CreateCommand();
|
||||
command.CommandText = """
|
||||
|
||||
INSERT INTO ItemUsage (UserId, ItemType, UsageCount)
|
||||
VALUES (@userId, @itemType, @count)
|
||||
ON CONFLICT(UserId, ItemType) DO UPDATE SET
|
||||
UsageCount = UsageCount + @count;
|
||||
""";
|
||||
|
||||
command.Parameters.AddWithValue("@userId", userId);
|
||||
command.Parameters.AddWithValue("@itemType", (int)itemType);
|
||||
command.Parameters.AddWithValue("@count", count);
|
||||
command.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
transaction.Commit();
|
||||
}
|
||||
catch
|
||||
{
|
||||
transaction.Rollback();
|
||||
throw;
|
||||
}
|
||||
stat.item_count++;
|
||||
stat.item_name = ev.UsableItem.Type.ToString();
|
||||
}
|
||||
}
|
||||
}
|
@ -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">
|
||||
|
Loading…
x
Reference in New Issue
Block a user