#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/stop waves, clear enemies, grant resources/upgrades, teleport, god-mode, /// heal/kill, class swap, gym enemy spawns. Sender-targeted ops resolve the player via SourceConnection -> /// NetworkId -> GhostOwner (the RegionTransitSystem pattern). Plain server SimulationSystemGroup (NOT the /// predicted loop). 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(), ComponentType.Exclude()); // corpses expire on their own (B3; avoids a cross-ECB double-destroy) 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; uint now = SystemAPI.TryGetSingleton(out var netTime) && netTime.ServerTick.IsValid ? netTime.ServerTick.TickIndexForValidTick : 0u; foreach (var (request, receive, reqEntity) in SystemAPI.Query, RefRO>().WithEntityAccess()) { var cmd = request.ValueRO; Entity sender = Entity.Null; var connEntity = receive.ValueRO.SourceConnection; PlayerResolve.TryResolve(ref state, playerByConn, connEntity, out sender); switch (cmd.Op) { case DebugOp.SpawnWave: // re-meant (LANTERN): force the NEXT wave to start this tick if (SystemAPI.TryGetSingletonEntity(out var forceWaveE)) { var fw = SystemAPI.GetComponent(forceWaveE); fw.Phase = WavePhase.Lull; fw.NextActionTick = 0; // due immediately -> WaveSystem starts the next (bigger) wave SystemAPI.SetComponent(forceWaveE, fw); } break; case DebugOp.EndSiege: // re-meant (LANTERN): "quiet the arena" — cull husks + push the next wave far out CullHusks(ref ecb); if (SystemAPI.TryGetSingletonEntity(out var stopWaveE)) { var w = SystemAPI.GetComponent(stopWaveE); w.Phase = WavePhase.Lull; w.RemainingToSpawn = 0; w.NextActionTick = TickUtil.NonZero(now + 216000u); // ~1 h @ 60 Hz: waves stay quiet for the session SystemAPI.SetComponent(stopWaveE, w); } 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.SetTuning: if (SystemAPI.TryGetSingleton(out var tuningCfg)) { TuningConfig.Apply(ref tuningCfg, (byte)cmd.ArgA, cmd.ArgB / 1000f); SystemAPI.SetSingleton(tuningCfg); } break; case DebugOp.SetClass: // Swap an already-spawned player's frame IN PLACE (editor dev tool). Frame = FrameId + the // ClassSourceId-tagged StatModifier seeds + the 4-socket Spark loadout; the owner's // StatRecomputeSystem refolds EffectiveCharacterStats. Server-authoritative + prediction- // correct (same buffer-mutation path as GrantUpgrade). The swap runs even on a corpse; the // heal is gated on a LIVING player so we don't resurrect out-of-band and race // PlayerRespawnSystem (which refills to the new max on respawn itself). if (sender != Entity.Null && SystemAPI.HasBuffer(sender) && SystemAPI.HasBuffer(sender)) { var classMods = SystemAPI.GetBuffer(sender); // 2026-08-07 audit purge: the meta re-sync half of the swap went with the meta shop; // ClassSwapUtil now re-seeds only the frame stat band. Shared with the player-facing // ClassSelectReceiveSystem so the two paths cannot drift. ClassSwapUtil.Apply((byte)cmd.ArgA, classMods, out byte swNewClass); if (SystemAPI.HasComponent(sender)) SystemAPI.SetComponent(sender, new FrameId { Value = swNewClass }); ClassTraits.FrameLoadout(swNewClass, out byte sf0, out byte sf1, out byte sf2, out byte sf3); var swSockets = SystemAPI.GetBuffer(sender); swSockets.Clear(); swSockets.Add(new AbilitySocket { SparkId = sf0 }); swSockets.Add(new AbilitySocket { SparkId = sf1 }); swSockets.Add(new AbilitySocket { SparkId = sf2 }); swSockets.Add(new AbilitySocket { SparkId = sf3 }); if (SystemAPI.HasComponent(sender)) SystemAPI.SetComponent(sender, default(SocketCooldown)); if (SystemAPI.HasComponent(sender)) SystemAPI.SetComponent(sender, default(ConeContactPending)); // 07-21 G6: socket identity changed — drop any armed slam if (SystemAPI.HasComponent(sender) && SystemAPI.HasComponent(sender) && SystemAPI.TryGetSingleton(out var abilityDb2)) { byte charId2 = SystemAPI.GetComponent(sender).Id; if (abilityDb2.Value.Value.TryGetCharacter(charId2, out var baseChar2)) { var hp2 = SystemAPI.GetComponent(sender); ClassSwapUtil.HealClamp(ref hp2, baseChar2.MaxHealth, classMods); SystemAPI.SetComponent(sender, hp2); } } } break; break; case DebugOp.SpawnEnemy: // GYM: spawn a chosen enemy KIND (Drowner/Grindylow) from the baked roster near the sender. if (sender != Entity.Null && SystemAPI.HasComponent(sender) && SystemAPI.TryGetSingletonEntity(out var gymEntity) && SystemAPI.HasBuffer(gymEntity)) { var roster = SystemAPI.GetBuffer(gymEntity); Entity enemyPrefab = Entity.Null; for (int r = 0; r < roster.Length; r++) if (roster[r].Kind == (byte)cmd.ArgA) { enemyPrefab = roster[r].Prefab; break; } if (enemyPrefab != Entity.Null) { var sPos = SystemAPI.GetComponent(sender).Position; var bakedEnemyLt = state.EntityManager.GetComponentData(enemyPrefab); int spawnCount = math.max(1, cmd.ArgB); for (int k = 0; k < spawnCount; k++) { float ang = k * 0.7f; float3 pos = sPos + new float3(math.cos(ang), 0f, math.sin(ang)) * 5f; pos.y = sPos.y; var enemy = ecb.Instantiate(enemyPrefab); ecb.SetComponent(enemy, bakedEnemyLt.WithPosition(pos)); ecb.AddComponent(enemy, new RegionTag { Region = RegionId.Base }); } } } 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