Files
Project-M/Assets/_Project/Scripts/Server/HomeBase/SharedStorageSpawnSystem.cs
T
kronic 813c829420 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>
2026-07-09 12:39:55 -07:00

62 lines
2.8 KiB
C#

using ProjectM.Simulation;
using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Transforms;
namespace ProjectM.Server
{
/// <summary>
/// Server-only, one-shot spawner for the shared home-base storage container (mirrors
/// UpgradePickupSpawnSystem). On its first update it reads the baked <see cref="StorageSpawner"/>
/// singleton and the <see cref="BaseAnchor"/>, instantiates the container ghost at the cell center
/// (<see cref="BaseGridMath.CellToWorld"/>), then destroys the spawner singleton so the system idles
/// (spawned exactly once). Runs in the default SimulationSystemGroup (NOT the prediction loop); the
/// container replicates to clients as an ownerless interpolated ghost. The container is intentionally
/// NOT linked to any connection's LinkedEntityGroup, so it persists across player disconnects.
/// </summary>
[BurstCompile]
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
public partial struct SharedStorageSpawnSystem : ISystem
{
[BurstCompile]
public void OnCreate(ref SystemState state)
{
state.RequireForUpdate<StorageSpawner>();
state.RequireForUpdate<BaseAnchor>();
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
var spawnerEntity = SystemAPI.GetSingletonEntity<StorageSpawner>();
var spawner = SystemAPI.GetComponent<StorageSpawner>(spawnerEntity);
var anchor = SystemAPI.GetSingleton<BaseAnchor>();
var ecb = new EntityCommandBuffer(Allocator.Temp);
if (spawner.Prefab != Entity.Null)
{
var container = ecb.Instantiate(spawner.Prefab);
var position = BaseGridMath.CellToWorld(anchor, spawner.Cell);
// Phase 0: the grid Y (GridOrigin.y=1) is the CC capsule-CENTER plane; center-pivot structure
// meshes look grounded there, but the crate's pivot is at its base -> it floated 1 u. Sit it on
// the terrain surface instead.
position.y = 0f;
// Preserve the prefab's baked scale/rotation (FromPosition would reset Scale to 1).
var xform = SystemAPI.GetComponent<LocalTransform>(spawner.Prefab);
xform.Position = position;
ecb.SetComponent(container, xform);
// M6: scope the shared storage to the base region for ghost relevancy.
ecb.AddComponent(container, new RegionTag { Region = RegionId.Base });
}
// One-shot: remove the spawner so RequireForUpdate fails and the system idles.
ecb.DestroyEntity(spawnerEntity);
ecb.Playback(state.EntityManager);
ecb.Dispose();
}
}
}