diff --git a/Commands.cs b/Commands.cs index ca1e5c67..fcf7ebed 100644 --- a/Commands.cs +++ b/Commands.cs @@ -37,6 +37,9 @@ namespace WeaponPaints GivePlayerGloves(player); RefreshWeapons(player); + GivePlayerAgent(player); + GivePlayerMusicKit(player); + AddTimer(0.15f, () => GivePlayerPin(player)); } if (!string.IsNullOrEmpty(Localizer["wp_command_refresh_done"])) diff --git a/Config.cs b/Config.cs index 752c7c0f..be606645 100644 --- a/Config.cs +++ b/Config.cs @@ -20,6 +20,9 @@ namespace WeaponPaints [JsonPropertyName("SkinEnabled")] public bool SkinEnabled { get; set; } = true; + [JsonPropertyName("PinsEnabled")] + public bool PinsEnabled { get; set; } = true; + [JsonPropertyName("CommandWpEnabled")] public bool CommandWpEnabled { get; set; } = true; @@ -62,7 +65,7 @@ namespace WeaponPaints public class WeaponPaintsConfig : BasePluginConfig { - [JsonPropertyName("ConfigVersion")] public override int Version { get; set; } = 7; + [JsonPropertyName("ConfigVersion")] public override int Version { get; set; } = 8; [JsonPropertyName("SkinsLanguage")] public string SkinsLanguage { get; set; } = "en"; @@ -88,7 +91,7 @@ namespace WeaponPaints [JsonPropertyName("Website")] public string Website { get; set; } = "example.com/skins"; - [JsonPropertyName("Additionalss")] + [JsonPropertyName("Additional")] public Additional Additional { get; set; } = new(); } } \ No newline at end of file diff --git a/Events.cs b/Events.cs index 0da98ea6..1570eaa0 100644 --- a/Events.cs +++ b/Events.cs @@ -4,7 +4,6 @@ using CounterStrikeSharp.API.Core.Attributes.Registration; using CounterStrikeSharp.API.Modules.Entities; using CounterStrikeSharp.API.Modules.Memory; using CounterStrikeSharp.API.Modules.Memory.DynamicFunctions; -using System.Runtime.InteropServices; namespace WeaponPaints { @@ -68,6 +67,22 @@ namespace WeaponPaints if (player is null || !player.IsValid || player.IsBot) return HookResult.Continue; + var playerInfo = new PlayerInfo + { + UserId = player.UserId, + Slot = player.Slot, + Index = (int)player.Index, + SteamId = player.SteamID.ToString(), + Name = player.PlayerName, + IpAddress = player.IpAddress?.Split(":")[0] + }; + + if (!GPlayerWeaponsInfo.TryGetValue(player.Slot, out var weaponInfos)) + return HookResult.Continue; + + if (WeaponSync != null) + _ = Task.Run(async () => await WeaponSync.SyncStatTrakToDatabase(playerInfo, weaponInfos)); + if (Config.Additional.SkinEnabled) { GPlayerWeaponsInfo.TryRemove(player.Slot, out _); @@ -88,9 +103,12 @@ namespace WeaponPaints { GPlayersMusic.TryRemove(player.Slot, out _); } + if (Config.Additional.PinsEnabled) + { + GPlayersPin.TryRemove(player.Slot, out _); + } _temporaryPlayerWeaponWear.TryRemove(player.Slot, out _); - CommandsCooldown.Remove(player.Slot); return HookResult.Continue; @@ -231,6 +249,37 @@ namespace WeaponPaints return HookResult.Continue; } + private HookResult OnPlayerDeath(EventPlayerDeath @event, GameEventInfo info) + { + CCSPlayerController? player = @event.Attacker; + + if (player is null || !player.IsValid) + return HookResult.Continue; + + if (!GPlayerWeaponsInfo.TryGetValue(player.Slot, out _)) return HookResult.Continue; + + CBasePlayerWeapon? weapon = player.PlayerPawn.Value?.WeaponServices?.ActiveWeapon.Value; + + if (weapon == null) return HookResult.Continue; + + int weaponDefIndex = weapon.AttributeManager.Item.ItemDefinitionIndex; + + if (!GPlayerWeaponsInfo[player.Slot].TryGetValue(weaponDefIndex, out var weaponInfo) || weaponInfo.Paint == 0) + return HookResult.Continue; + + if (!weaponInfo.StatTrak) return HookResult.Continue; + + weaponInfo.StatTrakCount += 1; + + CAttributeListSetOrAddAttributeValueByName.Invoke(weapon.AttributeManager.Item.NetworkedDynamicAttributes.Handle, "kill eater", ViewAsFloat((uint)weaponInfo.StatTrakCount)); + CAttributeListSetOrAddAttributeValueByName.Invoke(weapon.AttributeManager.Item.NetworkedDynamicAttributes.Handle, "kill eater score type", 0); + + CAttributeListSetOrAddAttributeValueByName.Invoke(weapon.AttributeManager.Item.AttributeList.Handle, "kill eater", ViewAsFloat((uint)weaponInfo.StatTrakCount)); + CAttributeListSetOrAddAttributeValueByName.Invoke(weapon.AttributeManager.Item.AttributeList.Handle, "kill eater score type", 0); + + return HookResult.Continue; + } + private void RegisterListeners() { RegisterListener(OnMapStart); @@ -239,6 +288,7 @@ namespace WeaponPaints RegisterEventHandler(OnRoundStart); RegisterEventHandler(OnRoundEnd); RegisterListener(OnEntityCreated); + RegisterEventHandler(OnPlayerDeath); if (Config.Additional.ShowSkinImage) RegisterListener(OnTick); diff --git a/Utility.cs b/Utility.cs index c66b4a6b..0a59459b 100644 --- a/Utility.cs +++ b/Utility.cs @@ -33,6 +33,8 @@ namespace WeaponPaints `weapon_wear` float NOT NULL DEFAULT 0.000001, `weapon_seed` int(16) NOT NULL DEFAULT 0, `weapon_nametag` VARCHAR(128) DEFAULT NULL, + `weapon_stattrak` tinyint(1) NOT NULL, + `weapon_stattrak_count` int(10) NOT NULL, `weapon_sticker_0` VARCHAR(128) NOT NULL DEFAULT '0;0;0;0;0;0;0' COMMENT 'id;schema;x;y;wear;scale;rotation', `weapon_sticker_1` VARCHAR(128) NOT NULL DEFAULT '0;0;0;0;0;0;0' COMMENT 'id;schema;x;y;wear;scale;rotation', `weapon_sticker_2` VARCHAR(128) NOT NULL DEFAULT '0;0;0;0;0;0;0' COMMENT 'id;schema;x;y;wear;scale;rotation', @@ -68,6 +70,13 @@ namespace WeaponPaints UNIQUE (`steamid`) ) ENGINE=InnoDB """, + """ + CREATE TABLE IF NOT EXISTS `wp_player_pins` ( + `steamid` varchar(64) NOT NULL, + `id` int(11) NOT NULL, + UNIQUE (`steamid`) + ) ENGINE=InnoDB + """, ]; foreach (var query in createTableQueries) diff --git a/VERSION b/VERSION index 4b887540..4818b1b8 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.6a \ No newline at end of file +2.7a \ No newline at end of file diff --git a/Variables.cs b/Variables.cs index 27422bf5..7df322a1 100644 --- a/Variables.cs +++ b/Variables.cs @@ -75,6 +75,7 @@ public partial class WeaponPaints internal static readonly ConcurrentDictionary GPlayersKnife = new(); internal static readonly ConcurrentDictionary GPlayersGlove = new(); internal static readonly ConcurrentDictionary GPlayersMusic = new(); + internal static readonly ConcurrentDictionary GPlayersPin = new(); public static readonly ConcurrentDictionary GPlayersAgent = new(); internal static readonly ConcurrentDictionary> GPlayerWeaponsInfo = new(); internal static List SkinsList = []; diff --git a/WeaponAction.cs b/WeaponAction.cs index 369fd69d..6724cd82 100644 --- a/WeaponAction.cs +++ b/WeaponAction.cs @@ -6,7 +6,6 @@ using CounterStrikeSharp.API.Modules.Timers; using CounterStrikeSharp.API.Modules.Utils; using Microsoft.Extensions.Logging; using System.Collections.Concurrent; -using System.Linq.Expressions; using System.Runtime.InteropServices; namespace WeaponPaints @@ -87,6 +86,15 @@ namespace WeaponPaints weapon.FallbackWear = weaponInfo.Wear; CAttributeListSetOrAddAttributeValueByName.Invoke(weapon.AttributeManager.Item.NetworkedDynamicAttributes.Handle, "set item texture prefab", weapon.FallbackPaintKit); + if (weaponInfo.StatTrak) + { + CAttributeListSetOrAddAttributeValueByName.Invoke(weapon.AttributeManager.Item.NetworkedDynamicAttributes.Handle, "kill eater", ViewAsFloat((uint)weaponInfo.StatTrakCount)); + CAttributeListSetOrAddAttributeValueByName.Invoke(weapon.AttributeManager.Item.NetworkedDynamicAttributes.Handle, "kill eater score type", 0); + + CAttributeListSetOrAddAttributeValueByName.Invoke(weapon.AttributeManager.Item.AttributeList.Handle, "kill eater", ViewAsFloat((uint)weaponInfo.StatTrakCount)); + CAttributeListSetOrAddAttributeValueByName.Invoke(weapon.AttributeManager.Item.AttributeList.Handle, "kill eater score type", 0); + } + fallbackPaintKit = weapon.FallbackPaintKit; if (fallbackPaintKit == 0) @@ -451,6 +459,19 @@ namespace WeaponPaints player.MusicKitID = value; Utilities.SetStateChanged(player, "CCSPlayerController", "m_iMusicKitID"); } + + private static void GivePlayerPin(CCSPlayerController player) + { + if (!GPlayersPin.TryGetValue(player.Slot, out var pin)) return; + + if (player.InventoryServices == null) return; + + for (var index = 0; index < player.InventoryServices.Rank.Length; index++) + { + player.InventoryServices.Rank[index] = index == 5 ? (MedalRank_t)pin : MedalRank_t.MEDAL_RANK_NONE; + Utilities.SetStateChanged(player, "CCSPlayerController", "m_pInventoryServices"); + } + } private void GiveOnItemPickup(CCSPlayerController player) { @@ -496,21 +517,7 @@ namespace WeaponPaints return viewModel.Value == null ? null : viewModel.Value; } - public static unsafe T[] GetFixedArray(nint pointer, string @class, string member, int length) where T : CHandle - { - var ptr = pointer + Schema.GetSchemaOffset(@class, member); - var references = MemoryMarshal.CreateSpan(ref ptr, length); - var values = new T[length]; - - for (var i = 0; i < length; i++) - { - values[i] = (T)Activator.CreateInstance(typeof(T), references[i])!; - } - - return values; - } - - private float ViewAsFloat(uint value) + private static float ViewAsFloat(uint value) { return BitConverter.Int32BitsToSingle((int)value); } diff --git a/WeaponInfo.cs b/WeaponInfo.cs index ffdabfd7..1bd66f6a 100644 --- a/WeaponInfo.cs +++ b/WeaponInfo.cs @@ -3,11 +3,13 @@ public class WeaponInfo { public int Paint { get; set; } - public int Seed { get; set; } = 0; - public float Wear { get; set; } = 0f; + public int Seed { get; set; } + public float Wear { get; set; } public string Nametag { get; set; } = ""; + public bool StatTrak { get; set; } = false; + public int StatTrakCount { get; set; } public KeyChainInfo? KeyChain { get; set; } - public List Stickers { get; set; } = new List(); + public List Stickers { get; set; } = new(); } public class StickerInfo diff --git a/WeaponPaints.cs b/WeaponPaints.cs index 544bdf3a..9bb28247 100644 --- a/WeaponPaints.cs +++ b/WeaponPaints.cs @@ -16,7 +16,7 @@ public partial class WeaponPaints : BasePlugin, IPluginConfig "Nereziel & daffyy"; public override string ModuleDescription => "Skin, gloves, agents and knife selector, standalone and web-based"; public override string ModuleName => "WeaponPaints"; - public override string ModuleVersion => "2.6a"; + public override string ModuleVersion => "2.7a"; public override void Load(bool hotReload) { @@ -36,6 +36,7 @@ public partial class WeaponPaints : BasePlugin, IPluginConfig OnAllPluginsLoaded(hotReload)); } Utility.LoadSkinsFromFile(ModuleDirectory + $"/data/skins_{_config.SkinsLanguage}.json", Logger); diff --git a/WeaponSynchronization.cs b/WeaponSynchronization.cs index f4b48cfa..801bdc8e 100644 --- a/WeaponSynchronization.cs +++ b/WeaponSynchronization.cs @@ -32,6 +32,8 @@ namespace WeaponPaints GetMusicFromDatabase(player, connection); if (_config.Additional.SkinEnabled) GetWeaponPaintsFromDatabase(player, connection); + if (_config.Additional.PinsEnabled) + GetPinsFromDatabase(player, connection); } catch (Exception ex) { @@ -235,6 +237,27 @@ namespace WeaponPaints } } + private void GetPinsFromDatabase(PlayerInfo? player, MySqlConnection connection) + { + try + { + if (string.IsNullOrEmpty(player?.SteamId)) + return; + + const string query = "SELECT `id` FROM `wp_player_pins` WHERE `steamid` = @steamid"; + var pinData = connection.QueryFirstOrDefault(query, new { steamid = player.SteamId }); + + if (pinData != null) + { + WeaponPaints.GPlayersPin[player.Slot] = pinData.Value; + } + } + catch (Exception ex) + { + Utility.Log($"An error occurred in GetPinsFromDatabase: {ex.Message}"); + } + } + internal async Task SyncKnifeToDatabase(PlayerInfo player, string knife) { if (!_config.Additional.KnifeEnabled || string.IsNullOrEmpty(player.SteamId) || string.IsNullOrEmpty(knife)) return; @@ -349,5 +372,48 @@ namespace WeaponPaints Utility.Log($"Error syncing music kit to database: {e.Message}"); } } + + internal async Task SyncStatTrakToDatabase(PlayerInfo player, ConcurrentDictionary weaponInfos) + { + if (WeaponPaints.WeaponSync == null || weaponInfos.IsEmpty) return; + + var statTrakWeapons = weaponInfos + .Where(w => w.Value is { StatTrak: true, StatTrakCount: > 0 }) + .ToDictionary(w => w.Key, w => w.Value.StatTrakCount); + + if (statTrakWeapons.Count == 0) return; + + if (string.IsNullOrEmpty(player.SteamId)) + return; + + try + { + await using var connection = await _database.GetConnectionAsync(); + await using var transaction = await connection.BeginTransactionAsync(); + + foreach (var (defindex, statTrakCount) in statTrakWeapons) + { + const string query = @" + INSERT INTO `wp_player_skins` (`steamid`, `weapon_defindex`, `weapon_stattrak_count`) + VALUES (@steamid, @weaponDefIndex, @StatTrakCount) + ON DUPLICATE KEY UPDATE `weapon_stattrak_count` = @StatTrakCount"; + + var parameters = new + { + steamid = player.SteamId, + weaponDefIndex = defindex, + StatTrakCount = statTrakCount + }; + + await connection.ExecuteAsync(query, parameters, transaction); + } + + await transaction.CommitAsync(); + } + catch (Exception e) + { + Utility.Log($"Error syncing stattrak to database: {e.Message}"); + } + } } } \ No newline at end of file