using ProjectM.Simulation; using Unity.Burst; using Unity.Collections; using Unity.Entities; using Unity.Mathematics; using Unity.NetCode; using Unity.Transforms; namespace ProjectM.Server { /// /// One-shot server restore of player-built structures for a "Continue" session. The menu (WorldLauncher) stages a /// carrier in the fresh ServerWorld BEFORE the gameplay subscene streams; this /// system waits (RequireForUpdate) for the streamed + + /// a valid NetworkTime, then replays each saved structure CHARGE-FREE: Instantiate the catalog prefab at the /// saved cell (preserving the baked Scale), restore the wounded HP born-correct, re-tag RegionTag{Base} + /// RuntimePlacedTag, then DESTROY the carrier so it never runs again. The ledger restores separately + /// absolutely via CycleDirectorSpawnSystem's born-correct load (no double-spend, no Withdraw here). /// [BurstCompile] [WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)] public partial struct BaseRestoreSystem : ISystem { ComponentLookup m_TransformLookup; ComponentLookup m_HealthLookup; [BurstCompile] public void OnCreate(ref SystemState state) { m_TransformLookup = state.GetComponentLookup(isReadOnly: true); m_HealthLookup = state.GetComponentLookup(isReadOnly: true); state.RequireForUpdate(); state.RequireForUpdate(); state.RequireForUpdate(); state.RequireForUpdate(state.GetEntityQuery(ComponentType.ReadOnly())); } [BurstCompile] public void OnUpdate(ref SystemState state) { var serverTick = SystemAPI.GetSingleton().ServerTick; if (!serverTick.IsValid) return; uint now = serverTick.TickIndexForValidTick; m_TransformLookup.Update(ref state); m_HealthLookup.Update(ref state); var anchor = SystemAPI.GetSingleton(); var catalog = SystemAPI.GetBuffer(SystemAPI.GetSingletonEntity()); var ecb = new EntityCommandBuffer(Allocator.Temp); foreach (var (pending, carrier) in SystemAPI.Query>().WithEntityAccess()) { for (int s = 0; s < pending.Length; s++) { var p = pending[s]; int entryIdx = -1; for (int i = 0; i < catalog.Length; i++) if (catalog[i].Type == p.Type) { entryIdx = i; break; } if (entryIdx < 0 || catalog[entryIdx].Prefab == Entity.Null) continue; // type not in the catalog (e.g. a save from a newer build) -> skip, don't crash var prefab = catalog[entryIdx].Prefab; var structure = ecb.Instantiate(prefab); int2 cell = new int2(p.CellX, p.CellZ); var xform = m_TransformLookup[prefab]; xform.Position = BaseGridMath.CellToWorld(anchor, cell); // preserve baked Scale (FromPosition would reset it) ecb.SetComponent(structure, xform); ecb.SetComponent(structure, new PlacedStructure { Type = p.Type, Cell = cell, NextTick = 0u, // cooldown restore retired with the automation chain (LANTERN purge) LastProcessedTick = TickUtil.NonZero(now), }); // EB-1: restore the wounded HP born-correct in the SAME ecb as Instantiate (Health.Current is a // [GhostField]; a deferred set would leak baked Max to clients for one snapshot). Max + the // 0->full fallback come from the BAKED prefab, never the save. if (m_HealthLookup.HasComponent(prefab)) { var hm = m_HealthLookup[prefab]; ecb.SetComponent(structure, new Health { Current = p.HP > 0f ? p.HP : hm.Max, Max = hm.Max }); } ecb.AddComponent(structure, new RegionTag { Region = RegionId.Base }); ecb.AddComponent(structure); } ecb.DestroyEntity(carrier); } ecb.Playback(state.EntityManager); ecb.Dispose(); } } }