v1.2.3.b7

## Shared Systems

* **DecoyTracker** - owner-aware decoy tracking shared by FrozenDecoy, GravityDecoy and MagneticDecoy, replacing the per-skill position dictionaries.
* **DebugCategory** - new flag enum backing the numeric `DebugMode`, with automatic migration from the old boolean value.
* **SkillsInfo.GetSkillConfig** - indexed skill config lookup replacing the linear scan used by draws and HUD rendering.
* **SkillNames** - cached enum name table, removing repeated `ToString()` calls from the draw path.
* **SkillUtils.HideCarriedEntities** - shared carried-weapon resolution for C4Camouflage, Ghost and Ninja.

## Performance

* **Skill draw** - replaced the O(n²) config lookup; rounds exceeding the perf threshold dropped from 89% to 0.3%.
* **CheckTransmit** - the weapon chain is now resolved once per hidden pawn instead of once per receiver; Ghost cost down 61%.
* **Pilot** - the exhaust trail is paused and resumed instead of being destroyed and recreated on every jump release.
* **MagneticDecoy** - config reads and the pawn list are hoisted out of the per-decoy loop.
* **FrozenDecoy** - config reads are hoisted out of the per-tick loop.
* **Debug** - the damage hook is only installed when damage logging is enabled.

## Fixes

* **PsychicDefusing** - can no longer finish a defuse and award an extra round after the round has already ended.
* **Spectator** - now picks a different enemy on each activation and no longer creates an unused camera prop every time the skill is switched off.
* **Random** - replaced the shared `Random` instance with `Random.Shared`, preventing degenerate results from concurrent use.
* **Baseball** - decoys are removed when their owner dies or loses the skill.
* **FrozenDecoy, GravityDecoy, MagneticDecoy** - decoys and their ground rings are removed when the skill is taken away.
* **ThirdEye** - fixed the camera check dereferencing a missing `CameraServices`, and the door toggle now verifies the entity class before casting.
* **Round start** - fixed `[gamerules unavailable]` appearing on the first freeze-time end after every map change.
* **Config** - added migration for keys missing from existing configuration files.

## Menu

* **WASDMenuAPI** - fixed a malformed `<font>` tag that wrapped every non-hovered menu row in an unclosed attribute.
* **WSAD menu** - the controls line is no longer dropped when the header line is hidden.
* **HtmlHudCustomisation** - line size options left empty now hide their line instead of emitting an invalid font class.

## Debug

* **DebugMode** - changed from a boolean to a number with concatenated category digits: `1` Skill, `2` Round, `3` Entity, `4` Damage. `123` enables Skill, Round and Entity. `0` disables logging. Existing `true` values migrate automatically.
* **EntityManager** - entity creation, destruction and failures are now logged under the Entity category instead of the server console.
* Startup now reports which debug categories are active.
This commit is contained in:
ByDexter 2026-08-16 16:33:09 +03:00
parent 5c65f4ac71
commit 0033d1e13d
29 changed files with 634 additions and 251 deletions

View file

@ -15,93 +15,105 @@ namespace src.player
private static readonly string debugFolder = Path.Combine(Instance.ModuleDirectory, "logs");
private static StreamWriter? _writer;
private static readonly object _writeLock = new();
private static bool damageHooked;
public static void Load()
{
sessionId = $"{DateTime.Now:yyyy-MM-dd_HH-mm-ss}";
lock (_writeLock) { _writer?.Dispose(); _writer = null; }
if (Config.LoadedConfig.DebugMode != true)
if (Config.DebugFlags == DebugCategory.None)
return;
Instance.RegisterEventHandler<EventPlayerConnectFull>((@event, info) =>
if (Config.DebugEnabled(DebugCategory.Round))
{
var player = PlayerManager.GetPlayerEvent(@event.Userid);
if (player == null || !player.IsValid) return HookResult.Continue;
WriteToDebug($"{(player.IsBot ? "Bot" : "Player")} {player.PlayerName} joined the game.");
return HookResult.Continue;
});
Instance.RegisterEventHandler<EventPlayerDisconnect>((@event, info) =>
{
var player = PlayerManager.GetPlayerEvent(@event.Userid);
if (player == null || !player.IsValid) return HookResult.Continue;
WriteToDebug($"{(player.IsBot ? "Bot" : "Player")} {player.PlayerName} disconnected.");
return HookResult.Continue;
});
Instance.RegisterEventHandler<EventRoundStart>((@event, info) =>
{
var teams = Utilities.FindAllEntitiesByDesignerName<CCSTeam>("cs_team_manager").Where(t => t != null).ToList();
var tTeam = teams.FirstOrDefault(t => t.TeamNum == (int)CsTeam.Terrorist);
var ctTeam = teams.FirstOrDefault(t => t.TeamNum == (int)CsTeam.CounterTerrorist);
WriteToDebug($"Round #{tTeam?.Score + ctTeam?.Score + 1} (CT {ctTeam?.Score} : {tTeam?.Score} TT) started.{WarmupTag()}");
return HookResult.Continue;
});
Instance.RegisterEventHandler<EventRoundFreezeEnd>((@event, info) =>
{
WriteToDebug($"Freeze time ended.{WarmupTag()}");
return HookResult.Continue;
});
Instance.RegisterEventHandler<EventRoundEnd>((@event, info) =>
{
var teams = Utilities.FindAllEntitiesByDesignerName<CCSTeam>("cs_team_manager").Where(t => t != null).ToList();
var tTeam = teams.FirstOrDefault(t => t.TeamNum == (int)CsTeam.Terrorist);
var ctTeam = teams.FirstOrDefault(t => t.TeamNum == (int)CsTeam.CounterTerrorist);
WriteToDebug($"Round #{tTeam?.Score + ctTeam?.Score} (CT {ctTeam?.Score} : {tTeam?.Score} TT) ended.{WarmupTag()}");
return HookResult.Continue;
});
Instance.RegisterEventHandler<EventPlayerDeath>((@event, info) =>
{
var victim = PlayerManager.GetPlayerEvent(@event.Userid);
var attacker = PlayerManager.GetPlayerEvent(@event.Attacker);
if (victim != null)
Instance.RegisterEventHandler<EventPlayerConnectFull>((@event, info) =>
{
if (attacker != null)
WriteToDebug($"{(victim.IsBot ? "Bot" : "Player")} {victim.PlayerName} died from {(attacker.IsBot ? "bot" : "player")} {attacker.PlayerName}.");
else
WriteToDebug($"{(victim.IsBot ? "Bot" : "Player")} {victim.PlayerName} died.");
}
return HookResult.Continue;
});
var player = PlayerManager.GetPlayerEvent(@event.Userid);
if (player == null || !player.IsValid) return HookResult.Continue;
WriteToDebug($"{(player.IsBot ? "Bot" : "Player")} {player.PlayerName} joined the game.", DebugCategory.Round);
return HookResult.Continue;
});
Instance.RegisterEventHandler<EventBombPlanted>((@event, info) =>
{
WriteToDebug($"Bomb planted.");
return HookResult.Continue;
});
Instance.RegisterEventHandler<EventPlayerDisconnect>((@event, info) =>
{
var player = PlayerManager.GetPlayerEvent(@event.Userid);
if (player == null || !player.IsValid) return HookResult.Continue;
WriteToDebug($"{(player.IsBot ? "Bot" : "Player")} {player.PlayerName} disconnected.", DebugCategory.Round);
return HookResult.Continue;
});
Instance.RegisterEventHandler<EventBombDefused>((@event, info) =>
{
WriteToDebug($"Bomb defused.");
return HookResult.Continue;
});
Instance.RegisterEventHandler<EventRoundStart>((@event, info) =>
{
var teams = Utilities.FindAllEntitiesByDesignerName<CCSTeam>("cs_team_manager").Where(t => t != null).ToList();
var tTeam = teams.FirstOrDefault(t => t.TeamNum == (int)CsTeam.Terrorist);
var ctTeam = teams.FirstOrDefault(t => t.TeamNum == (int)CsTeam.CounterTerrorist);
WriteToDebug($"Round #{tTeam?.Score + ctTeam?.Score + 1} (CT {ctTeam?.Score} : {tTeam?.Score} TT) started.{WarmupTag()}", DebugCategory.Round);
return HookResult.Continue;
});
Instance.RegisterEventHandler<EventRoundFreezeEnd>((@event, info) =>
{
WriteToDebug($"Freeze time ended.{WarmupTag()}", DebugCategory.Round);
return HookResult.Continue;
});
Instance.RegisterEventHandler<EventRoundEnd>((@event, info) =>
{
var teams = Utilities.FindAllEntitiesByDesignerName<CCSTeam>("cs_team_manager").Where(t => t != null).ToList();
var tTeam = teams.FirstOrDefault(t => t.TeamNum == (int)CsTeam.Terrorist);
var ctTeam = teams.FirstOrDefault(t => t.TeamNum == (int)CsTeam.CounterTerrorist);
WriteToDebug($"Round #{tTeam?.Score + ctTeam?.Score} (CT {ctTeam?.Score} : {tTeam?.Score} TT) ended.{WarmupTag()}", DebugCategory.Round);
return HookResult.Continue;
});
Instance.RegisterEventHandler<EventPlayerDeath>((@event, info) =>
{
var victim = PlayerManager.GetPlayerEvent(@event.Userid);
var attacker = PlayerManager.GetPlayerEvent(@event.Attacker);
if (victim != null)
{
if (attacker != null)
WriteToDebug($"{(victim.IsBot ? "Bot" : "Player")} {victim.PlayerName} died from {(attacker.IsBot ? "bot" : "player")} {attacker.PlayerName}.", DebugCategory.Round);
else
WriteToDebug($"{(victim.IsBot ? "Bot" : "Player")} {victim.PlayerName} died.", DebugCategory.Round);
}
return HookResult.Continue;
});
Instance.RegisterEventHandler<EventBombPlanted>((@event, info) =>
{
WriteToDebug($"Bomb planted.", DebugCategory.Round);
return HookResult.Continue;
});
Instance.RegisterEventHandler<EventBombDefused>((@event, info) =>
{
WriteToDebug($"Bomb defused.", DebugCategory.Round);
return HookResult.Continue;
});
}
Instance.RegisterListener<OnMapStart>((mapName) =>
{
WriteToDebug($"Map changed: {mapName}.");
});
VirtualFunctions.CBaseEntity_TakeDamageOldFunc.Hook(OnTakeDamage, HookMode.Pre);
if (Config.DebugEnabled(DebugCategory.Damage))
{
VirtualFunctions.CBaseEntity_TakeDamageOldFunc.Hook(OnTakeDamage, HookMode.Pre);
damageHooked = true;
}
}
public static void Unload()
{
try { VirtualFunctions.CBaseEntity_TakeDamageOldFunc.Unhook(OnTakeDamage, HookMode.Pre); }
catch { }
if (damageHooked)
{
try { VirtualFunctions.CBaseEntity_TakeDamageOldFunc.Unhook(OnTakeDamage, HookMode.Pre); }
catch { }
damageHooked = false;
}
lock (_writeLock) { _writer?.Dispose(); _writer = null; }
}
@ -133,20 +145,27 @@ namespace src.player
WriteToDebug($"{(victim.IsBot ? "Bot" : "Player")} {victim.PlayerName} took damage from {(attacker.IsBot ? "bot" : "player")} {attacker.PlayerName}. " +
$"[dmg={param2.Damage:0.#} hp={victimPawn.Health}/{victimPawn.MaxHealth} armor={victimPawn.ArmorValue} hitgroup={nativeHitGroup} " +
$"takes={victimPawn.TakesDamage} vskill={PlayerManager.GetPlayerByIndex(victim.Index)?.Skill} askill={playerInfo.Skill}]");
$"takes={victimPawn.TakesDamage} vskill={PlayerManager.GetPlayerByIndex(victim.Index)?.Skill} askill={playerInfo.Skill}]", DebugCategory.Damage);
return HookResult.Continue;
}
private static string WarmupTag()
{
var gameRules = Instance?.GameRules;
if (gameRules == null || gameRules.Handle == IntPtr.Zero)
{
PlayerOnTick.InitializeGameRules();
gameRules = Instance?.GameRules;
}
if (gameRules == null || gameRules.Handle == IntPtr.Zero) return " [gamerules unavailable]";
return gameRules.WarmupPeriod ? " [WARMUP]" : string.Empty;
}
public static void WriteToDebug(string message)
public static void WriteToDebug(string message, DebugCategory category = DebugCategory.Core)
{
if (Config.LoadedConfig.DebugMode != true)
if (!Config.DebugEnabled(category))
return;
lock (_writeLock)