b34945c2d2
Deletes CyclePhaseSystem, GoalReachedSystem, CoreDamage/CoreRestore, ThreatDirector, CoreIntegrity/GoalProgress/RunPhase/RunOutcome/ThreatState components, CoreVisualFeedbackSystem, and the whole Client/Onboarding slice (+6 test files). Keepers reworked: RunDirectorSystem (UpdateBefore attr + launch guard + goal/threat bank removed; sole SaveRequest raiser now), CycleDirectorSpawnSystem (ledger/meta host only), WaveSystem UNGATED (waves run wherever a WaveDirector is baked), EnemyAISystem core-fallback stripped, AmbientAudioSystem reworked (bed + run cues; no CycleState gate), MusicSystem RunInfo-only, HudSystem big trim (goal meter, core bar, siege banner, terminal banner, outcome flash, onboarding hook all gone), MetaShop/ClassPrep/AimReticle siege gates dropped, DebugOverlay/ops re-meant (SpawnWave=force next wave, EndSiege=quiet arena; SetCalm/AdvanceGoal/SetHeat retired, bytes reserved), TuningConfig Core knobs retired (ids 20-23 reserved), StorageMath.DrainFraction deleted, HowToPlay copy rewritten. Save epoch v7 (fresh epoch, operator-approved): SaveData drops goal/core/outcome + conveyor/machine-IO fields; MinLoadableVersion=7; PendingSave/PendingStructure trimmed; RollTerminalCampaignForward deleted; SaveStructureScan signature slimmed. 390 tests green; Play world-creation clean (player + waves live, no exceptions). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
250 lines
14 KiB
C#
250 lines
14 KiB
C#
#if UNITY_EDITOR
|
|
using ProjectM.Simulation;
|
|
using Unity.Collections;
|
|
using Unity.Entities;
|
|
using Unity.Mathematics;
|
|
using Unity.NetCode;
|
|
using Unity.Transforms;
|
|
|
|
namespace ProjectM.Server
|
|
{
|
|
/// <summary>
|
|
/// EDITOR-ONLY server receiver for <see cref="DebugCommandRequest"/> 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
|
|
/// (<see cref="DebugCommandRequest"/>) is unconditional so the RPC collection hash matches across peers.
|
|
/// Non-Burst (managed-simple, editor-only) — perf is irrelevant.
|
|
/// </summary>
|
|
[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<EnemyTag>(), ComponentType.Exclude<Dying>()); // corpses expire on their own (B3; avoids a cross-ECB double-destroy)
|
|
var builder = new EntityQueryBuilder(Allocator.Temp)
|
|
.WithAll<DebugCommandRequest, ReceiveRpcCommandRequest>();
|
|
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<int, Entity>(8, Allocator.Temp);
|
|
foreach (var (owner, e) in SystemAPI.Query<RefRO<GhostOwner>>().WithAll<PlayerTag>().WithEntityAccess())
|
|
playerByConn[owner.ValueRO.NetworkId] = e;
|
|
|
|
uint now = SystemAPI.TryGetSingleton<NetworkTime>(out var netTime) && netTime.ServerTick.IsValid
|
|
? netTime.ServerTick.TickIndexForValidTick : 0u;
|
|
|
|
foreach (var (request, receive, reqEntity) in
|
|
SystemAPI.Query<RefRO<DebugCommandRequest>, RefRO<ReceiveRpcCommandRequest>>().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<WaveState>(out var forceWaveE))
|
|
{
|
|
var fw = SystemAPI.GetComponent<WaveState>(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<WaveState>(out var stopWaveE))
|
|
{
|
|
var w = SystemAPI.GetComponent<WaveState>(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<ResourceLedger>(out var ledgerE))
|
|
{
|
|
var ledger = SystemAPI.GetBuffer<StorageEntry>(ledgerE);
|
|
StorageMath.Deposit(ledger, (ushort)cmd.ArgA, cmd.ArgB);
|
|
}
|
|
break;
|
|
|
|
case DebugOp.GrantUpgrade:
|
|
if (sender != Entity.Null && SystemAPI.HasBuffer<StatModifier>(sender))
|
|
GrowDamageModifier(SystemAPI.GetBuffer<StatModifier>(sender));
|
|
break;
|
|
|
|
case DebugOp.Teleport:
|
|
if (sender != Entity.Null && SystemAPI.HasComponent<RegionTag>(sender)
|
|
&& SystemAPI.HasComponent<LocalTransform>(sender))
|
|
{
|
|
byte region = (byte)cmd.ArgA;
|
|
SystemAPI.GetComponentRW<RegionTag>(sender).ValueRW.Region = region;
|
|
float3 baseCenter = new float3(0f, 1f, 0f);
|
|
if (SystemAPI.TryGetSingleton<BaseAnchor>(out var anchor))
|
|
baseCenter = BaseGridMath.PlotCenter(anchor);
|
|
SystemAPI.GetComponentRW<LocalTransform>(sender).ValueRW.Position =
|
|
RegionMath.RegionOrigin(region, baseCenter);
|
|
}
|
|
break;
|
|
|
|
case DebugOp.ToggleGod:
|
|
if (sender != Entity.Null && SystemAPI.HasComponent<DebugGodMode>(sender))
|
|
SystemAPI.SetComponentEnabled<DebugGodMode>(sender, !SystemAPI.IsComponentEnabled<DebugGodMode>(sender));
|
|
break;
|
|
|
|
case DebugOp.Heal:
|
|
if (sender != Entity.Null && SystemAPI.HasComponent<Health>(sender))
|
|
{
|
|
var h = SystemAPI.GetComponent<Health>(sender);
|
|
h.Current = SystemAPI.HasComponent<EffectiveCharacterStats>(sender)
|
|
? SystemAPI.GetComponent<EffectiveCharacterStats>(sender).MaxHealth
|
|
: h.Max;
|
|
SystemAPI.SetComponent(sender, h);
|
|
}
|
|
break;
|
|
|
|
case DebugOp.KillPlayer:
|
|
if (sender != Entity.Null && SystemAPI.HasComponent<Health>(sender))
|
|
{
|
|
var h = SystemAPI.GetComponent<Health>(sender);
|
|
h.Current = 0f;
|
|
SystemAPI.SetComponent(sender, h);
|
|
}
|
|
break;
|
|
|
|
case DebugOp.SetTuning:
|
|
if (SystemAPI.TryGetSingleton<TuningConfig>(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 class IN PLACE (editor dev tool). Class = two replicated
|
|
// pieces: the AbilityRef Fire slot + the ClassSourceId-tagged StatModifier seeds; the owner's
|
|
// StatRecomputeSystem refolds EffectiveCharacterStats. Server-authoritative + prediction-correct
|
|
// (same buffer-mutation path as GrantUpgrade). Reapply + AbilityRef run unconditionally so the
|
|
// class is correct even on a corpse; the heal is gated on a LIVING player so we don't resurrect
|
|
// it out-of-band and race PlayerRespawnSystem (which refills to the new max on respawn itself).
|
|
if (sender != Entity.Null && SystemAPI.HasComponent<AbilityRef>(sender)
|
|
&& SystemAPI.HasBuffer<StatModifier>(sender))
|
|
{
|
|
var classMods = SystemAPI.GetBuffer<StatModifier>(sender);
|
|
Entity dir2 = Entity.Null;
|
|
bool haveMeta2 = SystemAPI.TryGetSingleton<MetaUpgradeCatalog>(out var metaCat2)
|
|
&& SystemAPI.TryGetSingletonEntity<ResourceLedger>(out dir2) && SystemAPI.HasBuffer<MetaTierState>(dir2);
|
|
var metaRec2 = haveMeta2 ? SystemAPI.GetBuffer<MetaTierState>(dir2) : default;
|
|
// DR-046: the FULL swap (class seeds + meta re-sync) now lives in the shared ClassSwapUtil,
|
|
// used by BOTH this dev path and the base ClassSelectReceiveSystem so they cannot drift.
|
|
ClassSwapUtil.Apply((byte)cmd.ArgA, classMods, haveMeta2, metaCat2, metaRec2,
|
|
out byte swNewClass, out byte swNewAbility);
|
|
SystemAPI.SetComponent(sender, new AbilityRef { Id = swNewAbility });
|
|
if (SystemAPI.HasComponent<PlayerClass>(sender))
|
|
SystemAPI.SetComponent(sender, new PlayerClass { ClassId = swNewClass });
|
|
if (SystemAPI.HasComponent<AbilityCooldown>(sender))
|
|
SystemAPI.SetComponent(sender, new AbilityCooldown { NextFireTick = 0 });
|
|
if (SystemAPI.HasComponent<Health>(sender) && SystemAPI.HasComponent<CharacterStatsRef>(sender)
|
|
&& SystemAPI.TryGetSingleton<AbilityDatabase>(out var abilityDb2))
|
|
{
|
|
byte charId2 = SystemAPI.GetComponent<CharacterStatsRef>(sender).Id;
|
|
if (abilityDb2.Value.Value.TryGetCharacter(charId2, out var baseChar2))
|
|
{
|
|
var hp2 = SystemAPI.GetComponent<Health>(sender);
|
|
ClassSwapUtil.HealClamp(ref hp2, baseChar2.MaxHealth, classMods);
|
|
SystemAPI.SetComponent(sender, hp2);
|
|
}
|
|
}
|
|
}
|
|
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<LocalTransform>(sender)
|
|
&& SystemAPI.TryGetSingletonEntity<GymTag>(out var gymEntity)
|
|
&& SystemAPI.HasBuffer<GymEnemyRoster>(gymEntity))
|
|
{
|
|
var roster = SystemAPI.GetBuffer<GymEnemyRoster>(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<LocalTransform>(sender).Position;
|
|
var bakedEnemyLt = state.EntityManager.GetComponentData<LocalTransform>(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<StatModifier> 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
|