Hygiene B4d: PlayerResolve dedup + explicit Temp-ECB Dispose tail

- New Server/PlayerResolve.TryResolve single-sources the RPC
  SourceConnection -> NetworkId -> conn->player map resolve (3 sites:
  ClassSelectReceive, PrepPurchase, DebugCommandReceive); EntityManager
  reads keep it source-gen-safe from Bursted receivers.
- ecb.Dispose() after Playback in 9 Temp-ECB systems (explicit-lifetime
  hygiene).
- The TuningConfig.GetOrDefault(ref state) variant of this tail was
  REVERTED: state.GetEntityQuery in OnUpdate trips the Entities
  "creates a query during OnUpdate" diagnostic per system per world
  (caught in Play smoke) - the SystemAPI.TryGetSingleton idiom is
  already source-gen-optimal, confirming the original B4 deferral.

Verified: 466/466 EditMode green on the final tree, console clean,
Play smoke 0 errors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-09 12:39:55 -07:00
parent 21a4c71413
commit 813c829420
14 changed files with 41 additions and 6 deletions
@@ -54,6 +54,7 @@ namespace ProjectM.Client
}
ecb.Playback(state.EntityManager);
ecb.Dispose();
}
}
}
@@ -129,6 +129,7 @@ namespace ProjectM.Server
}
ecb.Playback(state.EntityManager);
ecb.Dispose();
}
}
}
@@ -115,6 +115,7 @@ namespace ProjectM.Server
}
ecb.Playback(state.EntityManager);
ecb.Dispose();
occupied.Dispose();
}
}
@@ -48,8 +48,7 @@ namespace ProjectM.Server
if (!accept) continue;
var conn = receive.ValueRO.SourceConnection;
if (!SystemAPI.HasComponent<NetworkId>(conn)
|| !playerByConn.TryGetValue(SystemAPI.GetComponent<NetworkId>(conn).Value, out var player))
if (!PlayerResolve.TryResolve(ref state, playerByConn, conn, out var player))
continue;
if (!SystemAPI.HasComponent<AbilityRef>(player)) continue;
@@ -46,8 +46,7 @@ namespace ProjectM.Server
if (!accept) continue;
var conn = receive.ValueRO.SourceConnection;
if (!SystemAPI.HasComponent<NetworkId>(conn)
|| !playerByConn.TryGetValue(SystemAPI.GetComponent<NetworkId>(conn).Value, out var buyer))
if (!PlayerResolve.TryResolve(ref state, playerByConn, conn, out var buyer))
continue;
if (!PrepCatalog.TryGet(req.ValueRO.OptionId, out var row)) continue; // unknown id -> drop
@@ -115,6 +115,7 @@ namespace ProjectM.Server
}
ecb.Playback(state.EntityManager);
ecb.Dispose();
}
}
}
@@ -50,8 +50,7 @@ namespace ProjectM.Server
Entity sender = Entity.Null;
var connEntity = receive.ValueRO.SourceConnection;
if (SystemAPI.HasComponent<NetworkId>(connEntity))
playerByConn.TryGetValue(SystemAPI.GetComponent<NetworkId>(connEntity).Value, out sender);
PlayerResolve.TryResolve(ref state, playerByConn, connEntity, out sender);
switch (cmd.Op)
{
@@ -141,6 +141,7 @@ namespace ProjectM.Server
}
ecb.Playback(state.EntityManager);
ecb.Dispose();
}
}
}
@@ -55,6 +55,7 @@ namespace ProjectM.Server
ecb.DestroyEntity(spawnerEntity);
ecb.Playback(state.EntityManager);
ecb.Dispose();
}
}
}
@@ -49,6 +49,7 @@ namespace ProjectM.Server
}
ecb.Playback(state.EntityManager);
ecb.Dispose();
}
}
}
@@ -0,0 +1,27 @@
using Unity.Collections;
using Unity.Entities;
using Unity.NetCode;
namespace ProjectM.Server
{
/// <summary>
/// Shared server-side RPC sender resolution: a request's <c>ReceiveRpcCommandRequest.SourceConnection</c> →
/// its <see cref="NetworkId"/> → the conn→player map that every RPC-receive system builds (the
/// AbilityUpgradeSystem / ReadyToggle idiom). The MAP BUILD stays per-caller — each system's player query
/// carries different extra required components (InventorySlot / EquipmentSlot / StatModifier / BoonOffer /
/// PlayerReady / …) — but the RESOLVE step is byte-for-byte identical across sites, so it lives here once.
/// Burst-safe (plain <see cref="EntityManager"/> reads; called from <c>[BurstCompile]</c> receivers).
/// </summary>
public static class PlayerResolve
{
/// <summary>True (with <paramref name="player"/> set) iff <paramref name="conn"/> has a
/// <see cref="NetworkId"/> present in <paramref name="map"/>. Behaviour-identical to the inline
/// <c>HasComponent&lt;NetworkId&gt;(conn) &amp;&amp; map.TryGetValue(GetComponent&lt;NetworkId&gt;(conn).Value, out player)</c>.</summary>
public static bool TryResolve(ref SystemState state, NativeHashMap<int, Entity> map, Entity conn, out Entity player)
{
player = Entity.Null;
return state.EntityManager.HasComponent<NetworkId>(conn)
&& map.TryGetValue(state.EntityManager.GetComponentData<NetworkId>(conn).Value, out player);
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 5e602229d090fee489d063aebece5da2
@@ -154,6 +154,7 @@ namespace ProjectM.Server
ecb.DestroyEntity(spawnerEntity);
ecb.Playback(state.EntityManager);
ecb.Dispose();
}
}
}
@@ -90,6 +90,7 @@ namespace ProjectM.Server
ecb.DestroyEntity(requestEntity);
}
ecb.Playback(state.EntityManager);
ecb.Dispose();
regionByConn.Dispose();
}
}