using ProjectM.Simulation; using Unity.Burst; using Unity.Collections; using Unity.Entities; using Unity.NetCode; using Unity.Transforms; namespace ProjectM.Server { /// /// Server-authoritative player spawn. On each received : mark the /// source connection in-game, instantiate the player ghost from the baked /// , stamp with the connection's /// , place it at the spawn point, and link it to the connection's /// LinkedEntityGroup so it auto-despawns on disconnect. Mirrors the netcode "networked-cube" /// ModifiedGoInGameServer sample. All structural changes are batched through an /// . /// [BurstCompile] [WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)] public partial struct GoInGameServerSystem : ISystem { [BurstCompile] public void OnCreate(ref SystemState state) { state.RequireForUpdate(); var builder = new EntityQueryBuilder(Allocator.Temp) .WithAll(); state.RequireForUpdate(state.GetEntityQuery(builder)); } [BurstCompile] public void OnUpdate(ref SystemState state) { // Step 12a availability guard (review N2/L1-6): the born-correct meta seeding below needs the baked // catalog + the live director's tier record. Both are per-tick-uniform, so guard ONCE at the TOP, // BEFORE the ECB exists — a per-request continue would already have marked the connection in-game. // Nothing is consumed; RequireForUpdate re-passes and the request retries next tick (a ≤1-tick window // in practice — the director spawns at subscene-stream, before any GoInGame round-trip). // GYM (LANTERN A1): a fresh gym subscene has no CycleDirector, so the base meta-catalog guard would // block spawns forever. GymTag switches to the clean gym path (no meta seeding; a default Spark socket // loadout below instead). The class seeds still apply (harmless: AbilityFireSystem reads sockets). bool isGym = SystemAPI.HasSingleton(); // 2026-08-07 audit purge: spawning used to block until the MetaUpgradeCatalog + MetaTierState buffer // were present (the audit's M12 — a missing catalog stranded the GoInGame RPC and no player ever // spawned). Both are deleted, so that whole gate and its one-shot warning are gone with them. var spawner = SystemAPI.GetSingleton(); // M5 home base: re-root the spawn ring on the baked BaseAnchor when present; fall back // to the spawner's SpawnPoint if the base subscene hasn't streamed in yet. var center = spawner.SpawnPoint; if (SystemAPI.TryGetSingleton(out var baseAnchor)) center = BaseGridMath.PlotCenter(baseAnchor); var ecb = new EntityCommandBuffer(Allocator.Temp); foreach (var (receive, goReq, requestEntity) in SystemAPI.Query, RefRO>().WithEntityAccess()) { var connection = receive.ValueRO.SourceConnection; ecb.AddComponent(connection); var networkId = SystemAPI.GetComponent(connection); var player = ecb.Instantiate(spawner.PlayerPrefab); ecb.SetComponent(player, LocalTransform.FromPosition(center + PlayerSpawnMath.SpawnOffset(networkId.Value, spawner.SpawnRingRadius, spawner.RingSlots))); ecb.SetComponent(player, new GhostOwner { NetworkId = networkId.Value }); // Tag the player into the base region (M6 region/relevancy split). ecb.AddComponent(player, new RegionTag { Region = RegionId.Base }); // Slice 2 -> LANTERN: seed the chosen frame on the just-instantiated player. The 4-socket Spark // loadout IS the ability model (the legacy single-AbilityRef path is deleted); the DRG-asymmetry // traits ride permanent StatModifiers (CharacterStatsRef stays Default -> deltas replicate via the // OwnerSendType.All buffer). 0 -> Warrior/Bathynaut. byte classId = ClassTraits.Normalize(goReq.ValueRO.ClassId); ClassTraits.AppendSeeds(classId, player, ecb); // Expedition redesign: the server-only class anchor the meta systems key on (born-correct meta // seeding at Step 12a + per-class spend at Step 13 resolve the tier record through this). // 2026-08-07 audit purge: PlayerClass was a second, server-only copy of the same byte FrameId // already replicates (audit finding M5). It existed so the meta shop could key on it; the meta // shop is gone, so FrameId is now the single frame identity. ecb.AddComponent(player, new FrameId { Value = classId }); // Add (not Set): baked on the real player; absent on the minimal test prefab // replicated frame/class signal // Per-frame default Spark loadout on keys 1-4 (UNCONDITIONAL since the legacy path died — without // this a non-gym spawn would have four empty sockets and no abilities at all). ClassTraits.FrameLoadout(classId, out byte f0, out byte f1, out byte f2, out byte f3); var sockets = ecb.SetBuffer(player); sockets.Add(new AbilitySocket { SparkId = f0 }); sockets.Add(new AbilitySocket { SparkId = f1 }); sockets.Add(new AbilitySocket { SparkId = f2 }); sockets.Add(new AbilitySocket { SparkId = f3 }); // 2026-08-07 audit purge: born-correct PERMANENT meta seeding replayed each frame's persisted // upgrade tiers as meta-band StatModifiers. The meta shop is deleted, so a spawn now carries only // the frame's own stat band (ClassTraits.AppendSeeds above). // Auto-despawn the player when its owning connection is removed. ecb.AppendToBuffer(connection, new LinkedEntityGroup { Value = player }); ecb.DestroyEntity(requestEntity); } ecb.Playback(state.EntityManager); ecb.Dispose(); } } }