Files
Project-M/Assets/_Project/Scripts/Server/World/CycleDirectorSpawnSystem.cs
T
kronic f31ffe910b Frontend menu + settings + saves foundation
Netcode frontend pattern: UITK main menu / pause / settings (MenuUi + controllers), on-demand world lifecycle (WorldLauncher/SessionRunner), GameBootstrap menu branch; Graphics/Audio settings (SettingsService/GameVolume); single-slot save foundation (SaveData/SaveService, born-correct load at director spawn, autosave on Siege->Calm + quit); RuntimePanelSettings + theme; BuildTool menu; 10 EditMode tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 15:05:36 -07:00

88 lines
4.0 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 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 <see cref="CycleDirectorSpawner"/> + NetworkTime,
/// instantiates the ghost, initializes <see cref="CycleState"/> (Expedition, cycle 1, PhaseEndTick =
/// now + <see cref="CyclePhase.ExpeditionTicks"/>), adds the server-only <see cref="CycleRuntime"/>, 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);
// 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());
// Born-correct load: if the menu staged a save (Continue), apply it AT SPAWN so the director
// ghost never serializes a default GoalProgress / empty ledger to clients (no replication flicker).
if (SystemAPI.TryGetSingletonEntity<PendingSave>(out var pendingEntity))
{
var pending = SystemAPI.GetComponent<PendingSave>(pendingEntity);
if (pending.HasData != 0)
{
ecb.SetComponent(director, new GoalProgress { Charge = pending.GoalCharge, Target = pending.GoalTarget });
var srcLedger = SystemAPI.GetBuffer<PendingSaveLedgerRow>(pendingEntity);
var destLedger = ecb.SetBuffer<StorageEntry>(director);
SaveApply.WriteLedger(srcLedger, destLedger);
}
ecb.DestroyEntity(pendingEntity);
}
// 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);
}
}
}