Files
Project-M/Assets/_Project/Scripts/Server/World/CycleDirectorSpawnSystem.cs
T
kronic b34945c2d2 LANTERN purge B3+B5: delete the cycle/core/win-lose spine + onboarding; save epoch v7
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>
2026-07-15 15:27:12 -07:00

126 lines
6.9 KiB
C#

using ProjectM.Simulation;
using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.NetCode;
using Unity.Transforms;
namespace ProjectM.Server
{
/// <summary>
/// Server-only, one-shot spawner for the GLOBAL director ghost (mirrors SharedStorageSpawnSystem, but MINUS
/// the RegionTag — the director must stay global so GhostRelevancy keeps it relevant to every region). On its
/// first update it reads the baked <see cref="CycleDirectorSpawner"/> + NetworkTime, instantiates the ghost
/// — the shared-ledger / RunInfo / meta host (its old cycle/siege/goal/core state is retired, LANTERN purge) —
/// applies a menu-staged save born-correct, and places it at the base center (preserving the prefab's baked
/// LocalTransform scale — FromPosition would reset the replicated Scale GhostField), then destroys the
/// spawner so it idles.
/// </summary>
[BurstCompile]
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
public partial struct CycleDirectorSpawnSystem : ISystem
{
[BurstCompile]
public void OnCreate(ref SystemState state)
{
state.RequireForUpdate<CycleDirectorSpawner>();
state.RequireForUpdate<NetworkTime>();
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
var serverTick = SystemAPI.GetSingleton<NetworkTime>().ServerTick;
if (!serverTick.IsValid)
return;
var spawnerEntity = SystemAPI.GetSingletonEntity<CycleDirectorSpawner>();
var spawner = SystemAPI.GetComponent<CycleDirectorSpawner>(spawnerEntity);
var ecb = new EntityCommandBuffer(Allocator.Temp);
if (spawner.Prefab != Entity.Null)
{
var director = ecb.Instantiate(spawner.Prefab);
// Place at the base center, preserving the prefab's baked scale/rotation.
var xform = SystemAPI.GetComponent<LocalTransform>(spawner.Prefab);
if (SystemAPI.TryGetSingleton<BaseAnchor>(out var anchor))
xform.Position = BaseGridMath.PlotCenter(anchor);
ecb.SetComponent(director, xform);
// Expedition redesign: run-FSM working state + the co-op route first-commit latch + the persisted
// meta counters — ALL added UNCONDITIONALLY at spawn (D-F2: a New-Game boot must have the components
// the bank block reads; Continue restores VALUES only, inside the HasData block — Step 12b).
// HostSalt starts a fixed non-tick seed lineage (bumped per launch); the save folds persisted
// RunsCompleted in at restore so cross-session runs diverge.
ecb.AddComponent(director, new RunRuntime { HostSalt = 0x5EED0001u });
ecb.AddComponent(director, default(RouteCommand));
ecb.AddComponent(director, default(PortalCommand)); // DR-046 room-exit portal interact latch
ecb.AddComponent(director, default(MetaCounters));
// Born-correct load: if the menu staged a save (Continue), apply it AT SPAWN so the director
// ghost never serializes an empty ledger to clients (no replication flicker).
// DR-042 C6c: a NEW game seeds starting Ore below; a restored save (Continue) keeps its ledger.
bool restoredLedger = false;
if (SystemAPI.TryGetSingletonEntity<PendingSave>(out var pendingEntity))
{
var pending = SystemAPI.GetComponent<PendingSave>(pendingEntity);
if (pending.HasData != 0)
{
var srcLedger = SystemAPI.GetBuffer<PendingSaveLedgerRow>(pendingEntity);
var destLedger = ecb.SetBuffer<StorageEntry>(director);
SaveApply.WriteLedger(srcLedger, destLedger);
restoredLedger = true; // a save restored the ledger -> do NOT seed starting Ore (C6c)
// v6: restore the permanent meta — counters (VALUES only; the component was added
// unconditionally above, D-F2), the tier record (SetBuffer replaces the baked-empty
// [GhostField] buffer pre-Playback — the StorageEntry idiom — rows VERBATIM incl. unknown
// ids), the born-correct RunInfo HUD mirror (the spawn-time exception to RunDirector's
// sole-writer rule), and the HostSalt fold (cross-session first-run maps diverge once
// you've banked clears — the promise at the RunRuntime add).
ecb.SetComponent(director, new MetaCounters
{
RunsCompleted = pending.RunsCompleted,
MaxDepthReached = pending.MaxDepthReached,
});
var metaSrc = SystemAPI.GetBuffer<PendingMetaRow>(pendingEntity);
var metaDst = ecb.SetBuffer<MetaTierState>(director);
for (int mi = 0; mi < metaSrc.Length; mi++)
metaDst.Add(new MetaTierState { ClassId = metaSrc[mi].ClassId, UpgradeId = metaSrc[mi].UpgradeId, Tier = metaSrc[mi].Tier });
if (SystemAPI.HasComponent<RunInfo>(spawner.Prefab))
{
var runInfo = SystemAPI.GetComponent<RunInfo>(spawner.Prefab); // baked Lifecycle=Staging
runInfo.RunsCompleted = pending.RunsCompleted;
runInfo.MaxDepthReached = pending.MaxDepthReached;
ecb.SetComponent(director, runInfo);
}
ecb.SetComponent(director, new RunRuntime
{
HostSalt = RunMapMath.Hash(0x5EED0001u, (uint)pending.RunsCompleted + 1u),
});
}
ecb.DestroyEntity(pendingEntity);
}
// DR-042 C6c: NEW game only (no restored ledger) -> seed a little Ore so the build loop isn't a
// cold start with nothing to place. Appended BEFORE Playback so the ghost first-serializes WITH
// the seed (no empty-ledger replication flicker).
if (!restoredLedger)
ecb.AppendToBuffer(director, new StorageEntry { ItemId = ResourceId.Ore, Count = Tuning.StartingOre });
// Host-only autosave flag; SaveWriteSystem consumes it (RunDirectorSystem raises it on bank).
ecb.AddComponent(director, new SaveRequest { Pending = 0 });
}
// One-shot: remove the spawner so RequireForUpdate fails and the system idles.
ecb.DestroyEntity(spawnerEntity);
ecb.Playback(state.EntityManager);
ecb.Dispose();
}
}
}