Files
Project-M/Assets/_Project/Scripts/Server/Debug/DevTelemetrySystem.cs
T
kronic 3836e9c842 Phase 1 B3: enemies die with a corpse window instead of popping out of existence
Server: HealthApplyDamageSystem marks EnemyTag Dying{UntilTick} (TickUtil.
NonZero, ~54 ticks) on the lethal 0-crossing instead of instant destroy,
zeroes every live cue (replicated AttackWindup, LungeState + IsLunging bit,
KnockbackState), destroys on expiry; plain-world tests keep instant destroy
(no NetworkTime) so the suite's assertions stay meaningful.

Every consumer now ignores corpses (all confirmed entity-count/unfiltered by
the design review): EnemyAISystem all 4 passes, BossAISystem brain + summon
cap, RoomEnemyDirector room-clear + MaxAlive fit, WaveSystem cap + cleared,
CyclePhaseSystem breach wipe + DefendCleared, ThreatDirector timeout cull,
TurretFire targeting, CoreDamage drain, ProjectileDamage snapshot (corpses
are not shields), AbilityFire auto-aim candidates, debug cull + telemetry.
Wipe passes skip Dying to avoid cross-ECB double-destroys.

Client: EnemyAnimationDriveSystem finally drives the controller's IsDead
param (Health.Current<=0 is the replicated death read; corpse locomotion
zeroed); CombatFeedbackSystem moves the kill CRUNCH to the 0-crossing (the
prune-edge timing would land it ~1 s late) - the prune keeps a small
dissolve puff for corpses and the legacy full read for alive-vanish culls.

456/456 EditMode.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 18:34:12 -07:00

70 lines
3.0 KiB
C#

#if UNITY_EDITOR
using ProjectM.Simulation;
using Unity.Collections;
using Unity.Entities;
using Unity.NetCode;
namespace ProjectM.Server
{
/// <summary>
/// MC-0 — EDITOR-ONLY server telemetry sampler/sender. Ensures the <see cref="DevTelemetry"/> singleton,
/// samples live-enemy-count + the server tick each tick, and every <see cref="ReportPeriodTicks"/> ships a
/// <see cref="DebugTelemetryReport"/> snapshot to every connection (so the dev overlay shows live fun-gate
/// counters over a real connection too). Combat systems increment the real counters at the stamp sites (MC-1+).
/// Plain server <see cref="SimulationSystemGroup"/> (NOT the predicted loop); non-Burst (managed-simple,
/// editor-only). Stripped from builds; the wire TYPE <see cref="DebugTelemetryReport"/> is unconditional.
/// </summary>
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
[UpdateInGroup(typeof(SimulationSystemGroup))]
public partial struct DevTelemetrySystem : ISystem
{
const uint ReportPeriodTicks = 15;
EntityQuery m_Husks;
public void OnCreate(ref SystemState state)
{
state.RequireForUpdate<NetworkTime>();
m_Husks = state.GetEntityQuery(ComponentType.ReadOnly<EnemyTag>(), ComponentType.Exclude<Dying>()); // telemetry counts LIVING (B3)
if (state.GetEntityQuery(ComponentType.ReadWrite<DevTelemetry>()).IsEmpty)
state.EntityManager.CreateEntity(typeof(DevTelemetry));
}
public void OnUpdate(ref SystemState state)
{
var serverTick = SystemAPI.GetSingleton<NetworkTime>().ServerTick;
if (!serverTick.IsValid)
return;
uint now = serverTick.TickIndexForValidTick;
var telem = SystemAPI.GetSingletonRW<DevTelemetry>();
telem.ValueRW.LiveEnemyCount = (uint)m_Husks.CalculateEntityCount();
telem.ValueRW.LastSampleTick = now;
if (now == 0 || (now % ReportPeriodTicks) != 0)
return;
var t = telem.ValueRO;
var report = new DebugTelemetryReport
{
DashIFrameNegatedHits = t.DashIFrameNegatedHits,
DashesWasted = t.DashesWasted,
ChargerWhiffWindowsOpened = t.ChargerWhiffWindowsOpened,
ChargerWhiffPunishesLanded = t.ChargerWhiffPunishesLanded,
LiveEnemyCount = t.LiveEnemyCount,
LastSampleTick = t.LastSampleTick,
};
var ecb = new EntityCommandBuffer(Allocator.Temp);
foreach (var (netId, connEnt) in SystemAPI.Query<RefRO<NetworkId>>().WithEntityAccess())
{
var req = ecb.CreateEntity();
ecb.AddComponent(req, report);
ecb.AddComponent(req, new SendRpcCommandRequest { TargetConnection = connEnt });
}
ecb.Playback(state.EntityManager);
ecb.Dispose();
}
}
}
#endif