- #### Shared Systems
- **PlayerManager.FillSkillHolders** - Skill holders are resolved from the player index instead of scanning every controller each tick, replacing the per-skill full player loop used by ten skills.
- **PlayerManager.GetControllerByPawn** - Tick-cached pawn to controller map, replacing the linear observer-target scan.
- **PlayerManager.IsServerIdle** - Tick-cached idle check shared by the debug and perf writers.
- **SkillUtils.IsFriendlyFireBlocked** - Now takes the skill, so the new per-skill `FriendlyFire` key and the server convars are resolved in one place. The convar-only overload remains for victim-side skills.
- **SkillUtils.TerminateRound** - Accepts the player who ended the round and pays out the round-end money that the engine skips on plugin-terminated rounds.
- #### Performance
- **OnTick(hud)** - Average cost down 17% against the previous build.
- **CheckTransmit** - Average cost down 25%, peak down 39%.
- **OnTick(skills)** - Peak cost down 62%.
- **Host stalls** - Real freezes measured with players connected dropped from six to two across 785 rounds.
- **DecoyRing** - Beam creation is spread across four frames instead of spawning the whole ring in one tick.
- **Fourteen skills** - `OnTick` now returns immediately when the skill holds no state.
- **Berserker, BladeMaster, BunnyHop, DemonEye, Flash, PawelJumper, Pilot, Poison, QuickShot, RadarHack, Regeneration** - Switched to the shared skill-holder list; config reads hoisted out of per-player loops.
- #### Fixes
- **GravityDecoy** - Restored a hardcoded gravity of 1.0 when leaving the field, permanently breaking Astronaut. Each pawn's own scale is now captured and restored.
- **PsychicDefusing** - The bomb location stayed unset when the skill was assigned after the plant, the stored position pointed into the entity and dangled once the bomb was killed, and setup was silently skipped when the pawn was not ready.
- **PsychicDefusing, FragileBomb** - Winning teams were paid nothing when the plugin ended the round. Winners are now paid from the matching `cash_*` convars, with the defuser bonus on top.
- **ToxicSmoke, HealingSmoke** - Smoke kept damaging and healing after its owner lost the skill, and expiry left behind smokes whose owner had already lost it. HealingSmoke never tracked an owner at all.
- **EmpGrenade** - Crosshair and radar stayed jammed forever, because `OnTick` stops being dispatched once nobody holds the skill and the jam timer never expired.
- **ThrowingKnife** - Losing the skill while the knife was thrown destroyed it without giving one back.
- **OnlyHead, ReactiveArmor** - Both ignored fall and world damage, which has no player attacker.
- **Gambler** - Refreshing consumed the one-shot instead of rolling two new skills, and the refresh row lost its colour prefix to the text-direction wrapper.
- **Berserker, Flash** - The falling-speed clamp fired while noclipping.
- **ExplodingBarrel** - The prop spawned without a model and received it a frame later; the model is now passed through `CEntityKeyValues` at spawn.
- **FastReload** - Now has a cooldown, five seconds by default, with a HUD countdown. The cooldown only starts when a magazine is actually refilled.
- **Config** - Missing-key migration now descends into nested sections, so keys under `HtmlHudCustomisation` are detected.
- #### Damage Hook
- **OnEntityTakeDamagePre / OnEntityTakeDamagePost** - Replaced the deprecated `CBaseEntity_TakeDamageOldFunc` hook across 27 files.
- **Teammate damage** - The new hook runs before the engine zeroes friendly fire, so attacker-side modifiers need explicit guards. Added to OneShot, Cutter, Assassin, Soldier, Berserker, Nemesis and Punisher.
- **FriendlyFire** - New per-skill key on twelve skills, defaulting to `true`. Setting it to `false` stops teammates being affected, the way LongKnife and KillerFlash already allow. Covers both the damage amplifiers and the explosives that previously only reduced teammate damage.
- #### Menu
- **WSADMenuVisibleItems** - New setting replacing the hardcoded three visible menu rows, clamped between one and ten.
- **WSAD menu** - The final menu row keeps its `#RRGGBB` colour prefix instead of losing it to the text-direction wrapper.
- #### Config Migration
- **skillsInfo.json** - Missing keys are written back to the file on load instead of only being defaulted in memory. Existing values are preserved.
- **playersLanguage.json** - Now grouped by language instead of one row per player, which cuts a ten thousand player file by 20%. Old files are converted on load and invalid SteamIDs are dropped.
- #### Debug
- **Perf samples** - Report `p50`, `p95` and `p99` alongside the average and maximum.
- **Per-skill sampling** - Every skill is now sampled, instead of only logging the calls that exceeded two milliseconds. The measurement was already running; only the reporting threshold discarded it.
- **Load context** - Aggregate lines carry the player, alive, bot, active-skill and round counts, so sessions can be compared under different loads.
- **ENTITIES** - Tracked entities are broken down by type.
- **STALL** - Lines carry the tick, tracked entity count and load context, which separates map-load pauses from genuine freezes.
- **DMGHOOK** - Reports how often the attacker and ability resolve on the damage hook.
- **Idle server** - Nothing is written while the server sits empty, apart from lifecycle lines and player connects and disconnects.
- #### Cleanup
- **Compiler warnings** - All twelve resolved; the build is clean.
- **SkillUtils.SetPlayerCollisions** - Removed, its body started with an unconditional `return`.
- **Damage hook aliases** - Dropped the `param` and `param2` locals the migration left behind across 27 files.
298 lines
No EOL
12 KiB
C#
298 lines
No EOL
12 KiB
C#
using CounterStrikeSharp.API;
|
|
using CounterStrikeSharp.API.Core;
|
|
using CounterStrikeSharp.API.Modules.Utils;
|
|
using static src.jRandomSkills;
|
|
using System.Collections.Concurrent;
|
|
using src.utils;
|
|
|
|
namespace src.player.skills
|
|
{
|
|
public class Pilot : ISkill
|
|
{
|
|
private const Skills skillName = Skills.Pilot;
|
|
private static readonly ConcurrentDictionary<uint, Pilot_PlayerInfo> PlayerPilotInfo = [];
|
|
private static readonly List<CCSPlayerController> holderBuffer = [];
|
|
|
|
public static void LoadSkill()
|
|
{
|
|
SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue<string>(skillName, "color"));
|
|
Instance.AddToManifest(SkillsInfo.GetValue<string>(skillName, "particleName"));
|
|
}
|
|
|
|
public static void NewRound()
|
|
{
|
|
foreach (var pilotInfo in PlayerPilotInfo.Values)
|
|
ClearTrail(pilotInfo);
|
|
|
|
PlayerPilotInfo.Clear();
|
|
}
|
|
|
|
public static void PlayerDeath(EventPlayerDeath @event)
|
|
{
|
|
var player = PlayerManager.GetPlayerEvent(@event.Userid);
|
|
if (player == null || !player.IsValid) return;
|
|
|
|
if (PlayerPilotInfo.TryGetValue(player.Index, out var pilotInfo))
|
|
{
|
|
ClearTrail(pilotInfo);
|
|
pilotInfo.IsFlying = false;
|
|
}
|
|
}
|
|
|
|
public static void OnTick()
|
|
{
|
|
PlayerManager.FillSkillHolders(skillName, holderBuffer);
|
|
if (holderBuffer.Count == 0) return;
|
|
|
|
foreach (var player in holderBuffer)
|
|
HandlePilot(player);
|
|
}
|
|
|
|
public static void EnableSkill(CCSPlayerController player)
|
|
{
|
|
PlayerPilotInfo.TryAdd(player.Index, new Pilot_PlayerInfo { SteamID = player.Index });
|
|
}
|
|
|
|
public static void DisableSkill(CCSPlayerController player)
|
|
{
|
|
if (PlayerPilotInfo.TryRemove(player.Index, out var pilotInfo))
|
|
ClearTrail(pilotInfo);
|
|
|
|
SkillUtils.ResetPrintHTML(player);
|
|
}
|
|
|
|
private static void HandlePilot(CCSPlayerController player)
|
|
{
|
|
var playerPawn = player.PlayerPawn.Value;
|
|
if (playerPawn == null || !playerPawn.IsValid) return;
|
|
|
|
if (!PlayerPilotInfo.TryGetValue(player.Index, out var pilotInfo)) return;
|
|
|
|
var flags = (PlayerFlags)playerPawn.Flags;
|
|
var buttons = player.Buttons;
|
|
float currentTime = Server.CurrentTime;
|
|
|
|
bool isJumpDown = (buttons & PlayerButtons.Jump) != 0
|
|
|| (playerPawn.MovementServices?.Buttons?.ButtonStates[0] & (ulong)PlayerButtons.Jump) != 0;
|
|
bool wasJumpDown = (pilotInfo.LastButtons & PlayerButtons.Jump) != 0;
|
|
bool jumpPressed = (isJumpDown && !wasJumpDown)
|
|
|| (playerPawn.MovementServices?.QueuedButtonChangeMask & (ulong)PlayerButtons.Jump) != 0;
|
|
bool isOnGround = (flags & PlayerFlags.FL_ONGROUND) != 0;
|
|
|
|
if (jumpPressed && !isOnGround)
|
|
{
|
|
if (pilotInfo.JumpCount == 0)
|
|
pilotInfo.JumpCount++;
|
|
else
|
|
{
|
|
pilotInfo.JumpCount++;
|
|
pilotInfo.LastJumpTime = currentTime;
|
|
|
|
if (pilotInfo.JumpCount >= 2)
|
|
pilotInfo.IsFlying = true;
|
|
}
|
|
}
|
|
else if (!isJumpDown && currentTime - pilotInfo.LastJumpTime > .1f)
|
|
pilotInfo.IsFlying = false;
|
|
|
|
if (isOnGround)
|
|
{
|
|
pilotInfo.IsFlying = false;
|
|
if (isOnGround) pilotInfo.JumpCount = 0;
|
|
}
|
|
|
|
bool inUse = pilotInfo.IsFlying && pilotInfo.Fuel > 0 && !isOnGround;
|
|
|
|
float maximumFuel = SkillsInfo.GetValue<float>(skillName, "maximumFuel");
|
|
float consumption = SkillsInfo.GetValue<float>(skillName, "fuelConsumption");
|
|
float refuelling = SkillsInfo.GetValue<float>(skillName, "refuelling");
|
|
|
|
pilotInfo.Fuel = Math.Clamp(
|
|
pilotInfo.Fuel + (inUse ? -consumption : refuelling),
|
|
0,
|
|
maximumFuel
|
|
);
|
|
|
|
if (inUse)
|
|
{
|
|
ApplyPilotEffect(player);
|
|
StartTrail(player, playerPawn, pilotInfo);
|
|
}
|
|
else
|
|
{
|
|
PauseTrail(pilotInfo);
|
|
|
|
if (isOnGround)
|
|
ClearTrail(pilotInfo);
|
|
|
|
if (pilotInfo.Fuel <= 0)
|
|
pilotInfo.IsFlying = false;
|
|
}
|
|
|
|
pilotInfo.LastButtons = buttons;
|
|
UpdateHUD(player, pilotInfo);
|
|
}
|
|
|
|
private static void UpdateHUD(CCSPlayerController player, Pilot_PlayerInfo pilotInfo)
|
|
{
|
|
if (pilotInfo == null) return;
|
|
var playerInfo = PlayerManager.GetPlayerByIndex(player!.Index);
|
|
if (playerInfo == null) return;
|
|
|
|
var maximumFuel = SkillsInfo.GetValue<float>(skillName, "maximumFuel");
|
|
if (pilotInfo.Fuel == maximumFuel && playerInfo.SkillDescriptionHudExpired >= DateTime.Now)
|
|
{
|
|
playerInfo.PrintHTML = null;
|
|
return;
|
|
}
|
|
|
|
var buttons = player.Buttons;
|
|
float fuelPercentage = maximumFuel;
|
|
|
|
string fuelColor = GetFuelColor(pilotInfo.Fuel);
|
|
playerInfo.PrintHTML = $"{player.GetTranslation("pilot_hud_info")}: <font color='{fuelColor}'>{(pilotInfo.Fuel / maximumFuel) * 100:F0}%</font>";
|
|
}
|
|
|
|
private static string GetFuelColor(float fuelPercentage)
|
|
{
|
|
var maximumFuel = SkillsInfo.GetValue<float>(skillName, "maximumFuel");
|
|
if (fuelPercentage > (maximumFuel / 2f)) return "#00FF00";
|
|
if (fuelPercentage > (maximumFuel / 4f)) return "#FFFF00";
|
|
return "#FF0000";
|
|
}
|
|
|
|
private static void StartTrail(CCSPlayerController player, CCSPlayerPawn pawn, Pilot_PlayerInfo pilotInfo)
|
|
{
|
|
if (pilotInfo.TrailIndex != null)
|
|
{
|
|
var existing = Utilities.GetEntityFromIndex<CParticleSystem>((int)pilotInfo.TrailIndex.Value);
|
|
if (existing != null && existing.IsValid)
|
|
{
|
|
ResumeTrail(pilotInfo);
|
|
return;
|
|
}
|
|
|
|
ClearTrail(pilotInfo);
|
|
}
|
|
|
|
if (pawn.AbsOrigin == null) return;
|
|
|
|
var particle = EntityManager.CreateTrackedParticleSystem(player.Index, SkillsInfo.GetValue<string>(skillName, "particleName"));
|
|
if (particle == null || !particle.IsValid) return;
|
|
|
|
float offset = SkillsInfo.GetValue<float>(skillName, "particleOffset");
|
|
particle.Teleport(new Vector(pawn.AbsOrigin.X, pawn.AbsOrigin.Y, pawn.AbsOrigin.Z + offset));
|
|
particle.AcceptInput("SetParent", pawn, particle, "!activator");
|
|
particle.AcceptInput("Start");
|
|
|
|
pilotInfo.TrailIndex = particle.Index;
|
|
pilotInfo.TrailActive = true;
|
|
}
|
|
|
|
private static void ResumeTrail(Pilot_PlayerInfo pilotInfo)
|
|
{
|
|
if (pilotInfo.TrailActive || pilotInfo.TrailIndex == null) return;
|
|
|
|
var particle = Utilities.GetEntityFromIndex<CParticleSystem>((int)pilotInfo.TrailIndex.Value);
|
|
if (particle != null && particle.IsValid)
|
|
particle.AcceptInput("Start");
|
|
|
|
pilotInfo.TrailActive = true;
|
|
}
|
|
|
|
private static void PauseTrail(Pilot_PlayerInfo pilotInfo)
|
|
{
|
|
if (!pilotInfo.TrailActive || pilotInfo.TrailIndex == null) return;
|
|
|
|
var particle = Utilities.GetEntityFromIndex<CParticleSystem>((int)pilotInfo.TrailIndex.Value);
|
|
if (particle != null && particle.IsValid)
|
|
particle.AcceptInput("Stop");
|
|
|
|
pilotInfo.TrailActive = false;
|
|
}
|
|
|
|
private static void StopTrail(Pilot_PlayerInfo pilotInfo)
|
|
{
|
|
if (pilotInfo.TrailIndex == null) return;
|
|
|
|
uint index = pilotInfo.TrailIndex.Value;
|
|
pilotInfo.TrailIndex = null;
|
|
pilotInfo.TrailActive = false;
|
|
|
|
var particle = Utilities.GetEntityFromIndex<CParticleSystem>((int)index);
|
|
if (particle != null && particle.IsValid)
|
|
{
|
|
particle.AcceptInput("Stop");
|
|
particle.AcceptInput("DestroyImmediately");
|
|
}
|
|
|
|
EntityManager.DestroyEntity(index);
|
|
|
|
if (EntityManager.SuppressKills) return;
|
|
|
|
Server.NextFrame(() =>
|
|
{
|
|
var leftover = Utilities.GetEntityFromIndex<CParticleSystem>((int)index);
|
|
if (leftover != null && leftover.IsValid)
|
|
leftover.Remove();
|
|
});
|
|
}
|
|
|
|
private static void ClearTrail(Pilot_PlayerInfo pilotInfo)
|
|
{
|
|
StopTrail(pilotInfo);
|
|
|
|
foreach (var leftover in EntityManager.GetPlayerEntities((uint)pilotInfo.SteamID, "particle_system"))
|
|
EntityManager.DestroyEntity(leftover);
|
|
}
|
|
|
|
private static void ApplyPilotEffect(CCSPlayerController player)
|
|
{
|
|
var playerPawn = player.PlayerPawn.Value;
|
|
if (playerPawn?.CBodyComponent == null) return;
|
|
|
|
QAngle eye_angle = playerPawn.EyeAngles;
|
|
double pitch = (Math.PI / 180) * eye_angle.X;
|
|
double yaw = (Math.PI / 180) * eye_angle.Y;
|
|
Vector eye_vector = new((float)(Math.Cos(yaw) * Math.Cos(pitch)), (float)(Math.Sin(yaw) * Math.Cos(pitch)), (float)(-Math.Sin(pitch)));
|
|
|
|
Vector currentVelocity = playerPawn.AbsVelocity;
|
|
|
|
Vector jetpackVelocity = new(
|
|
eye_vector.X * 5.0f,
|
|
eye_vector.Y * 5.0f,
|
|
0.80f * 15.0f
|
|
);
|
|
|
|
float newVelocityX = currentVelocity.X + jetpackVelocity.X;
|
|
float newVelocityY = currentVelocity.Y + jetpackVelocity.Y;
|
|
float newVelocityZ = currentVelocity.Z + jetpackVelocity.Z;
|
|
|
|
playerPawn.AbsVelocity.X = newVelocityX;
|
|
playerPawn.AbsVelocity.Y = newVelocityY;
|
|
playerPawn.AbsVelocity.Z = newVelocityZ;
|
|
}
|
|
|
|
|
|
public class Pilot_PlayerInfo
|
|
{
|
|
public required ulong SteamID { get; set; }
|
|
public float Fuel { get; set; } = SkillsInfo.GetValue<float>(skillName, "maximumFuel");
|
|
public PlayerButtons LastButtons { get; set; } = 0;
|
|
public int JumpCount { get; set; } = 0;
|
|
public float LastJumpTime { get; set; } = 0;
|
|
public bool IsFlying { get; set; } = false;
|
|
public uint? TrailIndex { get; set; }
|
|
public bool TrailActive { get; set; }
|
|
}
|
|
|
|
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#1466F5", 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 maximumFuel = 150f, float fuelConsumption = .64f, float refuelling = .1f, string particleName = "particles/inferno_fx/incgrenade_thrown_trail.vpcf", float particleOffset = 10f) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
|
|
{
|
|
public string ParticleName { get; set; } = particleName;
|
|
public float ParticleOffset { get; set; } = particleOffset;
|
|
public float MaximumFuel { get; set; } = maximumFuel;
|
|
public float FuelConsumption { get; set; } = fuelConsumption;
|
|
public float Refuelling { get; set; } = refuelling;
|
|
}
|
|
}
|
|
} |