v1.2.3.b6

New skills (9)
- Bounty: put a cash price on an enemy's head, the killer collects it.
- EmpGrenade: grenade victims lose radar and crosshair for a while.
- GravityDecoy: the decoy alters gravity for everyone in its radius.
- Heavyweight: immune to push and slow effects from other skills.
- Nemesis: the marked player takes extra damage from everyone.
- Punisher: banks part of the damage you take and releases it on your next hit.
- Rewind: drop a marker and teleport back to it shortly after.
- TrueArmor: armor absorbs part of incoming damage instead of health.
- Voodoo: the marked enemy shares the damage you take.

Shared systems
- DecoyRing: reusable ground ring so decoy skills show their real area of effect. Adopted by FireRain, FrozenDecoy, GravityDecoy and MagneticDecoy.
- WeaponPool: single cached weapon classification (rifle/pistol/grenade) with a normalizing lookup, backed by the new config Weapons section. Replaces the duplicated hardcoded arrays in Assassin, Chicken, Ghost, NoNades, PrimaryBan, RandomWeapon, SniperElite and WeaponsSwap. [because of compatibility with WeaponRestrict]
- HUD suppression registry in SkillUtils: crosshair and radar hiding is now refcounted per owner, so overlapping skills (Glitch, Jammer, EmpGrenade) cannot restore each other's state early.
- SkillUtils.ResolveHiddenPawns: shared invisible-pawn resolution for C4Camouflage, Ghost and Ninja.
- Config gained a Weapons section with automatic migration for existing installs, plus a per-skill DisableOnPistolRound switch honoured by both the round draw and css_setskill.

Sound
- Volume rebalance
- Add SkillUtils.EmitSoundToPlayer with a private RecipientFilter. A null filter had been broadcasting every skill sound to all clients. Migrated 16 call sites: Poison, DemonEye, ToxicSmoke, CarefulBullets, Thorns, HotBomb, LastGasp, LongKnife, Nightmare, Jester, Tripwire, Medic, HealingSmoke, Rewind, Watchmaker.
- New effects: LongKnife knife-hit feedback (Weapon_Knife.Hit.Heavy/Light), Grapple hook impact, Fortnite barricade placement, Ricochet bounce, Nightmare cursed tone, Replicator/Illusionist spawn, PsychicDefusing defuse cue, TakeAmmo pickup, Tripwire and Jammer trigger tones.
- 29 skills gained a configurable SoundVolume, LongKnife also HitSoundVolume.
- Grapple: emit the hook impact at the hook entity instead of at the owner so the enemy team can locate it.
- PawelJumper: the engine already plays the first jump, only extra jumps emit.

Models and visuals
- Grapple: real hook model with configurable hookScale and hookEmbed so the hook sits in the surface it hit.
- Pilot: glowing exhaust trail parented to the pawn, active only while airborne.

Performance
- SkillsInfo.GetValue now resolves through a ConcurrentDictionary cache instead of reflecting on every call; invalidated on config reload.
- HUD render cache keyed on player, config, description flag and the three source lines; identical frames skip the rebuild.
- Glaz.CheckTransmit: hoist smoke index resolution out of the per-viewer loop and build the pawn-owner map lazily.
- playersSkills switched from ConcurrentBag to HashSet<Skills>, turning the NoRepeat filter from an O(n) scan into a set lookup.

Fixes
- NoRepeat could hand a player two or three skills in one round because the history bag was never deduplicated; the HashSet rewrite closes it.
- SniperElite: restore the saved weapon after the halftime team swap instead of dropping it on the pistol round.
- Illiterate: the holder alert now reaches chat and is exempt from its own scrambler via PrintToChat(ignoreIlliterate).
- EntityManager.DestroyBeam: zero out width and render colour before the kill so beams cannot linger visibly, with a next-frame Remove fallback.
- Ricochet: drop all 18 diagnostic Trace calls and the Trace helper; the skill measured stable over 48h and the tracing dominated the debug log.

Debug
- Tag round start, round end and freeze-time-end lines with [WARMUP] so warmup load is separable from live rounds.
This commit is contained in:
ByDexter 2026-08-09 00:37:02 +03:00
parent 615c55b2da
commit b2d2c5bf9a
93 changed files with 3414 additions and 329 deletions

View file

@ -13,9 +13,13 @@ namespace src.player.skills
public class Ghost : ISkill
{
private const Skills skillName = Skills.Ghost;
private static readonly string[] allowedWeapons = [
"weapon_molotov", "weapon_incgrenade", "weapon_flashbang", "weapon_smokegrenade", "weapon_decoy", "weapon_hegrenade", "weapon_knife", "weapon_bayonet", "weapon_c4"
];
private static readonly HashSet<string> alwaysAllowed = new(StringComparer.Ordinal)
{
"weapon_knife", "weapon_bayonet", "weapon_c4"
};
private static bool IsAllowed(string? weapon) =>
weapon != null && (alwaysAllowed.Contains(weapon) || WeaponPool.IsGrenade(weapon));
private static readonly ConcurrentDictionary<uint, byte> invisiblePlayers = [];
private const string bloodParticle = "particles/blood_impact/blood_impact_high.vpcf";
private const string cameraViewModel = "models/sprays/spray_plane.vmdl";
@ -61,38 +65,30 @@ namespace src.player.skills
var bomb = PlayerManager.GetTickBomb();
uint? bombOwnerIndex = bomb != null && bomb.IsValid ? bomb.OwnerEntity?.Index : null;
var hidden = SkillUtils.ResolveHiddenPawns(invisiblePlayers.Keys, bombOwnerIndex);
if (hidden.Count == 0) return;
foreach (var (info, player) in infoList)
{
if (player == null || !player.IsValid || player.Team == CsTeam.Spectator) continue;
var targetHandle = player.Pawn.Value?.ObserverServices?.ObserverTarget.Value?.Handle ?? nint.Zero;
foreach (var playerIndex in invisiblePlayers.Keys)
foreach (var target in hidden)
{
var playerController = Utilities.GetPlayerFromIndex((int)playerIndex);
if (playerController == null || !playerController.IsValid || playerController.Index == player.Index)
continue;
if (player.Team == playerController.Team)
continue;
var playerPawn = playerController.PlayerPawn.Value;
if (playerPawn == null || !playerPawn.IsValid) continue;
if (target.Index == player.Index) continue;
if (player.Team == target.Team) continue;
// Only the actively spectated pawn stays transmitted; hiding it breaks the camera.
if (targetHandle != nint.Zero && playerPawn.Handle == targetHandle)
continue;
if (targetHandle != nint.Zero && target.Pawn.Handle == targetHandle) continue;
var entity = Utilities.GetEntityFromIndex<CBaseEntity>((int)playerPawn.Index);
if (entity == null || !entity.IsValid) continue;
if (info.TransmitEntities.Contains(target.Pawn.Index))
info.TransmitEntities.Remove(target.Pawn.Index);
if (info.TransmitEntities.Contains(entity.Index))
info.TransmitEntities.Remove(entity.Index);
SkillUtils.HideCarriedEntities(info, playerPawn);
SkillUtils.HideCarriedEntities(info, target.Pawn);
// Hide the bomb as well, but only while this hidden player is the one holding it.
if (bomb == null || bombOwnerIndex != playerController.Index) continue;
if (bomb == null || !target.HoldsBomb) continue;
if (info.TransmitEntities.Contains(bomb.Index))
info.TransmitEntities.Remove(bomb.Index);
@ -156,7 +152,7 @@ namespace src.player.skills
foreach (var player in PlayerManager.GetTickPlayers())
{
var playerInfo = PlayerManager.GetPlayerByIndex(player!.Index);
if (playerInfo?.Skill == skillName)
if (playerInfo?.Skill == skillName && SkillUtils.IsHudFrame())
UpdateHUD(player);
if (player.LifeState != (byte)LifeState_t.LIFE_ALIVE)
@ -206,7 +202,7 @@ namespace src.player.skills
if (weapon != null && weapon.IsValid && weapon.Value != null && weapon.Value.IsValid && !string.IsNullOrEmpty(weapon.Value.DesignerName))
{
string weaponName = weapon.Value.DesignerName;
if (!allowedWeapons.Contains(weaponName))
if (!IsAllowed(weaponName))
{
weapon.Value.NextPrimaryAttackTick = disableWeapon ? int.MaxValue : Server.TickCount;
weapon.Value.NextSecondaryAttackTick = disableWeapon ? int.MaxValue : Server.TickCount;
@ -227,7 +223,7 @@ namespace src.player.skills
if (playerInfo == null) return;
var weapon = pawn.WeaponServices.ActiveWeapon.Value;
if (weapon == null || !weapon.IsValid || allowedWeapons.Contains(weapon.DesignerName))
if (weapon == null || !weapon.IsValid || IsAllowed(weapon.DesignerName))
{
playerInfo.PrintHTML = null;
return;