using ProjectM.Simulation; using Unity.Collections; using Unity.Entities; using Unity.NetCode; namespace ProjectM.Server { /// /// Server receiver for — the player picks their frame at base. Honored ONLY in /// Staging (frame = a between-runs choice; mid-run it would desync the fight). Resolves sender → player (the /// MetaSpend/ReadyToggle idiom), then applies the FULL in-place swap via (class seeds + /// permanent-meta re-sync), writes FrameId, re-seeds the 4-socket Spark loadout, and calls /// . Plain server group, before RunDirectorSystem (the receiver convention); /// requests are ALWAYS destroyed. NOT Burst-compiled (a cross-assembly blob+buffer helper on a low-frequency RPC). /// [WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)] [UpdateInGroup(typeof(SimulationSystemGroup))] public partial struct ClassSelectReceiveSystem : ISystem { public void OnCreate(ref SystemState state) { var b = new EntityQueryBuilder(Allocator.Temp).WithAll(); state.RequireForUpdate(state.GetEntityQuery(b)); } public void OnUpdate(ref SystemState state) { // 2026-08-07 audit purge: this used to accept a frame swap only during RunInfo Lifecycle==Staging // (the base-phase gate). With the run FSM gone the gym accepts a swap at any time. const bool accept = true; var playerByConn = new NativeHashMap(8, Allocator.Temp); foreach (var (owner, e) in SystemAPI.Query>().WithAll().WithEntityAccess()) playerByConn[owner.ValueRO.NetworkId] = e; // 2026-08-07 audit purge: the permanent-meta re-sync (MetaUpgradeCatalog + MetaTierState) went // with the meta shop; a frame swap now re-seeds only the frame stat band. bool haveDb = SystemAPI.TryGetSingleton(out var abilityDb); var ecb = new EntityCommandBuffer(Allocator.Temp); foreach (var (receive, req, reqEntity) in SystemAPI.Query, RefRO>().WithEntityAccess()) { ecb.DestroyEntity(reqEntity); // ALWAYS consumed if (!accept) continue; var conn = receive.ValueRO.SourceConnection; if (!PlayerResolve.TryResolve(ref state, playerByConn, conn, out var player)) continue; if (!SystemAPI.HasBuffer(player)) continue; var mods = SystemAPI.GetBuffer(player); ClassSwapUtil.Apply(req.ValueRO.ClassId, mods, out byte newClass); if (SystemAPI.HasComponent(player)) SystemAPI.SetComponent(player, new FrameId { Value = newClass }); // Re-seed the 4-socket Spark loadout for the new frame + clear its cooldowns (fires now). ClassTraits.FrameLoadout(newClass, out byte f0, out byte f1, out byte f2, out byte f3); var sockets = SystemAPI.GetBuffer(player); sockets.Clear(); sockets.Add(new AbilitySocket { SparkId = f0 }); sockets.Add(new AbilitySocket { SparkId = f1 }); sockets.Add(new AbilitySocket { SparkId = f2 }); sockets.Add(new AbilitySocket { SparkId = f3 }); if (SystemAPI.HasComponent(player)) SystemAPI.SetComponent(player, default(SocketCooldown)); // 0 = ready: the swapped kit fires now if (SystemAPI.HasComponent(player)) SystemAPI.SetComponent(player, default(ConeContactPending)); // 07-21 G6: socket identity changed — drop any armed slam (parity with the dev SetClass op) if (haveDb && SystemAPI.HasComponent(player) && SystemAPI.HasComponent(player)) { byte charId = SystemAPI.GetComponent(player).Id; if (abilityDb.Value.Value.TryGetCharacter(charId, out var baseChar)) { var hp = SystemAPI.GetComponent(player); ClassSwapUtil.HealClamp(ref hp, baseChar.MaxHealth, mods); SystemAPI.SetComponent(player, hp); } } } ecb.Playback(state.EntityManager); ecb.Dispose(); playerByConn.Dispose(); } } }