diff --git a/jRandomSkills - SRC Files/src/jRandomSkills.cs b/jRandomSkills - SRC Files/src/jRandomSkills.cs index 26bdc3d..976d1ae 100644 --- a/jRandomSkills - SRC Files/src/jRandomSkills.cs +++ b/jRandomSkills - SRC Files/src/jRandomSkills.cs @@ -31,7 +31,7 @@ namespace src public override string ModuleName => "[CS2] [ jRandomSkills ]"; public override string ModuleAuthor => "D3X (Original), Juzlus (Modifier), ByDexterTR (Contributor)"; public override string ModuleDescription => "Plugin adds random skills every round for CS2 by D3X. Modified by Juzlus."; - public override string ModuleVersion => "1.2.3.b9"; + public override string ModuleVersion => "1.2.4.b1"; public override void Load(bool hotReload) { diff --git a/jRandomSkills - SRC Files/src/lang/tr.json b/jRandomSkills - SRC Files/src/lang/tr.json index 617a3dd..05d408f 100644 --- a/jRandomSkills - SRC Files/src/lang/tr.json +++ b/jRandomSkills - SRC Files/src/lang/tr.json @@ -82,7 +82,7 @@ "catapult": "Mancınık", "catapult_desc": "Vurduğun rakibi uzaya (yukarı doğru) fırlatma şansın var", - "catapult_desc2": "Fırlatma şansın: %{0}", + "catapult_desc2": "Vurduğun rakibi havaya fırlatma şansın: %{0}", "chameleon": "Bukalemun", "chameleon_desc": "Öldürdüğün ilk oyuncunun yeteneği sana geçer", @@ -452,7 +452,7 @@ "push": "İtici", "push_desc": "Düşmana vurduğunda onu geri itme şansın olur", - "push_desc2": "Geri itme şansın: %{0}", + "push_desc2": "Vurduğun rakibi geri itme şansın: %{0}", "pyro": "Ateşbaz", "pyro_desc": "Molotof canını yeniler", @@ -511,7 +511,7 @@ "shade": "Gölge", "shade_desc": "Vurduğun düşmanın arkasına ışınlanma şansın olur", - "shade_desc2": "Arkasına ışınlanma şansın: %{0}", + "shade_desc2": "Vurduğun rakibin arkasına ışınlanma şansın: %{0}", "shade_nospace": "Uygun alan yok", "shortbomb": "Kısa Fünye", diff --git a/jRandomSkills - SRC Files/src/player/PlayerEvents.cs b/jRandomSkills - SRC Files/src/player/PlayerEvents.cs index 8f5507d..d6de4b1 100644 --- a/jRandomSkills - SRC Files/src/player/PlayerEvents.cs +++ b/jRandomSkills - SRC Files/src/player/PlayerEvents.cs @@ -294,10 +294,14 @@ namespace src.player } } + private static bool IsEventAlive(GameEvent? @event) => @event != null && @event.Handle != nint.Zero; + private static HookResult WeaponEquip(EventItemEquip @event, GameEventInfo info) { lock (setLock) { + if (!IsEventAlive(@event)) return HookResult.Continue; + DispatchToActiveSkills("WeaponEquip", @event); return HookResult.Continue; } @@ -307,6 +311,8 @@ namespace src.player { lock (setLock) { + if (!IsEventAlive(@event)) return HookResult.Continue; + DispatchToActiveSkills("WeaponPickup", @event); return HookResult.Continue; } @@ -584,7 +590,7 @@ namespace src.player string welcomeMsg = player.GetTranslationWithoutIlliterate("welcome_message", "welcome"); foreach (string line in welcomeMsg.Split("\n")) player.PrintToChat($" {ChatColors.Green}" + line.Replace("{PLAYER}", $" {ChatColors.Red}\u202A{player.PlayerName}\u202C{ChatColors.Green}", StringComparison.OrdinalIgnoreCase) - .Replace("{SERVER_NAME}", $" {ChatColors.Red}{ConVar.Find("hostname")?.StringValue ?? "Default Server"}{ChatColors.Green}", StringComparison.OrdinalIgnoreCase) + .Replace("{SERVER_NAME}", $" {ChatColors.Red}{SkillUtils.CvarString("hostname", "Default Server")}{ChatColors.Green}", StringComparison.OrdinalIgnoreCase) .Replace("{VERSION}", $" {ChatColors.Red}v{Instance.ModuleVersion}{ChatColors.Green}", StringComparison.OrdinalIgnoreCase) .Replace("{SKILLS_COUNT}", $" {ChatColors.Red}{SkillData.Skills.Count - 1}{ChatColors.Green}", StringComparison.OrdinalIgnoreCase) .Replace("{AUTHOR1}", $" {ChatColors.Red}Jakub Bartosik (D3X){ChatColors.Green} ({ChatColors.Red}https://github.com/jakubbartosik/dRandomSkills{ChatColors.Green})", StringComparison.OrdinalIgnoreCase) diff --git a/jRandomSkills - SRC Files/src/player/RoundEvents.cs b/jRandomSkills - SRC Files/src/player/RoundEvents.cs index 9908d4f..6e070be 100644 --- a/jRandomSkills - SRC Files/src/player/RoundEvents.cs +++ b/jRandomSkills - SRC Files/src/player/RoundEvents.cs @@ -2,7 +2,6 @@ using CounterStrikeSharp.API.Core; using CounterStrikeSharp.API.Core.Attributes; using CounterStrikeSharp.API.Modules.Admin; -using CounterStrikeSharp.API.Modules.Cvars; using CounterStrikeSharp.API.Modules.Events; using CounterStrikeSharp.API.Modules.Memory; using CounterStrikeSharp.API.Modules.Memory.DynamicFunctions; @@ -20,7 +19,17 @@ namespace src.player { public static partial class Event { - private static jSkill_SkillInfo ChooseSkillByRarityAndMax(List candidates, Dictionary assignmentCounts, Config.GameModes gameMode) + private static bool IsVip(CCSPlayerController? player) + { + if (player == null || !player.IsValid || player.IsBot) return false; + + string flag = Config.LoadedConfig.VIPFlag; + if (string.IsNullOrWhiteSpace(flag)) return false; + + return AdminManager.PlayerHasPermissions(player, flag); + } + + private static jSkill_SkillInfo ChooseSkillByRarityAndMax(List candidates, Dictionary assignmentCounts, Config.GameModes gameMode, bool vip) { if (candidates == null || candidates.Count == 0) return noneSkill; @@ -31,7 +40,7 @@ namespace src.player for (int attempt = 0; attempt < attempts; attempt++) { - var (roll, rolled) = RarityManager.RollRarity(); + var (roll, rolled) = RarityManager.RollRarity(vip); string rolledName = rolled.ToString(); filtered.Clear(); @@ -92,7 +101,7 @@ namespace src.player } Instance.RemoveListener(CheckTransmit); - int freezetime = ConVar.Find("mp_freezetime")?.GetPrimitiveValue() ?? 0; + int freezetime = SkillUtils.CvarValue("mp_freezetime", 0); freezeTimeEnd = DateTime.Now.AddSeconds(freezetime + (Instance?.GameRules?.TeamIntroPeriod == true ? 7 : 0)); setSkillTimer?.Kill(); @@ -212,7 +221,7 @@ namespace src.player PlayerManager.Clear(); - ConVar.Find("sv_legacy_jump")?.SetValue("1"); + SkillUtils.Cvar("sv_legacy_jump")?.SetValue("1"); } } @@ -348,7 +357,7 @@ namespace src.player if (skillList.Count == 0) skills.Clear(); } - var randomSkill = skillList.Count == 0 ? noneSkill : ChooseSkillByRarityAndMax(skillList, assignmentCounts, gameMode); + var randomSkill = skillList.Count == 0 ? noneSkill : ChooseSkillByRarityAndMax(skillList, assignmentCounts, gameMode, IsVip(player)); if (gameMode == Config.GameModes.NoRepeat) { @@ -686,7 +695,7 @@ namespace src.player else assignmentCounts[sp.Skill] = 1; } - randomSkill = skillList.Count == 0 ? noneSkill : ChooseSkillByRarityAndMax(skillList, assignmentCounts, gameMode); + randomSkill = skillList.Count == 0 ? noneSkill : ChooseSkillByRarityAndMax(skillList, assignmentCounts, gameMode, IsVip(player)); } else if (gameMode == Config.GameModes.TeamSkills) randomSkill = player.Team == CsTeam.Terrorist ? tSkill : ctSkill; diff --git a/jRandomSkills - SRC Files/src/player/skills/AntyFlash.cs b/jRandomSkills - SRC Files/src/player/skills/AntyFlash.cs index 0e30862..a637f5a 100644 --- a/jRandomSkills - SRC Files/src/player/skills/AntyFlash.cs +++ b/jRandomSkills - SRC Files/src/player/skills/AntyFlash.cs @@ -1,5 +1,4 @@ using CounterStrikeSharp.API.Core; -using CounterStrikeSharp.API.Modules.Cvars; using CounterStrikeSharp.API.Modules.Entities.Constants; using CounterStrikeSharp.API.Modules.Utils; using src.utils; @@ -86,7 +85,7 @@ namespace src.player.skills { if (player == null || !player.IsValid) return; - int flashbangLimit = ConVar.Find("ammo_grenade_limit_flashbang")?.GetPrimitiveValue() ?? 2; + int flashbangLimit = SkillUtils.CvarValue("ammo_grenade_limit_flashbang", 2); int grenadeLimit = SkillsInfo.GetValue(skillName, "grenadeLimit"); if (grenadeLimit > flashbangLimit) diff --git a/jRandomSkills - SRC Files/src/player/skills/BlastShot.cs b/jRandomSkills - SRC Files/src/player/skills/BlastShot.cs index 3671de9..9fcb878 100644 --- a/jRandomSkills - SRC Files/src/player/skills/BlastShot.cs +++ b/jRandomSkills - SRC Files/src/player/skills/BlastShot.cs @@ -179,7 +179,7 @@ namespace src.player.skills damageInfo.Damage *= SkillUtils.GetTeamDamageMultiplier(skillName); } - if (owner != null && damageInfo.Damage >= victimPawn.Health) + if (owner != null && SkillUtils.IsPredictedLethal(damageInfo, victimPawn)) SkillUtils.RegisterKillCredit(victim.Index, owner.Index, KillfeedIcons.Explosion); } diff --git a/jRandomSkills - SRC Files/src/player/skills/Bounty.cs b/jRandomSkills - SRC Files/src/player/skills/Bounty.cs index 48b55d3..aa10a2f 100644 --- a/jRandomSkills - SRC Files/src/player/skills/Bounty.cs +++ b/jRandomSkills - SRC Files/src/player/skills/Bounty.cs @@ -1,6 +1,5 @@ using CounterStrikeSharp.API; using CounterStrikeSharp.API.Core; -using CounterStrikeSharp.API.Modules.Cvars; using CounterStrikeSharp.API.Modules.Utils; using src.utils; using System.Collections.Concurrent; @@ -152,7 +151,7 @@ namespace src.player.skills killerEvent.PrintToChat($" {ChatColors.Green}{killerEvent.GetTranslation("bounty_claimed_info", victim.PlayerName, reward)}"); } - private static int GetMaxMoney() => ConVar.Find("mp_maxmoney")?.GetPrimitiveValue() ?? 16000; + private static int GetMaxMoney() => SkillUtils.CvarValue("mp_maxmoney", 16000); private static bool GiveMoney(CCSPlayerController player, int amount) { diff --git a/jRandomSkills - SRC Files/src/player/skills/Catapult.cs b/jRandomSkills - SRC Files/src/player/skills/Catapult.cs index 585be94..1d49c1f 100644 --- a/jRandomSkills - SRC Files/src/player/skills/Catapult.cs +++ b/jRandomSkills - SRC Files/src/player/skills/Catapult.cs @@ -21,6 +21,7 @@ namespace src.player.skills var victim = PlayerManager.GetPlayerEvent(@event.Userid); if (!Instance.IsPlayerValid(attacker) || !Instance.IsPlayerValid(victim) || attacker == victim) return; + if (!SkillUtils.FiresBullets(@event.Weapon)) return; var attackerInfo = PlayerManager.GetPlayerByIndex(attacker!.Index); if (Heavyweight.Resists(victim)) return; @@ -29,8 +30,9 @@ namespace src.player.skills if (Instance.Random.NextDouble() <= attackerInfo.SkillChance) { var victimPawn = victim.PlayerPawn?.Value; - if (victimPawn != null) - victimPawn.AbsVelocity.Z = 300f; + if (victimPawn == null || !victimPawn.IsValid || victimPawn.LifeState != (byte)LifeState_t.LIFE_ALIVE) return; + + victimPawn.AbsVelocity.Z = 300f; } } diff --git a/jRandomSkills - SRC Files/src/player/skills/ChillOut.cs b/jRandomSkills - SRC Files/src/player/skills/ChillOut.cs index 425e7b1..36a1383 100644 --- a/jRandomSkills - SRC Files/src/player/skills/ChillOut.cs +++ b/jRandomSkills - SRC Files/src/player/skills/ChillOut.cs @@ -84,7 +84,7 @@ namespace src.player.skills float currentTime = Server.CurrentTime; float extraTime = SkillsInfo.GetValue(skillName, "bombArmedTime"); - foreach (var player in PlayerManager.GetTickPlayers().Where(p => p.Team == CsTeam.Terrorist)) + foreach (var player in PlayerManager.GetTickPlayers().Where(p => p != null && p.IsValid && p.Team == CsTeam.Terrorist)) { if (player == null || !player.IsValid) continue; diff --git a/jRandomSkills - SRC Files/src/player/skills/DeathBomb.cs b/jRandomSkills - SRC Files/src/player/skills/DeathBomb.cs index 4facc37..14b06c7 100644 --- a/jRandomSkills - SRC Files/src/player/skills/DeathBomb.cs +++ b/jRandomSkills - SRC Files/src/player/skills/DeathBomb.cs @@ -128,7 +128,7 @@ namespace src.player.skills damageInfo.Damage *= SkillUtils.GetTeamDamageMultiplier(skillName); } - if (owner != null && damageInfo.Damage >= victimPawn.Health) + if (owner != null && SkillUtils.IsPredictedLethal(damageInfo, victimPawn)) SkillUtils.RegisterKillCredit(victim.Index, owner.Index, KillfeedIcons.Explosion); } diff --git a/jRandomSkills - SRC Files/src/player/skills/ExplodingBarrel.cs b/jRandomSkills - SRC Files/src/player/skills/ExplodingBarrel.cs index 04ea71d..c25881e 100644 --- a/jRandomSkills - SRC Files/src/player/skills/ExplodingBarrel.cs +++ b/jRandomSkills - SRC Files/src/player/skills/ExplodingBarrel.cs @@ -240,7 +240,7 @@ namespace src.player.skills damageInfo.Damage *= SkillUtils.GetTeamDamageMultiplier(skillName); } - if (owner != null && damageInfo.Damage >= victimPawn.Health) + if (owner != null && SkillUtils.IsPredictedLethal(damageInfo, victimPawn)) SkillUtils.RegisterKillCredit(victim.Index, owner.Index, KillfeedIcons.Explosion); } diff --git a/jRandomSkills - SRC Files/src/player/skills/ExplosiveShot.cs b/jRandomSkills - SRC Files/src/player/skills/ExplosiveShot.cs index 70ff794..7bdedc6 100644 --- a/jRandomSkills - SRC Files/src/player/skills/ExplosiveShot.cs +++ b/jRandomSkills - SRC Files/src/player/skills/ExplosiveShot.cs @@ -15,13 +15,13 @@ namespace src.player.skills private const Skills skillName = Skills.ExplosiveShot; private static readonly QAngle angle = new(5, 10, -4); - private static int lastTick = 0; - private static byte pendingTeam = (byte)CsTeam.None; - private static readonly ConcurrentDictionary nades = []; + private static readonly ConcurrentDictionary lastTickByPlayer = []; + private static readonly ConcurrentDictionary> nades = []; public static void NewRound() { nades.Clear(); + lastTickByPlayer.Clear(); } public static void LoadSkill() @@ -43,9 +43,8 @@ namespace src.player.skills private static void SpawnExplosion(Vector vector, CCSPlayerController player) { - lastTick = Server.TickCount; - pendingTeam = player.TeamNum; - nades.AddOrUpdate(Server.TickCount, (player.TeamNum, player.Index), (_, _) => (player.TeamNum, player.Index)); + lastTickByPlayer[player.Index] = Server.TickCount; + nades.GetOrAdd(Server.TickCount, static _ => new ConcurrentQueue<(byte, uint)>()).Enqueue((player.TeamNum, player.Index)); SkillUtils.CreateHEGrenadeProjectile(vector, angle, new Vector(0, 0, 0), player.TeamNum); } @@ -64,14 +63,16 @@ namespace src.player.skills if (!(NearlyEquals(angle.X, heProjectile.AbsRotation.X) && NearlyEquals(angle.Y, heProjectile.AbsRotation.Y) && NearlyEquals(angle.Z, heProjectile.AbsRotation.Z))) return; + if (!nades.TryGetValue(spawnTick, out var queue) || !queue.TryDequeue(out var source)) return; + if (queue.IsEmpty) nades.TryRemove(spawnTick, out _); + heProjectile.TicksAtZeroVelocity = 100; - heProjectile.TeamNum = pendingTeam; + heProjectile.TeamNum = source.Team; heProjectile.Damage = SkillsInfo.GetValue(skillName, "damage"); heProjectile.DmgRadius = SkillsInfo.GetValue(skillName, "damageRadius"); heProjectile.DetonateTime = 0; - if (nades.TryRemove(spawnTick, out var source)) - heProjectile.Globalname = $"explosiveshot_team_{source.Team}_{source.Owner}_{heProjectile.Index}"; + heProjectile.Globalname = $"explosiveshot_team_{source.Team}_{source.Owner}_{heProjectile.Index}"; }); } @@ -112,7 +113,7 @@ namespace src.player.skills var owner = Utilities.GetPlayerFromIndex((int)ownerIndex); if (owner != null && !owner.IsValid) owner = null; - if (owner != null && damageInfo.Damage >= victimPawn.Health) + if (owner != null && SkillUtils.IsPredictedLethal(damageInfo, victimPawn)) SkillUtils.RegisterKillCredit(victim.Index, owner.Index, KillfeedIcons.Explosion); } @@ -120,11 +121,12 @@ namespace src.player.skills public static void BulletImpact(EventBulletImpact @event) { - if (lastTick == Server.TickCount) return; - var player = PlayerManager.GetPlayerEvent(@event.Userid); if (player == null || !player.IsValid) return; + // One explosion per player per tick: a shotgun reports every pellet as its own impact. + if (lastTickByPlayer.TryGetValue(player.Index, out int last) && last == Server.TickCount) return; + var pos = new Vector(@event.X, @event.Y, @event.Z); var playerInfo = PlayerManager.GetPlayerByIndex(player!.Index); diff --git a/jRandomSkills - SRC Files/src/player/skills/FireRain.cs b/jRandomSkills - SRC Files/src/player/skills/FireRain.cs index 0f38d47..9b9271e 100644 --- a/jRandomSkills - SRC Files/src/player/skills/FireRain.cs +++ b/jRandomSkills - SRC Files/src/player/skills/FireRain.cs @@ -11,9 +11,7 @@ namespace src.player.skills { private const Skills skillName = Skills.FireRain; private static readonly ConcurrentDictionary decoys = []; - private static int rainTick = -1; - private static CCSPlayerPawn? rainThrower; - private static byte rainTeam = (byte)CsTeam.None; + private static readonly ConcurrentDictionary> rainBatches = []; private static readonly ConcurrentDictionary rainMolotovs = []; public static void LoadSkill() @@ -25,6 +23,7 @@ namespace src.player.skills public static void NewRound() { KillAllDecoys(); + rainBatches.Clear(); rainMolotovs.Clear(); DecoyRing.ClearAll(skillName); } @@ -54,10 +53,6 @@ namespace src.player.skills const float spawnHeight = 1500.0f; const float approachDistance = 600.0f; - rainTick = Server.TickCount; - rainThrower = pawn; - rainTeam = player.TeamNum; - float startAngle = Random.Shared.NextSingle() * MathF.Tau; Vector? skyCenter = null; @@ -88,10 +83,12 @@ namespace src.player.skills if (!foundPosition || skyCenter == null) { + QueueRain(pawn, player.TeamNum, grenadeGroundCount); CreateMolotovSplash(targetPos, grenadeGroundCount, player.TeamNum); return; } + QueueRain(pawn, player.TeamNum, grenadeCount); CreateMolotovRaid(targetPos, skyCenter, grenadeCount, player.TeamNum); } @@ -202,24 +199,45 @@ namespace src.player.skills } } + private sealed class RainBatch + { + public required CCSPlayerPawn Thrower; + public required byte Team; + public int Remaining; + } + + private static void QueueRain(CCSPlayerPawn thrower, byte team, int count) + { + rainBatches.GetOrAdd(Server.TickCount, static _ => new ConcurrentQueue()) + .Enqueue(new RainBatch { Thrower = thrower, Team = team, Remaining = count }); + } + + private static void ConsumeOne(ConcurrentQueue queue, RainBatch batch) + { + if (--batch.Remaining > 0) return; + queue.TryDequeue(out _); + } + public static void OnEntitySpawned(CEntityInstance entity) { var name = entity.DesignerName; if (name == "molotov_projectile") { - if (Server.TickCount != rainTick) return; + if (!rainBatches.TryGetValue(Server.TickCount, out var queue) || !queue.TryPeek(out var batch)) return; - var thrower = rainThrower; - if (thrower == null || !thrower.IsValid) return; + var thrower = batch.Thrower; + if (thrower == null || !thrower.IsValid) { ConsumeOne(queue, batch); return; } var molotov = entity.As(); - if (molotov == null || !molotov.IsValid) return; + if (molotov == null || !molotov.IsValid) { ConsumeOne(queue, batch); return; } - molotov.TeamNum = rainTeam; + ConsumeOne(queue, batch); + + molotov.TeamNum = batch.Team; molotov.Thrower.Raw = thrower.EntityHandle.Raw; molotov.OwnerEntity.Raw = thrower.EntityHandle.Raw; - rainMolotovs[molotov.Index] = (thrower.EntityHandle.Raw, rainTeam); + rainMolotovs[molotov.Index] = (thrower.EntityHandle.Raw, batch.Team); Server.NextWorldUpdate(() => { diff --git a/jRandomSkills - SRC Files/src/player/skills/FriendlyFire.cs b/jRandomSkills - SRC Files/src/player/skills/FriendlyFire.cs index 5f47e04..dc1ea2c 100644 --- a/jRandomSkills - SRC Files/src/player/skills/FriendlyFire.cs +++ b/jRandomSkills - SRC Files/src/player/skills/FriendlyFire.cs @@ -1,6 +1,5 @@ using CounterStrikeSharp.API; using CounterStrikeSharp.API.Core; -using CounterStrikeSharp.API.Modules.Cvars; using CounterStrikeSharp.API.Modules.Memory.DynamicFunctions; using CounterStrikeSharp.API.Modules.Utils; using src.utils; @@ -32,7 +31,7 @@ namespace src.player.skills if (!SkillsInfo.GetValue(skillName, "manageAutoKick")) return false; bool live; - try { live = ConVar.Find("mp_autokick")?.GetPrimitiveValue() ?? false; } + try { live = SkillUtils.CvarValue("mp_autokick", false); } catch { return false; } if (!live) return false; diff --git a/jRandomSkills - SRC Files/src/player/skills/GravityDecoy.cs b/jRandomSkills - SRC Files/src/player/skills/GravityDecoy.cs index 6a583b8..24ccc18 100644 --- a/jRandomSkills - SRC Files/src/player/skills/GravityDecoy.cs +++ b/jRandomSkills - SRC Files/src/player/skills/GravityDecoy.cs @@ -150,6 +150,27 @@ namespace src.player.skills } } + public static void WeaponEquip(EventItemEquip @event) + { + var player = PlayerManager.GetPlayerEvent(@event.Userid); + if (player == null || !player.IsValid) return; + + if (playersWithSkill.TryGetValue(player.Index, out int grenadesLeft) && grenadesLeft > 1) + SkillUtils.UpdateGrenadeCount(player, CsItem.DecoyGrenade, grenadesLeft); + } + + public static void WeaponPickup(EventItemPickup @event) + { + var player = PlayerManager.GetPlayerEvent(@event.Userid); + if (player == null || !player.IsValid) return; + + var weapon = @event.Item; + if (string.IsNullOrEmpty(weapon) || weapon != "decoy") return; + + if (playersWithSkill.TryGetValue(player.Index, out int grenadesLeft) && grenadesLeft > 1) + SkillUtils.UpdateGrenadeCount(player, CsItem.DecoyGrenade, grenadesLeft); + } + public static void EnableSkill(CCSPlayerController player) { if (player == null || !player.IsValid) return; diff --git a/jRandomSkills - SRC Files/src/player/skills/KillerFlash.cs b/jRandomSkills - SRC Files/src/player/skills/KillerFlash.cs index 926b3b2..da45424 100644 --- a/jRandomSkills - SRC Files/src/player/skills/KillerFlash.cs +++ b/jRandomSkills - SRC Files/src/player/skills/KillerFlash.cs @@ -1,5 +1,4 @@ using CounterStrikeSharp.API.Core; -using CounterStrikeSharp.API.Modules.Cvars; using CounterStrikeSharp.API.Modules.Entities.Constants; using CounterStrikeSharp.API.Modules.Utils; using jRandomSkills.src.utils; @@ -79,7 +78,7 @@ namespace src.player.skills { if (player == null || !player.IsValid) return; - int flashbangLimit = ConVar.Find("ammo_grenade_limit_flashbang")?.GetPrimitiveValue() ?? 2; + int flashbangLimit = SkillUtils.CvarValue("ammo_grenade_limit_flashbang", 2); int grenadeLimit = SkillsInfo.GetValue(skillName, "grenadeLimit"); if (grenadeLimit > flashbangLimit) diff --git a/jRandomSkills - SRC Files/src/player/skills/Magnifier.cs b/jRandomSkills - SRC Files/src/player/skills/Magnifier.cs index 3f1499b..8d50b72 100644 --- a/jRandomSkills - SRC Files/src/player/skills/Magnifier.cs +++ b/jRandomSkills - SRC Files/src/player/skills/Magnifier.cs @@ -54,7 +54,7 @@ namespace src.player.skills if (playerInfo == null || playerInfo.Skill != skillName) continue; if (!SkillUtils.HasMenu(player)) continue; - var enemies = PlayerManager.GetTickPlayers().Where(p => p.PawnIsAlive && p.Team != player.Team && p.IsValid && p.Team != CsTeam.Spectator && p.Team != CsTeam.None).ToArray(); + var enemies = PlayerManager.GetTickPlayers().Where(p => p != null && p.IsValid && p.PawnIsAlive && p.Team != player.Team && p.Team != CsTeam.Spectator && p.Team != CsTeam.None).ToArray(); ConcurrentBag<(string, string)> menuItems = new(enemies.Select(e => (e.PlayerName, e.Index.ToString()))); SkillUtils.UpdateMenu(player, menuItems); diff --git a/jRandomSkills - SRC Files/src/player/skills/NoRecoil.cs b/jRandomSkills - SRC Files/src/player/skills/NoRecoil.cs index 90ddde7..14e1907 100644 --- a/jRandomSkills - SRC Files/src/player/skills/NoRecoil.cs +++ b/jRandomSkills - SRC Files/src/player/skills/NoRecoil.cs @@ -1,6 +1,5 @@ using CounterStrikeSharp.API; using CounterStrikeSharp.API.Core; -using CounterStrikeSharp.API.Modules.Cvars; using CounterStrikeSharp.API.Modules.Utils; using src.utils; using System.Collections.Concurrent; @@ -20,7 +19,7 @@ namespace src.player.skills public static void LoadSkill() { SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue(skillName, "color")); - defaultNoSpread = ConVar.Find("weapon_accuracy_nospread")?.GetPrimitiveValue() ?? false; + defaultNoSpread = SkillUtils.CvarValue("weapon_accuracy_nospread", false); } public static void NewRound() diff --git a/jRandomSkills - SRC Files/src/player/skills/Phoenix.cs b/jRandomSkills - SRC Files/src/player/skills/Phoenix.cs index b31581b..d8353f0 100644 --- a/jRandomSkills - SRC Files/src/player/skills/Phoenix.cs +++ b/jRandomSkills - SRC Files/src/player/skills/Phoenix.cs @@ -11,7 +11,6 @@ namespace src.player.skills public class Phoenix : ISkill { private const Skills skillName = Skills.Phoenix; - private const float DefaultHeadshotMultiplier = 4f; private static readonly ConcurrentDictionary phoenixTicks = new(); public static void LoadSkill() @@ -53,11 +52,7 @@ namespace src.player.skills if (SkillUtils.IsFriendlyFireBlocked(damageInfo, victimPawn)) return; - float effectiveDamage = damageInfo.Damage; - if (SkillUtils.GetHitGroup(damageInfo) == HitGroup_t.HITGROUP_HEAD) - effectiveDamage *= GetHeadshotMultiplier(damageInfo); - - if (effectiveDamage < victimPawn.Health) return; + if (!SkillUtils.IsPredictedLethal(damageInfo, victimPawn)) return; if (TryConsumeRevive(victim, victimPawn)) damageInfo.Damage = 0; @@ -99,20 +94,6 @@ namespace src.player.skills return true; } - private static float GetHeadshotMultiplier(CTakeDamageInfo info) - { - var ability = info.Ability?.Value; - if (ability == null || !ability.IsValid) return DefaultHeadshotMultiplier; - - var weapon = ability.As(); - if (weapon == null || !weapon.IsValid) return DefaultHeadshotMultiplier; - - var vdata = weapon.GetVData(); - if (vdata == null || vdata.HeadshotMultiplier <= 0) return DefaultHeadshotMultiplier; - - return vdata.HeadshotMultiplier; - } - public static void EnableSkill(CCSPlayerController player) { var playerInfo = PlayerManager.GetPlayerByIndex(player!.Index); diff --git a/jRandomSkills - SRC Files/src/player/skills/Planter.cs b/jRandomSkills - SRC Files/src/player/skills/Planter.cs index ee1acb3..6fe7b94 100644 --- a/jRandomSkills - SRC Files/src/player/skills/Planter.cs +++ b/jRandomSkills - SRC Files/src/player/skills/Planter.cs @@ -1,6 +1,5 @@ using CounterStrikeSharp.API; using CounterStrikeSharp.API.Core; -using CounterStrikeSharp.API.Modules.Cvars; using CounterStrikeSharp.API.Modules.Memory; using CounterStrikeSharp.API.Modules.Utils; using src.utils; @@ -20,7 +19,7 @@ namespace src.player.skills public static void LoadSkill() { SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue(skillName, "color")); - defaultC4Timer = ConVar.Find("mp_c4timer")?.GetPrimitiveValue() ?? 40; + defaultC4Timer = SkillUtils.CvarValue("mp_c4timer", 40); } public static void EnableSkill(CCSPlayerController player) @@ -96,7 +95,7 @@ namespace src.player.skills float currentTime = Server.CurrentTime; bool hudFrame = SkillUtils.IsHudFrame(); - foreach (var player in PlayerManager.GetTickPlayers().Where(p => p.Team == CsTeam.Terrorist)) + foreach (var player in PlayerManager.GetTickPlayers().Where(p => p != null && p.IsValid && p.Team == CsTeam.Terrorist)) { if (!Instance.IsPlayerValid(player)) continue; diff --git a/jRandomSkills - SRC Files/src/player/skills/Push.cs b/jRandomSkills - SRC Files/src/player/skills/Push.cs index d5e9115..3464dc1 100644 --- a/jRandomSkills - SRC Files/src/player/skills/Push.cs +++ b/jRandomSkills - SRC Files/src/player/skills/Push.cs @@ -23,6 +23,8 @@ namespace src.player.skills if (!Instance.IsPlayerValid(attacker) || !Instance.IsPlayerValid(victim) || attacker == victim) return; + if (!SkillUtils.FiresBullets(@event.Weapon)) return; + if (Heavyweight.Resists(victim)) return; var playerInfo = PlayerManager.GetPlayerByIndex(attacker!.Index); diff --git a/jRandomSkills - SRC Files/src/player/skills/ReZombie.cs b/jRandomSkills - SRC Files/src/player/skills/ReZombie.cs index 4a4d93a..d5451e2 100644 --- a/jRandomSkills - SRC Files/src/player/skills/ReZombie.cs +++ b/jRandomSkills - SRC Files/src/player/skills/ReZombie.cs @@ -11,7 +11,6 @@ namespace src.player.skills public class ReZombie : ISkill { private const Skills skillName = Skills.ReZombie; - private const float DefaultHeadshotMultiplier = 4f; private static readonly ConcurrentDictionary zombies = []; private static readonly object setLock = new(); @@ -88,11 +87,7 @@ namespace src.player.skills // Friendly-fire-off teammate hit deals 0 damage; don't zombify over a hit that never lands. if (SkillUtils.IsFriendlyFireBlocked(damageInfo, victimPawn)) return; - float effectiveDamage = damageInfo.Damage; - if (SkillUtils.GetHitGroup(damageInfo) == HitGroup_t.HITGROUP_HEAD) - effectiveDamage *= GetHeadshotMultiplier(damageInfo); - - if (effectiveDamage < victimPawn.Health) return; // survivable, let it through + if (!SkillUtils.IsPredictedLethal(damageInfo, victimPawn)) return; // survivable, let it through if (TryBecomeZombie(victim, victimPawn)) damageInfo.Damage = 0; @@ -138,20 +133,6 @@ namespace src.player.skills holdTime: 600); } - private static float GetHeadshotMultiplier(CTakeDamageInfo info) - { - var ability = info.Ability?.Value; - if (ability == null || !ability.IsValid) return DefaultHeadshotMultiplier; - - var weapon = ability.As(); - if (weapon == null || !weapon.IsValid) return DefaultHeadshotMultiplier; - - var vdata = weapon.GetVData(); - if (vdata == null || vdata.HeadshotMultiplier <= 0) return DefaultHeadshotMultiplier; - - return vdata.HeadshotMultiplier; - } - private static void DropAllBotWeapons(CCSPlayerController player) { if (player == null || !player.IsValid || !player.IsBot) return; diff --git a/jRandomSkills - SRC Files/src/player/skills/RichBoy.cs b/jRandomSkills - SRC Files/src/player/skills/RichBoy.cs index cdce359..42bf074 100644 --- a/jRandomSkills - SRC Files/src/player/skills/RichBoy.cs +++ b/jRandomSkills - SRC Files/src/player/skills/RichBoy.cs @@ -1,8 +1,8 @@ using CounterStrikeSharp.API; using CounterStrikeSharp.API.Core; -using CounterStrikeSharp.API.Modules.Cvars; using CounterStrikeSharp.API.Modules.Utils; using src.utils; +using System.Collections.Concurrent; using static src.jRandomSkills; namespace src.player.skills @@ -11,48 +11,49 @@ namespace src.player.skills { private const Skills skillName = Skills.RichBoy; + private static readonly ConcurrentDictionary accountBeforeBonus = []; + public static void LoadSkill() { SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue(skillName, "color")); } - private static int GetMaxMoney() => ConVar.Find("mp_maxmoney")?.GetPrimitiveValue() ?? 16000; + private static int GetMaxMoney() => SkillUtils.CvarValue("mp_maxmoney", 16000); public static void EnableSkill(CCSPlayerController player) { - var playerInfo = PlayerManager.GetPlayerByIndex(player!.Index); - if (playerInfo == null) return; - int moneyBonus = Instance.Random.Next(SkillsInfo.GetValue(skillName, "minMoney"), SkillsInfo.GetValue(skillName, "maxMoney")); + if (player == null || !player.IsValid) return; var moneyServices = player.InGameMoneyServices; if (moneyServices == null) return; + int moneyBonus = Instance.Random.Next(SkillsInfo.GetValue(skillName, "minMoney"), SkillsInfo.GetValue(skillName, "maxMoney")); moneyBonus = Math.Min(moneyBonus, GetMaxMoney() - moneyServices.Account); + if (moneyBonus <= 0) return; - playerInfo.SkillChance = moneyBonus; - AddMoney(player, moneyBonus); + accountBeforeBonus[player.Index] = moneyServices.Account; + + moneyServices.Account += moneyBonus; + Utilities.SetStateChanged(player, "CCSPlayerController", "m_pInGameMoneyServices"); } public static void DisableSkill(CCSPlayerController player) { - var playerInfo = PlayerManager.GetPlayerByIndex(player!.Index); - if (playerInfo == null) return; + if (player == null || !player.IsValid) return; + if (!accountBeforeBonus.TryRemove(player.Index, out int accountBefore)) return; var moneyServices = player.InGameMoneyServices; if (moneyServices == null) return; - int money = Math.Abs((int)playerInfo.SkillChance! - moneyServices.CashSpentThisRound); - AddMoney(player, -money, 3000); + if (moneyServices.Account <= accountBefore) return; + + moneyServices.Account = accountBefore; + Utilities.SetStateChanged(player, "CCSPlayerController", "m_pInGameMoneyServices"); } - private static void AddMoney(CCSPlayerController player, int money, int minimum = 0) + public static void PlayerDisconnect(uint playerIndex) { - if (player == null || !player.IsValid) return; - var moneyServices = player.InGameMoneyServices; - if (moneyServices == null) return; - - moneyServices.Account = Math.Clamp(moneyServices.Account + money, minimum, GetMaxMoney()); - Utilities.SetStateChanged(player, "CCSPlayerController", "m_pInGameMoneyServices"); + accountBeforeBonus.TryRemove(playerIndex, out _); } public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#D4AF37", CsTeam onlyTeam = CsTeam.None, bool disableOnFreezeTime = false, bool needsTeammates = false, string requiredPermission = "", float? hudDuration = null, float? descriptionHudDuration = null, int maxPerServer = -1, Rarity rarity = Rarity.Common, int minMoney = 5000, int maxMoney = 15000) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity) diff --git a/jRandomSkills - SRC Files/src/player/skills/RobinHood.cs b/jRandomSkills - SRC Files/src/player/skills/RobinHood.cs index b5f1b4a..7f92aff 100644 --- a/jRandomSkills - SRC Files/src/player/skills/RobinHood.cs +++ b/jRandomSkills - SRC Files/src/player/skills/RobinHood.cs @@ -1,6 +1,5 @@ using CounterStrikeSharp.API; using CounterStrikeSharp.API.Core; -using CounterStrikeSharp.API.Modules.Cvars; using CounterStrikeSharp.API.Modules.Utils; using src.utils; using static src.jRandomSkills; @@ -16,7 +15,7 @@ namespace src.player.skills SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue(skillName, "color")); } - private static int GetMaxMoney() => ConVar.Find("mp_maxmoney")?.GetPrimitiveValue() ?? 16000; + private static int GetMaxMoney() => SkillUtils.CvarValue("mp_maxmoney", 16000); public static void PlayerHurt(EventPlayerHurt @event) { diff --git a/jRandomSkills - SRC Files/src/player/skills/Rubber.cs b/jRandomSkills - SRC Files/src/player/skills/Rubber.cs index 1d6b817..d1d0f12 100644 --- a/jRandomSkills - SRC Files/src/player/skills/Rubber.cs +++ b/jRandomSkills - SRC Files/src/player/skills/Rubber.cs @@ -28,6 +28,7 @@ namespace src.player.skills var victim = PlayerManager.GetPlayerEvent(@event.Userid); if (!Instance.IsPlayerValid(attacker) || !Instance.IsPlayerValid(victim) || attacker == victim) return; + if (!SkillUtils.FiresBullets(@event.Weapon)) return; var attackerInfo = PlayerManager.GetPlayerByIndex(attacker!.Index); if (Heavyweight.Resists(victim)) return; diff --git a/jRandomSkills - SRC Files/src/player/skills/SecondLife.cs b/jRandomSkills - SRC Files/src/player/skills/SecondLife.cs index b32357b..1aac6bc 100644 --- a/jRandomSkills - SRC Files/src/player/skills/SecondLife.cs +++ b/jRandomSkills - SRC Files/src/player/skills/SecondLife.cs @@ -11,7 +11,6 @@ namespace src.player.skills public class SecondLife : ISkill { private const Skills skillName = Skills.SecondLife; - private const float DefaultHeadshotMultiplier = 4f; private static readonly ConcurrentDictionary usedThisRound = []; private static readonly object setLock = new(); @@ -45,11 +44,7 @@ namespace src.player.skills if (SkillUtils.IsFriendlyFireBlocked(damageInfo, victimPawn)) return; - float effectiveDamage = damageInfo.Damage; - if (SkillUtils.GetHitGroup(damageInfo) == HitGroup_t.HITGROUP_HEAD) - effectiveDamage *= GetHeadshotMultiplier(damageInfo); - - if (effectiveDamage < victimPawn.Health) return; + if (!SkillUtils.IsPredictedLethal(damageInfo, victimPawn)) return; if (usedThisRound.TryGetValue(victim.Handle, out int savedTick)) { @@ -62,20 +57,6 @@ namespace src.player.skills damageInfo.Damage = 0; } - private static float GetHeadshotMultiplier(CTakeDamageInfo info) - { - var ability = info.Ability?.Value; - if (ability == null || !ability.IsValid) return DefaultHeadshotMultiplier; - - var weapon = ability.As(); - if (weapon == null || !weapon.IsValid) return DefaultHeadshotMultiplier; - - var vdata = weapon.GetVData(); - if (vdata == null || vdata.HeadshotMultiplier <= 0) return DefaultHeadshotMultiplier; - - return vdata.HeadshotMultiplier; - } - public static bool TryConsumeRevive(CCSPlayerController? victim, CCSPlayerPawn? victimPawn) { if (victim == null || !victim.IsValid) return false; diff --git a/jRandomSkills - SRC Files/src/player/skills/Shade.cs b/jRandomSkills - SRC Files/src/player/skills/Shade.cs index 9ca3a91..3b1d5f0 100644 --- a/jRandomSkills - SRC Files/src/player/skills/Shade.cs +++ b/jRandomSkills - SRC Files/src/player/skills/Shade.cs @@ -32,6 +32,7 @@ namespace src.player.skills if (!Instance.IsPlayerValid(attacker) || !Instance.IsPlayerValid(victim)) return; if (attacker!.Index == victim!.Index) return; + if (!SkillUtils.FiresBullets(@event.Weapon)) return; var victimInfo = PlayerManager.GetPlayerByIndex(victim!.Index); var attackerInfo = PlayerManager.GetPlayerByIndex(attacker!.Index); diff --git a/jRandomSkills - SRC Files/src/player/skills/ShortBomb.cs b/jRandomSkills - SRC Files/src/player/skills/ShortBomb.cs index d315095..d8d223d 100644 --- a/jRandomSkills - SRC Files/src/player/skills/ShortBomb.cs +++ b/jRandomSkills - SRC Files/src/player/skills/ShortBomb.cs @@ -1,6 +1,5 @@ using CounterStrikeSharp.API; using CounterStrikeSharp.API.Core; -using CounterStrikeSharp.API.Modules.Cvars; using CounterStrikeSharp.API.Modules.Utils; using src.utils; using static src.jRandomSkills; @@ -17,7 +16,7 @@ namespace src.player.skills public static void LoadSkill() { SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue(skillName, "color")); - defaultC4Timer = ConVar.Find("mp_c4timer")?.GetPrimitiveValue() ?? 40; + defaultC4Timer = SkillUtils.CvarValue("mp_c4timer", 40); } public static void EnableSkill(CCSPlayerController player) diff --git a/jRandomSkills - SRC Files/src/player/skills/ThrowingKnife.cs b/jRandomSkills - SRC Files/src/player/skills/ThrowingKnife.cs index d32f35f..11ffc30 100644 --- a/jRandomSkills - SRC Files/src/player/skills/ThrowingKnife.cs +++ b/jRandomSkills - SRC Files/src/player/skills/ThrowingKnife.cs @@ -26,23 +26,30 @@ namespace src.player.skills } private static bool roundEnded; + private static readonly ConcurrentDictionary owedKnife = []; public static void NewRound() { roundEnded = false; KillAllKnives(); + owedKnife.Clear(); } public static void RoundEnd() { roundEnded = true; - KillAllKnives(); + KillAllKnives(rememberOwed: true); } - private static void KillAllKnives() + private static void KillAllKnives(bool rememberOwed = false) { foreach (var knifeInfo in knivesInfo.Values.ToArray()) + { + if (rememberOwed && knifeInfo.IsDropped) + owedKnife[knifeInfo.PlayerIndex] = 0; + DisableKnifeSkill(knifeInfo); + } knivesInfo.Clear(); } @@ -155,18 +162,24 @@ namespace src.player.skills public static void DisableSkill(CCSPlayerController player) { + if (player == null || !player.IsValid) return; + + bool wasDropped = owedKnife.TryRemove(player.Index, out _); + if (knivesInfo.TryRemove(player.Index, out KnifeInfo? knifeInfo) && knifeInfo != null) { - bool wasDropped = knifeInfo.IsDropped; + wasDropped |= knifeInfo.IsDropped; DisableKnifeSkill(knifeInfo); - - if (wasDropped && player != null && player.IsValid && player.PawnIsAlive) - player.GiveNamedItem("weapon_knife"); } + + if (wasDropped && player.PawnIsAlive) + player.GiveNamedItem("weapon_knife"); } public static void PlayerDisconnect(uint playerIndex) { + owedKnife.TryRemove(playerIndex, out _); + if (knivesInfo.TryRemove(playerIndex, out KnifeInfo? knifeInfo) && knifeInfo != null) DisableKnifeSkill(knifeInfo); } diff --git a/jRandomSkills - SRC Files/src/player/skills/TrueArmor.cs b/jRandomSkills - SRC Files/src/player/skills/TrueArmor.cs index 39a1582..ba7d0c8 100644 --- a/jRandomSkills - SRC Files/src/player/skills/TrueArmor.cs +++ b/jRandomSkills - SRC Files/src/player/skills/TrueArmor.cs @@ -12,7 +12,7 @@ namespace src.player.skills private const Skills skillName = Skills.TrueArmor; private static readonly ConcurrentDictionary holders = []; - private static readonly ConcurrentDictionary pending = []; + private static readonly ConcurrentDictionary pending = []; public static void LoadSkill() { @@ -62,13 +62,16 @@ namespace src.player.skills info.Damage = damage - absorbed; - pending[victim.Index] = (int)MathF.Round(armor - absorbed); + int restoredArmor = (int)MathF.Round(armor - absorbed); + bool hadHelmet = victimPawn.ItemServices?.As()?.HasHelmet ?? false; + + pending[victim.Index] = new PendingArmor(restoredArmor, hadHelmet); victimPawn.ArmorValue = 0; Utilities.SetStateChanged(victimPawn, "CCSPlayerPawn", "m_ArmorValue"); Debug.WriteToDebug($"[TrueArmor] {victim.PlayerName}: raw={damage:0.#} absorbed={absorbed:0.#} " + - $"passed={info.Damage:0.#} armor={armor}->{pending[victim.Index]} hp={victimPawn.Health}", DebugCategory.Damage); + $"passed={info.Damage:0.#} armor={armor}->{restoredArmor} helmet={hadHelmet} hp={victimPawn.Health}", DebugCategory.Damage); } public static void OnTakeDamagePost(CBaseEntity damagedEntity, CTakeDamageInfo damageInfo, CTakeDamageResult damageResult) @@ -76,12 +79,22 @@ namespace src.player.skills if (pending.IsEmpty) return; if (!TryResolveVictim(damagedEntity, damageInfo, out var victim, out var victimPawn, out _)) return; - if (!pending.TryRemove(victim!.Index, out int restored)) return; + if (!pending.TryRemove(victim!.Index, out var restored)) return; - victimPawn!.ArmorValue = restored; + victimPawn!.ArmorValue = restored.Armor; Utilities.SetStateChanged(victimPawn, "CCSPlayerPawn", "m_ArmorValue"); + + if (!restored.HasHelmet) return; + + var itemServices = victimPawn.ItemServices?.As(); + if (itemServices == null || itemServices.HasHelmet) return; + + itemServices.HasHelmet = true; + Utilities.SetStateChanged(victim, "CCSPlayerController", "m_bPawnHasHelmet"); } + private readonly record struct PendingArmor(int Armor, bool HasHelmet); + private static bool TryResolveVictim(CBaseEntity damagedEntity, CTakeDamageInfo damageInfo, out CCSPlayerController? victim, out CCSPlayerPawn? pawn, out CTakeDamageInfo? info) { victim = null; diff --git a/jRandomSkills - SRC Files/src/utils/Config.cs b/jRandomSkills - SRC Files/src/utils/Config.cs index 8766989..a39b120 100644 --- a/jRandomSkills - SRC Files/src/utils/Config.cs +++ b/jRandomSkills - SRC Files/src/utils/Config.cs @@ -24,9 +24,11 @@ namespace src.utils { var newConfig = new SettingsModel(); + Instance.Logger.LogInformation("config path: {Path} (exists={Exists})", configPath, File.Exists(configPath)); + if (!File.Exists(configPath)) { - Instance.Logger.LogInformation("Config file does not exist. Create a new config file..."); + Instance.Logger.LogError("config.json NOT FOUND at the path above; a fresh file with defaults is being written."); SaveConfig(newConfig); debugFlags = DebugCategories.Parse(newConfig.DebugMode); return config = newConfig; @@ -43,19 +45,44 @@ namespace src.utils if (HasMissingKeys(json) || IsLegacyDebugMode(json)) SaveConfig(newConfig); } - catch + catch (Exception ex) { - Instance.Logger.LogError("Error when loading the config file."); + Instance.Logger.LogError("Error when loading the config file: {Message}", ex.Message); + + if (config != null) + { + Instance.Logger.LogError("config.json was not applied; the previously loaded settings are kept."); + return config; + } } if (newConfig.DisplayAlwaysDescription) newConfig.SkillDescriptionDuration = 9999; debugFlags = DebugCategories.Parse(newConfig.DebugMode); + ApplyRarityTables(newConfig); return config = newConfig; } } + private static void ApplyRarityTables(SettingsModel model) + { + RarityManager.SetRarityPercentages(ToRarityTable(model.SkillsChance)); + RarityManager.SetVipRarityPercentages(ToRarityTable(model.VIPSkillsChance)); + } + + private static Dictionary ToRarityTable(Dictionary? source) + { + var table = new Dictionary(); + if (source == null) return table; + + foreach (var kv in source) + if (Enum.TryParse(kv.Key, true, out var rarity)) + table[rarity] = kv.Value; + + return table; + } + private static bool HasMissingKeys(string json) { try @@ -111,12 +138,11 @@ namespace src.utils string tempPath = $"{configPath}.temp"; File.WriteAllText(tempPath, json); - File.Copy(tempPath, configPath, overwrite: true); - File.Delete(tempPath); + File.Move(tempPath, configPath, overwrite: true); } - catch + catch (Exception ex) { - Instance.Logger.LogError("Error when saving the config file."); + Instance.Logger.LogError("Error when saving the config file: {Message}", ex.Message); } } } @@ -146,6 +172,11 @@ namespace src.utils public bool TraceRayBeam { get; set; } public string DisableHUDOnDeathPermission { get; set; } public bool DisableSkillsOnRoundEnd { get; set; } + public string VIPFlag { get; set; } + [JsonProperty(ObjectCreationHandling = ObjectCreationHandling.Replace)] + public Dictionary SkillsChance { get; set; } + [JsonProperty(ObjectCreationHandling = ObjectCreationHandling.Replace)] + public Dictionary VIPSkillsChance { get; set; } public int? CurseSkillPerPlayer { get; set; } public bool ShowDecoyRing { get; set; } public WeaponPools Weapons { get; set; } @@ -179,6 +210,25 @@ namespace src.utils HideHudForOtherPlugins = true; DisableHUDOnDeathPermission = "@jRandomSkills/death"; DisableSkillsOnRoundEnd = false; + VIPFlag = "@css/vip"; + + SkillsChance = new Dictionary + { + ["Common"] = 0.70f, + ["Uncommon"] = 0.14f, + ["Rare"] = 0.10f, + ["Epic"] = 0.05f, + ["Legendary"] = 0.01f, + }; + + VIPSkillsChance = new Dictionary + { + ["Common"] = 0.55f, + ["Uncommon"] = 0.23f, + ["Rare"] = 0.14f, + ["Epic"] = 0.07f, + ["Legendary"] = 0.01f, + }; CurseSkillPerPlayer = null; ShowDecoyRing = true; diff --git a/jRandomSkills - SRC Files/src/utils/EntityManager.cs b/jRandomSkills - SRC Files/src/utils/EntityManager.cs index 83a2068..ec57874 100644 --- a/jRandomSkills - SRC Files/src/utils/EntityManager.cs +++ b/jRandomSkills - SRC Files/src/utils/EntityManager.cs @@ -264,7 +264,8 @@ namespace src.utils } // Dying entities stay out of transmit until the engine processes the kill (Event.CheckTransmit). - private static readonly ConcurrentDictionary recentlyDestroyed = new(); + private readonly record struct DyingEntity(DateTime Expires, uint HandleRaw); + private static readonly ConcurrentDictionary recentlyDestroyed = new(); public static List GetRecentlyDestroyedSnapshot() { @@ -274,8 +275,20 @@ namespace src.utils var result = new List(); foreach (var kvp in recentlyDestroyed) { - if (now > kvp.Value) recentlyDestroyed.TryRemove(kvp.Key, out _); - else result.Add(kvp.Key); + if (now > kvp.Value.Expires) + { + recentlyDestroyed.TryRemove(kvp.Key, out _); + continue; + } + + var entity = Utilities.GetEntityFromIndex((int)kvp.Key); + if (entity == null || !entity.IsValid || entity.EntityHandle.Raw != kvp.Value.HandleRaw) + { + recentlyDestroyed.TryRemove(kvp.Key, out _); + continue; + } + + result.Add(kvp.Key); } return result; } @@ -297,7 +310,7 @@ namespace src.utils var entity = Utilities.GetEntityFromIndex((int)entityIndex); if (entity != null && entity.IsValid) { - recentlyDestroyed[entityIndex] = DateTime.UtcNow.AddSeconds(delay + 2.0); + recentlyDestroyed[entityIndex] = new DyingEntity(DateTime.UtcNow.AddSeconds(delay + 0.5), entity.EntityHandle.Raw); // Detach first so no follower is left on a freed parent. entity.AcceptInput("ClearParent"); entity.AddEntityIOEvent("Kill", entity, delay: delay); diff --git a/jRandomSkills - SRC Files/src/utils/RarityManager.cs b/jRandomSkills - SRC Files/src/utils/RarityManager.cs index 002a8f2..8580906 100644 --- a/jRandomSkills - SRC Files/src/utils/RarityManager.cs +++ b/jRandomSkills - SRC Files/src/utils/RarityManager.cs @@ -23,6 +23,15 @@ namespace src.utils { Rarity.Legendary, 1f } }; + private static Dictionary vipRarityPercentages = new() + { + { Rarity.Common, 55f }, + { Rarity.Uncommon, 23f }, + { Rarity.Rare, 14f }, + { Rarity.Epic, 7f }, + { Rarity.Legendary, 1f } + }; + public static IReadOnlyDictionary RarityPercentages { get @@ -34,26 +43,39 @@ namespace src.utils public static void SetRarityPercentages(IDictionary percentages) { - if (percentages == null || percentages.Count == 0) return; + var table = Normalize(percentages); + if (table == null) return; lock (rarityLock) - { - double sum = percentages.Values.Sum(v => (double)v); - if (sum <= 0) - return; + rarityPercentages = table; + } - if (Math.Abs(sum - 100.0) > 0.0001) - { - var normalized = new Dictionary(); + public static void SetVipRarityPercentages(IDictionary percentages) + { + var table = Normalize(percentages); + if (table == null) return; - foreach (var kv in percentages) - normalized[kv.Key] = (float)((kv.Value / sum) * 100.0); + lock (rarityLock) + vipRarityPercentages = table; + } - rarityPercentages = normalized; - } - else - rarityPercentages = percentages.ToDictionary(k => k.Key, v => v.Value); - } + // Accepts either percentages (70, 14, ...) or fractions (0.7, 0.14, ...); both are + // rescaled so the table sums to 100. + private static Dictionary? Normalize(IDictionary percentages) + { + if (percentages == null || percentages.Count == 0) return null; + + double sum = percentages.Values.Sum(v => (double)v); + if (sum <= 0) return null; + + if (Math.Abs(sum - 100.0) <= 0.0001) + return percentages.ToDictionary(k => k.Key, v => v.Value); + + var normalized = new Dictionary(); + foreach (var kv in percentages) + normalized[kv.Key] = (float)((kv.Value / sum) * 100.0); + + return normalized; } public static float GetRarityPercentage(Rarity rarity) @@ -62,16 +84,18 @@ namespace src.utils return rarityPercentages.TryGetValue(rarity, out var v) ? v : 0f; } - public static (double, Rarity) RollRarity() + public static (double, Rarity) RollRarity(bool vip = false) { double roll = Random.Shared.NextDouble() * 100.0; double accum = 0.0; lock (rarityLock) { + var table = vip ? vipRarityPercentages : rarityPercentages; + foreach (var r in Enum.GetValues(typeof(Rarity)).Cast()) { - float chance = rarityPercentages.TryGetValue(r, out var val) ? val : 0f; + float chance = table.TryGetValue(r, out var val) ? val : 0f; accum += chance; if (roll <= accum) return (roll, r); diff --git a/jRandomSkills - SRC Files/src/utils/SkillUtils.cs b/jRandomSkills - SRC Files/src/utils/SkillUtils.cs index 9599df3..1e7335a 100644 --- a/jRandomSkills - SRC Files/src/utils/SkillUtils.cs +++ b/jRandomSkills - SRC Files/src/utils/SkillUtils.cs @@ -21,6 +21,32 @@ namespace src.utils { public static class SkillUtils { + private static readonly ConcurrentDictionary cvarCache = []; + + public static ConVar? Cvar(string name) + { + if (cvarCache.TryGetValue(name, out var cached)) return cached; + + var cvar = ConVar.Find(name); + if (cvar != null) cvarCache[name] = cvar; + return cvar; + } + + public static T CvarValue(string name, T fallback) where T : unmanaged + { + var cvar = Cvar(name); + if (cvar == null) return fallback; + + try { return cvar.GetPrimitiveValue(); } + catch { return fallback; } + } + + public static string CvarString(string name, string fallback) + { + var cvar = Cvar(name); + return cvar == null ? fallback : cvar.StringValue; + } + private static Lazy LazySig(string name, Func factory) where T : class => new(() => { @@ -82,9 +108,9 @@ namespace src.utils try { - if (ConVar.Find("mp_halftime")?.GetPrimitiveValue() != true) return false; + if (!CvarValue("mp_halftime", false)) return false; - int maxRounds = ConVar.Find("mp_maxrounds")?.GetPrimitiveValue() ?? 0; + int maxRounds = CvarValue("mp_maxrounds", 0); return maxRounds > 0 && maxRounds / 2 == gameRules.TotalRoundsPlayed; } catch @@ -310,6 +336,62 @@ namespace src.utils return (HitGroup_t)Marshal.ReadInt32(hitGroupData, 56); } + private const float DefaultHeadshotMultiplier = 4f; + private const float StomachMultiplier = 1.25f; + private const float LegMultiplier = 0.75f; + + public static float GetAppliedDamageScale(CTakeDamageInfo? info, CCSPlayerPawn? victimPawn) + { + if (info == null || victimPawn == null || !victimPawn.IsValid) return 1f; + + var hitGroup = GetHitGroup(info); + var vdata = GetWeaponVData(info); + + float scale = hitGroup switch + { + HitGroup_t.HITGROUP_HEAD => vdata != null && vdata.HeadshotMultiplier > 0 ? vdata.HeadshotMultiplier : DefaultHeadshotMultiplier, + HitGroup_t.HITGROUP_STOMACH => StomachMultiplier, + HitGroup_t.HITGROUP_LEFTLEG or HitGroup_t.HITGROUP_RIGHTLEG => LegMultiplier, + _ => 1f, + }; + + if (vdata == null || vdata.ArmorRatio <= 0 || vdata.ArmorRatio >= 1f) return scale; + if (victimPawn.ArmorValue <= 0) return scale; + if (!ArmorCovers(hitGroup, victimPawn)) return scale; + + return scale * vdata.ArmorRatio; + } + + public static float PredictAppliedDamage(CTakeDamageInfo? info, CCSPlayerPawn? victimPawn) + { + if (info == null) return 0f; + return info.Damage * GetAppliedDamageScale(info, victimPawn); + } + + public static bool IsPredictedLethal(CTakeDamageInfo? info, CCSPlayerPawn? victimPawn) + { + if (victimPawn == null || !victimPawn.IsValid) return false; + return PredictAppliedDamage(info, victimPawn) >= victimPawn.Health; + } + + private static bool ArmorCovers(HitGroup_t hitGroup, CCSPlayerPawn victimPawn) => hitGroup switch + { + HitGroup_t.HITGROUP_LEFTLEG or HitGroup_t.HITGROUP_RIGHTLEG => false, + HitGroup_t.HITGROUP_HEAD => victimPawn.ItemServices?.As()?.HasHelmet ?? false, + _ => true, + }; + + private static CCSWeaponBaseVData? GetWeaponVData(CTakeDamageInfo info) + { + var ability = info.Ability?.Value; + if (ability == null || !ability.IsValid) return null; + + var weapon = ability.As(); + if (weapon == null || !weapon.IsValid) return null; + + return weapon.GetVData(); + } + public static void CreateHEGrenadeProjectile(Vector pos, QAngle angle, Vector vel, int teamNum) { HEGrenadeProjectile_CreateFunc.Value?.Invoke(pos.Handle, angle.Handle, vel.Handle, vel.Handle, IntPtr.Zero, 44, teamNum); @@ -341,8 +423,8 @@ namespace src.utils if (!attackerPawn.IsValid || attackerPawn.DesignerName != "player") return false; // non-player inflictor if (attackerPawn.TeamNum != victimPawn.TeamNum) return false; // enemy -> real damage - bool ff = ConVar.Find("mp_friendlyfire")?.GetPrimitiveValue() ?? false; - bool tae = ConVar.Find("mp_teammates_are_enemies")?.GetPrimitiveValue() ?? false; + bool ff = CvarValue("mp_friendlyfire", false); + bool tae = CvarValue("mp_teammates_are_enemies", false); return !ff && !tae; // same team + FF off -> engine will zero this damage } @@ -351,8 +433,8 @@ namespace src.utils if (!IsSameTeamHit(info, victimPawn)) return false; if (!SkillsInfo.GetValue(skill, "friendlyFire")) return true; - bool ff = ConVar.Find("mp_friendlyfire")?.GetPrimitiveValue() ?? false; - bool tae = ConVar.Find("mp_teammates_are_enemies")?.GetPrimitiveValue() ?? false; + bool ff = CvarValue("mp_friendlyfire", false); + bool tae = CvarValue("mp_teammates_are_enemies", false); return !ff && !tae; } @@ -1199,17 +1281,17 @@ namespace src.utils private static void AwardRoundEndMoney(CsTeam winnerTeam, CCSPlayerController? bonusPlayer = null) { int winnerReward = winnerTeam == CsTeam.CounterTerrorist - ? ConVar.Find("cash_team_win_by_defusing_bomb")?.GetPrimitiveValue() ?? 3500 - : ConVar.Find("cash_team_terrorist_win_bomb")?.GetPrimitiveValue() ?? 3500; + ? CvarValue("cash_team_win_by_defusing_bomb", 3500) + : CvarValue("cash_team_terrorist_win_bomb", 3500); if (winnerReward <= 0) return; - int maxMoney = ConVar.Find("mp_maxmoney")?.GetPrimitiveValue() ?? 16000; + int maxMoney = CvarValue("mp_maxmoney", 16000); int personalBonus = bonusPlayer == null ? 0 : winnerTeam == CsTeam.CounterTerrorist - ? ConVar.Find("cash_player_defused_bomb")?.GetPrimitiveValue() ?? 300 - : ConVar.Find("cash_player_bomb_planted")?.GetPrimitiveValue() ?? 300; + ? CvarValue("cash_player_defused_bomb", 300) + : CvarValue("cash_player_bomb_planted", 300); foreach (var player in PlayerManager.GetTickPlayers()) { @@ -1232,10 +1314,10 @@ namespace src.utils { if (jRandomSkills.Instance == null || jRandomSkills.Instance.GameRules == null) return; int totalRoundsPlayed = ctScore + tScore; - int maxRounds = ConVar.Find("mp_maxrounds")?.GetPrimitiveValue() ?? 24; + int maxRounds = CvarValue("mp_maxrounds", 24); int halfRounds = maxRounds / 2; - int overtimeMaxRounds = ConVar.Find("mp_overtime_maxrounds")?.GetPrimitiveValue() ?? 6; - int overtimeLimit = ConVar.Find("mp_overtime_limit")?.GetPrimitiveValue() ?? 1; + int overtimeMaxRounds = CvarValue("mp_overtime_maxrounds", 6); + int overtimeLimit = CvarValue("mp_overtime_limit", 1); var gameRulesProxy = jRandomSkills.Instance.GameRules; gameRulesProxy.TotalRoundsPlayed = totalRoundsPlayed; diff --git a/jRandomSkills - SRC Files/src/utils/SkillsInfo.cs b/jRandomSkills - SRC Files/src/utils/SkillsInfo.cs index 3045dd1..a6939fb 100644 --- a/jRandomSkills - SRC Files/src/utils/SkillsInfo.cs +++ b/jRandomSkills - SRC Files/src/utils/SkillsInfo.cs @@ -29,9 +29,12 @@ namespace src.utils { var newConfig = new SkillsInfoModel(); + Instance.Logger.LogInformation("skillsInfo path: {Path} (exists={Exists}, defaults built={Built})", + configPath, File.Exists(configPath), newConfig.Count); + if (!File.Exists(configPath)) { - Instance.Logger.LogInformation("Config file does not exist. Create a new skills info file..."); + Instance.Logger.LogError("skillsInfo.json NOT FOUND at the path above; a fresh file with defaults is being written."); SaveConfig(newConfig); return config = newConfig; } @@ -45,6 +48,7 @@ namespace src.utils var root = JsonConvert.DeserializeObject(json); bool needsRewrite = root == null; + bool populateFailed = false; var present = new HashSet(StringComparer.Ordinal); if (root != null) @@ -61,21 +65,40 @@ namespace src.utils if (skillObj is JObject current && HasMissingKeys(current, instance)) needsRewrite = true; - JsonConvert.PopulateObject(skillObj.ToString(), instance); + try + { + JsonConvert.PopulateObject(skillObj.ToString(), instance); + } + catch (Exception ex) + { + populateFailed = true; + Instance.Logger.LogError("skillsInfo.json: \"{Name}\" could not be read ({Message}); its defaults are kept.", name, ex.Message); + } } if (newConfig.Any(s => !string.IsNullOrEmpty(s.Name) && !present.Contains(s.Name))) needsRewrite = true; - if (needsRewrite) + Instance.Logger.LogInformation("skillsInfo.json read: {Read} entries, {Matched} matched a known skill, rewrite={Rewrite}", + root?.Count ?? 0, present.Count, needsRewrite); + + if (populateFailed) + Instance.Logger.LogError("skillsInfo.json was not rewritten because some entries failed to load; fix them before reloading."); + else if (needsRewrite) { SaveConfig(newConfig); Instance.Logger.LogInformation("skillsInfo.json was missing keys; rewritten with the defaults filled in."); } } - catch + catch (Exception ex) { - Instance.Logger.LogError("Error when loading the skills info file."); + Instance.Logger.LogError("Error when loading the skills info file: {Message}", ex.Message); + + if (config != null) + { + Instance.Logger.LogError("skillsInfo.json was not applied; the previously loaded skill settings are kept."); + return config; + } } return config = newConfig; @@ -104,12 +127,11 @@ namespace src.utils string tempPath = $"{configPath}.temp"; File.WriteAllText(tempPath, json); - File.Copy(tempPath, configPath, overwrite: true); - File.Delete(tempPath); + File.Move(tempPath, configPath, overwrite: true); } - catch + catch (Exception ex) { - Instance.Logger.LogError("Error when saving the skills info file."); + Instance.Logger.LogError("Error when saving the skills info file: {Message}", ex.Message); } } } diff --git a/jRandomSkills - Server Files/plugins/jRandomSkills/WASDMenuAPI.dll b/jRandomSkills - Server Files/plugins/jRandomSkills/WASDMenuAPI.dll index d0aee34..9719cda 100644 Binary files a/jRandomSkills - Server Files/plugins/jRandomSkills/WASDMenuAPI.dll and b/jRandomSkills - Server Files/plugins/jRandomSkills/WASDMenuAPI.dll differ diff --git a/jRandomSkills - Server Files/plugins/jRandomSkills/configs/config.json b/jRandomSkills - Server Files/plugins/jRandomSkills/configs/config.json index 9221eb3..edb4b46 100644 --- a/jRandomSkills - Server Files/plugins/jRandomSkills/configs/config.json +++ b/jRandomSkills - Server Files/plugins/jRandomSkills/configs/config.json @@ -21,6 +21,21 @@ "TraceRayBeam": false, "DisableHUDOnDeathPermission": "@jRandomSkills/death", "DisableSkillsOnRoundEnd": false, + "VIPFlag": "@css/vip", + "SkillsChance": { + "Common": 0.7, + "Uncommon": 0.14, + "Rare": 0.1, + "Epic": 0.05, + "Legendary": 0.01 + }, + "VIPSkillsChance": { + "Common": 0.55, + "Uncommon": 0.23, + "Rare": 0.14, + "Epic": 0.07, + "Legendary": 0.01 + }, "CurseSkillPerPlayer": null, "ShowDecoyRing": true, "Weapons": { diff --git a/jRandomSkills - Server Files/plugins/jRandomSkills/jRandomSkills.dll b/jRandomSkills - Server Files/plugins/jRandomSkills/jRandomSkills.dll index 1753ecb..6bd7605 100644 Binary files a/jRandomSkills - Server Files/plugins/jRandomSkills/jRandomSkills.dll and b/jRandomSkills - Server Files/plugins/jRandomSkills/jRandomSkills.dll differ diff --git a/jRandomSkills - Server Files/plugins/jRandomSkills/languages/tr.json b/jRandomSkills - Server Files/plugins/jRandomSkills/languages/tr.json index 617a3dd..05d408f 100644 --- a/jRandomSkills - Server Files/plugins/jRandomSkills/languages/tr.json +++ b/jRandomSkills - Server Files/plugins/jRandomSkills/languages/tr.json @@ -82,7 +82,7 @@ "catapult": "Mancınık", "catapult_desc": "Vurduğun rakibi uzaya (yukarı doğru) fırlatma şansın var", - "catapult_desc2": "Fırlatma şansın: %{0}", + "catapult_desc2": "Vurduğun rakibi havaya fırlatma şansın: %{0}", "chameleon": "Bukalemun", "chameleon_desc": "Öldürdüğün ilk oyuncunun yeteneği sana geçer", @@ -452,7 +452,7 @@ "push": "İtici", "push_desc": "Düşmana vurduğunda onu geri itme şansın olur", - "push_desc2": "Geri itme şansın: %{0}", + "push_desc2": "Vurduğun rakibi geri itme şansın: %{0}", "pyro": "Ateşbaz", "pyro_desc": "Molotof canını yeniler", @@ -511,7 +511,7 @@ "shade": "Gölge", "shade_desc": "Vurduğun düşmanın arkasına ışınlanma şansın olur", - "shade_desc2": "Arkasına ışınlanma şansın: %{0}", + "shade_desc2": "Vurduğun rakibin arkasına ışınlanma şansın: %{0}", "shade_nospace": "Uygun alan yok", "shortbomb": "Kısa Fünye",