jRandomSkills/jRandomSkills - SRC Files/src/player/skills/Iana.cs
ByDexter 709ed95614 v1.2.3.b5
## GENERAL
- The HUD loop now runs on the shared 16 Hz frame gate (SkillUtils.IsHudFrame) instead of keeping its own every-other-tick schedule, and fifteen skills that were refreshing their HUD on every single tick were moved onto that same gate.
- Added SkillData.GetSnapshot(), a cached array of the registered skills, so the HUD no longer copies the whole skill bag every frame. The cache is invalidated together with the existing lookup map, so a reload still picks up new skills.
- Added SkillUtils.ForceFullUpdateToAllChunked(), which spreads a full network update across frames (two clients per frame) instead of pushing one for every connected client in a single tick. The original ForceFullUpdateToAll is untouched, so the six skills that already call it keep their current behaviour.
- BlastShot, Cypher and Iana now clear their HUD line in DisableSkill, so a stale line can no longer survive the skill being taken away.

## FIXES
- SoundMaker threw a divide-by-zero on every tick when its cooldown was configured as 0, because the tick gate divides by that value. The cooldown is now clamped to at least 1.
- Regeneration and RadarHack were calling the wrong SkillsInfo.DefaultSkillInfo overload, which silently dropped disableOnFreezeTime, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer and rarity. Every configured field now reaches the base class, so those two skills finally honour their own config.
- Falcon Eye blocked the trigger on the wrong side of the check: picking up a weapon while back in the normal view locked primary and secondary fire, because the pickup handler tested for the player being in their own view instead of in the skill camera.
- Spectator locked primary and secondary fire when its camera could not be created at all, which happens when no enemy is alive to spectate. The player stayed in the normal view with a weapon that would not fire until the round ended.

## SKILL IMPROVEMENTS
- Wallhack:
  -- The glow build and the full network update now start when freeze time ends and are spread across frames, instead of running in a single tick the moment the skill is handed out. On a busy server this used to cost roughly 13 ms in one frame, close to a dropped tick at 64 tick; across four grants in an eight-hour session it no longer reaches the 2 ms profiler threshold at all.
- Jackal:
  -- CheckTransmit resolves the trail relays once per frame rather than once per viewer, so the same entity lookups are no longer repeated for every client on the server.
  -- Removed an opponent list that was gathered in EnableSkill and never used.
- ThrowingKnife:
  -- CheckTransmit builds the knife glow list once per frame, and the viewer index is resolved once per client instead of once per knife.
- Glaz:
  -- The observer target is only resolved for players who do not own the skill themselves, and the lookup now bails out early when there is no observer target at all.
- RadarHack and QuickShot:
  -- The skill check runs before the validity check, so players without the skill are skipped immediately instead of being fully validated first.
- Distancer, HealingChicken and SoundMaker:
  -- These now return straight away while nobody holds the skill, rather than walking the player list every tick.

## DEBUG
- Removed the [HITGROUP] mismatch check. Every recorded case was accounted for: ten were Aimbot rewriting the hit group as designed, and thirteen were non-bullet skill damage, which reports a generic hit group by nature. Dropping the check also removes a dictionary write from every damage event.
- Removed the identity-split tag from the damage log. It was dead by construction, because the victim controller had already been routed before the comparison ran, so it could never report a difference.
- Removed the player-shoot handler, which produced no output on this build.
- [DMG] lines now print the routing fields only when the routed index actually differs from the original one, so the check remains as a tripwire without adding noise to every line.

## TRANSLATIONS
- Turkish: reworded sixty-two entries that padded their sentences with second-person verb endings, dropped the English glosses left in parentheses, and settled every [css_useSkill] line into one shape. Also fixed genuine typos in Dracula, Giant, Thorns and Impostor.
- All eight languages received the strings for the four new skills. (AI)

## NEW SKILLS:
- Chameleon: The first player you kill gives you their skill.
- Grapple Hook: Press [css_useSkill] to fire a hook at the point you are aiming at and pull yourself there.
- Inheritance: When a teammate dies you can take over their skill.
- Ricochet: Your bullets bounce off walls and can still hit enemies.
2026-08-06 04:21:39 +03:00

477 lines
No EOL
20 KiB
C#

using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Memory.DynamicFunctions;
using CounterStrikeSharp.API.Modules.Timers;
using CounterStrikeSharp.API.Modules.Utils;
using jRandomSkills.src.utils;
using src.utils;
using System.Collections.Concurrent;
using Timer = CounterStrikeSharp.API.Modules.Timers.Timer;
using Vector = CounterStrikeSharp.API.Modules.Utils.Vector;
namespace src.player.skills
{
public class Iana : ISkill
{
private const Skills skillName = Skills.Iana;
private static readonly ConcurrentDictionary<uint, PlayerSkill> playersInfo = [];
private static readonly ConcurrentDictionary<uint, byte> consumedClones = [];
private static readonly object setLock = new();
public static void LoadSkill()
{
SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue<string>(skillName, "color"));
}
public static void NewRound()
{
foreach (var playerSkill in playersInfo.Values)
KillClone(playerSkill);
lock (setLock)
{
playersInfo.Clear();
consumedClones.Clear();
}
}
public static void WeaponPickup(EventItemPickup @event)
{
var player = PlayerManager.GetPlayerEvent(@event.Userid);
if (player == null) return;
if (playersInfo.TryGetValue(player.Index, out var playerSkill))
if (playerSkill.CloneProp != null)
BlockWeapon(player, true);
}
public static void WeaponEquip(EventItemEquip @event)
{
var player = PlayerManager.GetPlayerEvent(@event.Userid);
if (player == null) return;
if (playersInfo.TryGetValue(player.Index, out var playerSkill))
if (playerSkill.CloneProp != null)
BlockWeapon(player, true);
}
public static void UseSkill(CCSPlayerController player)
{
var playerPawn = player.PlayerPawn.Value;
if (playerPawn?.CBodyComponent == null) return;
if (playersInfo.TryGetValue(player.Index, out var playerSkill))
if (playerSkill.CloneProp == null && playerSkill.NextUse <= Server.TickCount)
{
UpdateWeapons(player, playerSkill);
CreateClone(playerSkill);
SkillUtils.SetPlayerCollisions(player, false);
}
else if (playerSkill.CloneProp != null)
KillClone(playerSkill);
}
public static void EnableSkill(CCSPlayerController player)
{
if (player == null || !player.IsValid) return;
playersInfo.TryAdd(player.Index, new PlayerSkill
{
PlayerIndex = player.Index,
CloneProp = null,
NextUse = 0,
UseTime = 0,
});
}
public static void DisableSkill(CCSPlayerController player)
{
if (player == null) return;
if (playersInfo.TryGetValue(player.Index, out var playerSkill))
KillClone(playerSkill);
playersInfo.TryRemove(player.Index, out _);
EntityManager.DestroyPlayerEntities(player.Index);
SkillUtils.ResetPrintHTML(player);
}
private static void UpdateWeapons(CCSPlayerController player, PlayerSkill playerSkill)
{
if (player == null || !player.IsValid) return;
var playerPawn = player.PlayerPawn.Value;
if (playerPawn == null || !playerPawn.IsValid || playerPawn.WeaponServices == null) return;
playerSkill.Weapons.Clear();
foreach (var weapon in playerPawn.WeaponServices.MyWeapons)
if (weapon != null && weapon.IsValid && weapon.Value != null && weapon.Value.IsValid)
playerSkill.Weapons.Add(weapon.Value.AttributeManager.Item.ItemID);
}
private static void KillClone(PlayerSkill playerSkill)
{
var player = Utilities.GetPlayerFromIndex((int)playerSkill.PlayerIndex);
if (player == null || !player.IsValid) return;
player.EmitSound("SolidMetal.BulletImpact");
CDynamicProp? cloneProp = null;
if (playerSkill.CloneProp != null)
cloneProp = Utilities.GetEntityFromIndex<CDynamicProp>((int)playerSkill.CloneProp);
if (cloneProp != null && cloneProp.IsValid && cloneProp.AbsOrigin != null && cloneProp.AbsRotation != null)
{
var playerPawn = player.PlayerPawn.Value;
if (playerPawn != null && playerPawn.IsValid)
{
Vector pos = new(cloneProp.AbsOrigin.X, cloneProp.AbsOrigin.Y, cloneProp.AbsOrigin.Z);
Server.NextFrame(() =>
{
if (playerPawn == null || !playerPawn.IsValid) return;
playerPawn.Teleport(pos);
});
}
EntityManager.DestroyEntity(cloneProp.Index);
playerSkill.CloneProp = null;
}
var eventTarget = PlayerManager.GetPlayerFromEvent(player);
if (eventTarget != null && eventTarget.IsValid)
SkillUtils.ApplyScreenColor(eventTarget, 0, 0, 0, 0, 10, 0, 2);
SkillUtils.SetPlayerCollisions(player, true);
BlockWeapon(player, false);
playerSkill.NextUse = Server.TickCount + SkillsInfo.GetValue<float>(skillName, "Cooldown") * 64;
playerSkill.UseTime = 0;
}
public static void OnTick()
{
if (!SkillUtils.IsHudFrame()) return;
foreach (var playerSkill in playersInfo.Values)
UpdateHUD(playerSkill);
}
private static CDynamicProp? CreateClone(PlayerSkill playerSkill)
{
var player = Utilities.GetPlayerFromIndex((int)playerSkill.PlayerIndex);
if (player == null || !player.IsValid) return null;
var playerPawn = player.PlayerPawn.Value;
if (playerPawn == null || !playerPawn.IsValid || playerPawn.AbsOrigin == null || playerPawn.AbsRotation == null) return null;
if (!((PlayerFlags)playerPawn.Flags).HasFlag(PlayerFlags.FL_ONGROUND))
{
playerSkill.InfoTime = Server.TickCount;
return null;
}
QAngle angle = new(0, playerPawn.EyeAngles.Y, 0);
Vector pos = playerPawn.AbsOrigin + SkillUtils.GetForwardVector(angle) * 50;
pos.Z += 2;
if (!CheckPosition(player, pos))
{
pos.Z += 10;
if (!CheckPosition(player, pos))
{
playerSkill.InfoTime = Server.TickCount;
return null;
}
}
var clone = EntityManager.CreateTrackedDynamicProp(player.Index);
if (clone == null || !clone.IsValid) return null;
clone.Collision.SolidType = SolidType_t.SOLID_VPHYSICS;
clone.CBodyComponent!.SceneNode!.Owner!.Entity!.Flags = (uint)(clone.CBodyComponent!.SceneNode!.Owner!.Entity!.Flags & ~(1 << 2));
clone.Entity!.Name = clone.Globalname = $"IanaClone_{Server.TickCount}_{player.Index}";
clone.DispatchSpawn();
Server.NextFrame(() =>
{
if (player == null || !player.IsValid) return;
if (playerPawn == null || !playerPawn.IsValid || playerPawn.AbsOrigin == null || playerPawn.AbsRotation == null) return;
if (clone == null || !clone.IsValid) return;
clone.SetModel(playerPawn!.CBodyComponent!.SceneNode!.GetSkeletonInstance().ModelState.ModelName);
clone.Teleport(playerPawn.AbsOrigin, new QAngle(0, playerPawn.V_angle.Y, 0));
playerPawn.Teleport(pos);
var eventTarget = PlayerManager.GetPlayerFromEvent(player);
if (eventTarget != null && eventTarget.IsValid)
SkillUtils.ApplyScreenColor(eventTarget, r: 255, g: 255, b: 0, a: 20, duration: 100, holdTime: 1020);
BlockWeapon(player, true);
playerSkill.UseTime = Server.TickCount;
playerSkill.NextUse = Server.TickCount + 64000;
playerSkill.CloneProp = clone.Index;
consumedClones.TryRemove(clone.Index, out _);
jRandomSkills.Instance.AddTimer(SkillsInfo.GetValue<float>(skillName, "Duration"), () =>
{
if (playerSkill.CloneProp != null)
KillClone(playerSkill);
}, CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE);
uint playerIndex = player.Index;
Timer? cloneTimer = null;
cloneTimer = jRandomSkills.Instance.AddTimer(2f, () =>
{
var target = Utilities.GetPlayerFromIndex((int)playerIndex);
if (target == null || !target.IsValid || target.LifeState != (byte)LifeState_t.LIFE_ALIVE)
{
cloneTimer?.Kill();
return;
}
if (!playersInfo.TryGetValue(target.Index, out var playerSkill) || playerSkill.CloneProp == null)
{
cloneTimer?.Kill();
return;
}
var eventTarget = PlayerManager.GetPlayerFromEvent(player);
if (eventTarget == null || !eventTarget.IsValid)
{
cloneTimer?.Kill();
return;
}
SkillUtils.ApplyScreenColor(eventTarget, r: 255, g: 255, b: 0, a: 20, duration: 100, holdTime: 1020);
}, TimerFlags.STOP_ON_MAPCHANGE | TimerFlags.REPEAT);
});
return clone;
}
private static bool CheckPosition(CCSPlayerController player, Vector endPos)
{
var playerPawn = player.PlayerPawn.Value;
Vector start = new(playerPawn!.AbsOrigin!.X, playerPawn!.AbsOrigin!.Y, endPos.Z);
var result = RayTrace.TraceHullShape(start, endPos, player);
if (!result.HasValue)
return false;
return !result.Value.DidHit;
}
private static float CalculateDamage(CCSPlayerController player, string weaponName, float damage, bool isHeadshot)
{
float calculatedDamage = damage;
var weapon = Utilities.FindAllEntitiesByDesignerName<CCSWeaponBase>(weaponName).FirstOrDefault();
if (weapon == null) return calculatedDamage;
var vdata = weapon.GetVData<CCSWeaponBaseVData>();
if (vdata == null) return calculatedDamage;
float penetration = vdata.ArmorRatio * .5f;
var eventPlayer = PlayerManager.GetPlayerFromEvent(player);
if (eventPlayer == null || !eventPlayer.IsValid)
return calculatedDamage;
if (isHeadshot)
{
calculatedDamage *= vdata.HeadshotMultiplier;
if (eventPlayer.PawnHasHelmet)
calculatedDamage *= penetration;
}
else
if (eventPlayer.PawnArmor > 0)
calculatedDamage *= penetration;
return calculatedDamage;
}
private static CCSPlayerController? GetShooter(CTakeDamageInfo info)
{
var attackerEnt = info.Attacker?.Value;
if (attackerEnt == null || !attackerEnt.IsValid) return null;
var attackerPawn = new CCSPlayerPawn(attackerEnt.Handle);
if (!attackerPawn.IsValid || attackerPawn.DesignerName != "player") return null;
if (attackerPawn.Controller?.Value == null) return null;
var shooter = PlayerManager.GetPlayerEvent(attackerPawn.Controller.Value.As<CCSPlayerController>());
return shooter != null && shooter.IsValid ? shooter : null;
}
public static void OnTakeDamage(DynamicHook h)
{
CEntityInstance param = h.GetParam<CEntityInstance>(0);
CTakeDamageInfo param2 = h.GetParam<CTakeDamageInfo>(1);
if (param == null || param.Entity == null || param2 == null)
return;
if (string.IsNullOrEmpty(param.Entity.Name)) return;
if (!param.Entity.Name.StartsWith("IanaClone_")) return;
var nameParams = param.Entity.Name.Split('_')[2];
_ = uint.TryParse(nameParams, out uint userIndex);
if (userIndex == 0) return;
var player = Utilities.GetPlayerFromIndex((int)userIndex);
if (player == null || !player.IsValid) return;
float dealDamage = param2.Damage;
if (playersInfo.TryGetValue(player.Index, out var playerSkill))
{
CDynamicProp? cloneProp = null;
if (playerSkill.CloneProp != null)
cloneProp = Utilities.GetEntityFromIndex<CDynamicProp>((int)playerSkill.CloneProp);
if (cloneProp != null && cloneProp.IsValid && cloneProp.AbsOrigin != null)
{
Vector pos = param2.DamagePosition;
Vector posCl = cloneProp.AbsOrigin;
bool isHead = false;
float viewOffset = 63.27f;
float diff = 2f;
if (pos.Z >= posCl.Z + viewOffset - diff)
isHead = true;
float damage = param2.Damage;
string? weapon = param2.Ability?.Value?.DesignerName;
if (weapon != null)
dealDamage = CalculateDamage(player, weapon, damage, isHead);
}
if (playerSkill.CloneProp != null && !consumedClones.TryAdd(playerSkill.CloneProp.Value, 0))
return;
KillClone(playerSkill);
var playerPawn = player.PlayerPawn.Value;
if (playerPawn != null)
{
KillfeedIcons? killfeed = KillfeedIconsExtensions.FromWeaponName(param2.Ability?.Value?.DesignerName);
SkillUtils.TakeHealth(playerPawn, (int)dealDamage, GetShooter(param2), killfeed ?? KillfeedIcons.Leg);
}
}
}
public static void PlayerHurt(EventPlayerHurt @event)
{
var victim = PlayerManager.GetPlayerEvent(@event.Userid);
if (victim == null || !victim.IsValid) return;
if (PlayerManager.GetPlayerByIndex(victim.Index)?.Skill != skillName) return;
if (playersInfo.TryGetValue(victim.Index, out var playerSkill))
{
if (playerSkill.CloneProp != null)
{
KillClone(playerSkill);
playerSkill.LastHit = Server.TickCount;
}
if (playerSkill.LastHit + 4 > Server.TickCount)
SkillUtils.RestoreHealth(victim);
}
}
private static void BlockWeapon(CCSPlayerController player, bool block)
{
if (player == null || !player.IsValid) return;
var pawn = player.PlayerPawn.Value;
if (pawn == null || !pawn.IsValid || pawn.WeaponServices == null) return;
foreach (var weapon in pawn.WeaponServices.MyWeapons)
if (weapon != null && weapon.IsValid && weapon.Value != null && weapon.Value.IsValid)
{
weapon.Value.NextPrimaryAttackTick = block ? int.MaxValue : Server.TickCount;
weapon.Value.NextSecondaryAttackTick = block ? int.MaxValue : Server.TickCount;
Utilities.SetStateChanged(weapon.Value, "CBasePlayerWeapon", "m_nNextPrimaryAttackTick");
Utilities.SetStateChanged(weapon.Value, "CBasePlayerWeapon", "m_nNextSecondaryAttackTick");
}
}
private static void UpdateHUD(PlayerSkill playerSkill)
{
if (playerSkill == null) return;
var player = Utilities.GetPlayerFromIndex((int)playerSkill.PlayerIndex);
if (player == null || !player.IsValid) return;
float cooldown = 0;
float time1 = (playerSkill.NextUse - Server.TickCount) / 64;
cooldown = (int)Math.Ceiling(Math.Max(time1, 0));
float duration = 0;
float time2 = SkillsInfo.GetValue<float>(skillName, "Duration") - (Server.TickCount - playerSkill.UseTime) / 64;
duration = (int)Math.Ceiling(Math.Max(time2, 0));
var playerInfo = PlayerManager.GetPlayerByIndex(player!.Index);
if (playerInfo == null) return;
if (playerSkill.InfoTime + (64 * 2) > Server.TickCount)
playerInfo.PrintHTML = $"{player.GetTranslation("shade_nospace")}";
else if (cooldown == 0 && duration == 0)
playerInfo.PrintHTML = null;
else if (duration > 0)
playerInfo.PrintHTML = $"{player.GetTranslation("hud_info", $"<font color='#00FF00'>{duration}</font>")}";
else if (cooldown > 0)
playerInfo.PrintHTML = $"{player.GetTranslation("hud_info", $"<font color='#FF0000'>{cooldown}</font>")}";
}
public static bool OnWeaponCanAcquire(DynamicHook hook, CCSPlayerController player, CEconItemView econItem, CCSWeaponBaseVData vdata)
{
string weaponName = vdata.Name;
if (string.IsNullOrEmpty(weaponName)) return false;
var playerInfo = PlayerManager.GetPlayerByIndex(player!.Index);
if (playerInfo?.Skill != skillName) return false;
if (!playersInfo.TryGetValue(player.Index, out var playerSkill)) return false;
if (playerSkill.CloneProp == null || playerSkill.Weapons.Contains(econItem.ItemID)) return false;
hook.SetReturn(AcquireResult.InvalidItem);
return true;
}
public static bool WeaponDrop(DynamicHook hook, CCSPlayerController player)
{
if (player == null || !player.IsValid) return false;
if (playersInfo.TryGetValue(player.Index, out var playerSkill))
if (playerSkill.CloneProp != null)
{
hook.SetReturn(1);
return true;
}
return false;
}
public class PlayerSkill
{
public required uint PlayerIndex { get; set; }
public uint? CloneProp { get; set; }
public float NextUse { get; set; }
public float UseTime { get; set; }
public int InfoTime { get; set; }
public int LastHit { get; set; }
public List<ulong> Weapons { get; set; } = [];
}
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#d0d930", CsTeam onlyTeam = CsTeam.None, bool disableOnFreezeTime = true, bool needsTeammates = false, string requiredPermission = "", float? hudDuration = null, float? descriptionHudDuration = null, int maxPerServer = -1, Rarity rarity = Rarity.Common, float cooldown = 30, float duration = 10) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
{
public float Cooldown { get; set; } = cooldown;
public float Duration { get; set; } = duration;
}
}
}