using ProjectM.Simulation; using Unity.Burst; using Unity.Collections; using Unity.Entities; using Unity.NetCode; using Unity.Transforms; namespace ProjectM.Server { /// /// Server-only, one-shot spawner for the GLOBAL cycle-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 + NetworkTime, /// instantiates the ghost, initializes (Expedition, cycle 1, PhaseEndTick = /// now + ), adds the server-only , 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. /// [BurstCompile] [WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)] public partial struct CycleDirectorSpawnSystem : ISystem { [BurstCompile] public void OnCreate(ref SystemState state) { state.RequireForUpdate(); state.RequireForUpdate(); } [BurstCompile] public void OnUpdate(ref SystemState state) { var serverTick = SystemAPI.GetSingleton().ServerTick; if (!serverTick.IsValid) return; var spawnerEntity = SystemAPI.GetSingletonEntity(); var spawner = SystemAPI.GetComponent(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(spawner.Prefab); if (SystemAPI.TryGetSingleton(out var anchor)) xform.Position = BaseGridMath.PlotCenter(anchor); ecb.SetComponent(director, xform); // Boot the run-state in Calm (the persistent default) — no timer; ThreatDirector arms sieges. ecb.SetComponent(director, new CycleState { Phase = CyclePhase.Calm, CycleNumber = 1, PhaseEndTick = 0u, }); ecb.AddComponent(director, new CycleRuntime { DefendStartWave = 0 }); ecb.AddComponent(director, new ThreatState()); // END-2: server-only run-phase marker (Normal until the goal cap arms the final siege). Added at // spawn like CycleRuntime/ThreatState (never on the ghost serializer). RunOutcome is baked on the prefab. ecb.AddComponent(director, new RunPhase { Value = RunPhaseId.Normal }); // Born-correct load: if the menu staged a save (Continue), apply it AT SPAWN so the director // DR-042 C6c: a NEW game seeds starting Ore below; a restored save (Continue) keeps its ledger. bool restoredLedger = false; // ghost never serializes a default GoalProgress / empty ledger to clients (no replication flicker). if (SystemAPI.TryGetSingletonEntity(out var pendingEntity)) { var pending = SystemAPI.GetComponent(pendingEntity); if (pending.HasData != 0) { // END-2: clamp the restored Target to the baked run-length so a pre-v5 save carrying the old // Target=10 still honours the slice's baked Target=4 (the final siege stays reachable). int bakedTarget = SystemAPI.HasComponent(spawner.Prefab) ? SystemAPI.GetComponent(spawner.Prefab).Target : pending.GoalTarget; int restoredTarget = pending.GoalTarget > 0 && pending.GoalTarget < bakedTarget ? pending.GoalTarget : bakedTarget; ecb.SetComponent(director, new GoalProgress { Charge = pending.GoalCharge, Target = restoredTarget }); var srcLedger = SystemAPI.GetBuffer(pendingEntity); var destLedger = ecb.SetBuffer(director); SaveApply.WriteLedger(srcLedger, destLedger); restoredLedger = true; // a save restored the ledger -> do NOT seed starting Ore (C6c) // END-1: born-correct the Engine Core. Max comes from the BAKED prefab (never the save); a // persisted wounded Current (>0) restores clamped to Max, else (0 = pre-v4 save) born full. if (SystemAPI.HasComponent(spawner.Prefab)) { var bakedCore = SystemAPI.GetComponent(spawner.Prefab); int restoredCore = pending.CoreCurrent > 0 ? (pending.CoreCurrent < bakedCore.Max ? pending.CoreCurrent : bakedCore.Max) : bakedCore.Max; ecb.SetComponent(director, new CoreIntegrity { Current = restoredCore, Max = bakedCore.Max, OverrunTick = 0u }); } // END-2: born-correct the terminal run outcome (a won/lost run loads finished + halted; a pre-v5 // save / New Game = 0 -> InProgress). Independent of the Core -> NOT nested in the CoreIntegrity guard. ecb.SetComponent(director, new RunOutcome { Value = pending.RunOutcome }); } ecb.DestroyEntity(pendingEntity); } // DR-042 C6c: NEW game only (no restored ledger) -> seed a little Ore so the build loop isn't a cold // deadlock (a turret needs Charge from a Fabricator that costs Ore you haven't mined yet). 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 on the Siege->Calm checkpoint. 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); } } }