#if UNITY_EDITOR using ProjectM.Simulation; using Unity.Collections; using Unity.Entities; using Unity.Mathematics; using Unity.NetCode; using Unity.Transforms; namespace ProjectM.Server { /// /// EDITOR-ONLY server receiver for dev-tool RPCs (from the DebugOverlay or /// execute_code). Applies authoritative effects so the dev buttons exercise the REAL server paths and work /// over a live connection too: force/end sieges, grant resources/upgrades, teleport, god-mode, heal/kill, /// advance the goal. Sender-targeted ops resolve the player via SourceConnection -> NetworkId -> GhostOwner /// (the RegionTransitSystem pattern). Plain server SimulationSystemGroup (NOT the predicted loop). Reuses /// StorageMath / StatModifier / RegionMath + the wave/cycle singletons. The whole system is #if UNITY_EDITOR /// (stripped from builds); the wire TYPE () is unconditional so the RPC /// collection hash matches across peers. Non-Burst (managed-simple, editor-only) — perf is irrelevant. /// [WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)] [UpdateInGroup(typeof(SimulationSystemGroup))] public partial struct DebugCommandReceiveSystem : ISystem { EntityQuery m_Husks; public void OnCreate(ref SystemState state) { m_Husks = state.GetEntityQuery(ComponentType.ReadOnly()); var builder = new EntityQueryBuilder(Allocator.Temp) .WithAll(); state.RequireForUpdate(state.GetEntityQuery(builder)); } public void OnUpdate(ref SystemState state) { var ecb = new EntityCommandBuffer(Allocator.Temp); // Connection NetworkId -> player entity (for sender-targeted ops). var playerByConn = new NativeHashMap(8, Allocator.Temp); foreach (var (owner, e) in SystemAPI.Query>().WithAll().WithEntityAccess()) playerByConn[owner.ValueRO.NetworkId] = e; bool haveCycle = SystemAPI.TryGetSingletonEntity(out var cycleEntity); foreach (var (request, receive, reqEntity) in SystemAPI.Query, RefRO>().WithEntityAccess()) { var cmd = request.ValueRO; Entity sender = Entity.Null; var connEntity = receive.ValueRO.SourceConnection; if (SystemAPI.HasComponent(connEntity)) playerByConn.TryGetValue(SystemAPI.GetComponent(connEntity).Value, out sender); switch (cmd.Op) { case DebugOp.SpawnWave: if (haveCycle && SystemAPI.HasComponent(cycleEntity)) { var ts = SystemAPI.GetComponent(cycleEntity); ts.PendingSiegeSize = math.max(1, cmd.ArgA); ts.ArmTick = 0; // fire as soon as CyclePhaseSystem sees it SystemAPI.SetComponent(cycleEntity, ts); } break; case DebugOp.EndSiege: case DebugOp.SetCalm: CullHusks(ref ecb); if (SystemAPI.TryGetSingletonEntity(out var we)) { var w = SystemAPI.GetComponent(we); w.Phase = WavePhase.Lull; w.RemainingToSpawn = 0; SystemAPI.SetComponent(we, w); } if (haveCycle && SystemAPI.HasComponent(cycleEntity)) { var ts = SystemAPI.GetComponent(cycleEntity); ts.PendingSiegeSize = 0; ts.ArmTick = 0; ts.SiegeStartTick = 0; SystemAPI.SetComponent(cycleEntity, ts); } if (cmd.Op == DebugOp.SetCalm && haveCycle) { var cs = SystemAPI.GetComponent(cycleEntity); cs.Phase = CyclePhase.Calm; cs.PhaseEndTick = 0; SystemAPI.SetComponent(cycleEntity, cs); } break; case DebugOp.ClearEnemies: CullHusks(ref ecb); break; case DebugOp.GrantResource: if (SystemAPI.TryGetSingletonEntity(out var ledgerE)) { var ledger = SystemAPI.GetBuffer(ledgerE); StorageMath.Deposit(ledger, (ushort)cmd.ArgA, cmd.ArgB); } break; case DebugOp.GrantUpgrade: if (sender != Entity.Null && SystemAPI.HasBuffer(sender)) GrowDamageModifier(SystemAPI.GetBuffer(sender)); break; case DebugOp.Teleport: if (sender != Entity.Null && SystemAPI.HasComponent(sender) && SystemAPI.HasComponent(sender)) { byte region = (byte)cmd.ArgA; SystemAPI.GetComponentRW(sender).ValueRW.Region = region; float3 baseCenter = new float3(0f, 1f, 0f); if (SystemAPI.TryGetSingleton(out var anchor)) baseCenter = BaseGridMath.PlotCenter(anchor); SystemAPI.GetComponentRW(sender).ValueRW.Position = RegionMath.RegionOrigin(region, baseCenter); } break; case DebugOp.ToggleGod: if (sender != Entity.Null && SystemAPI.HasComponent(sender)) SystemAPI.SetComponentEnabled(sender, !SystemAPI.IsComponentEnabled(sender)); break; case DebugOp.Heal: if (sender != Entity.Null && SystemAPI.HasComponent(sender)) { var h = SystemAPI.GetComponent(sender); h.Current = SystemAPI.HasComponent(sender) ? SystemAPI.GetComponent(sender).MaxHealth : h.Max; SystemAPI.SetComponent(sender, h); } break; case DebugOp.KillPlayer: if (sender != Entity.Null && SystemAPI.HasComponent(sender)) { var h = SystemAPI.GetComponent(sender); h.Current = 0f; SystemAPI.SetComponent(sender, h); } break; case DebugOp.AdvanceGoal: if (haveCycle && SystemAPI.HasComponent(cycleEntity)) { var g = SystemAPI.GetComponent(cycleEntity); g.Charge += math.max(1, cmd.ArgA); SystemAPI.SetComponent(cycleEntity, g); } break; case DebugOp.SetHeat: if (haveCycle && SystemAPI.HasComponent(cycleEntity)) { var ts = SystemAPI.GetComponent(cycleEntity); ts.Heat = cmd.ArgA; SystemAPI.SetComponent(cycleEntity, ts); } break; } ecb.DestroyEntity(reqEntity); } ecb.Playback(state.EntityManager); ecb.Dispose(); playerByConn.Dispose(); } void CullHusks(ref EntityCommandBuffer ecb) { var husks = m_Husks.ToEntityArray(Allocator.Temp); for (int i = 0; i < husks.Length; i++) ecb.DestroyEntity(husks[i]); husks.Dispose(); } static void GrowDamageModifier(DynamicBuffer mods) { const uint debugSourceId = 0x00DEB061u; // distinct debug sentinel (replace-by-SourceId keeps it bounded) for (int i = 0; i < mods.Length; i++) { if (mods[i].SourceId == debugSourceId && mods[i].Target == (byte)StatTarget.Damage) { var m = mods[i]; m.Value += 0.25f; mods[i] = m; return; } } mods.Add(new StatModifier { Target = (byte)StatTarget.Damage, Op = (byte)ModOp.PercentAdd, Value = 0.25f, SourceId = debugSourceId, }); } } } #endif