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 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 /// — 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. /// [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); // 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(out var pendingEntity)) { var pending = SystemAPI.GetComponent(pendingEntity); if (pending.HasData != 0) { 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) // 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(pendingEntity); var metaDst = ecb.SetBuffer(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(spawner.Prefab)) { var runInfo = SystemAPI.GetComponent(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(); } } }