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>
This commit is contained in:
2026-06-06 15:05:36 -07:00
parent f3f65bccbf
commit f31ffe910b
56 changed files with 1744 additions and 8 deletions
@@ -0,0 +1,60 @@
using System;
using ProjectM.Simulation;
using Unity.Entities;
using Unity.NetCode;
namespace ProjectM.Server
{
/// <summary>
/// Host-only autosave writer. A managed <see cref="SystemBase"/> (file IO =&gt; NO Burst) that reacts to the
/// <see cref="SaveRequest"/> flag the Bursted <c>CyclePhaseSystem</c> raises on the Siege-&gt;Calm checkpoint:
/// reads the authoritative <see cref="GoalProgress"/> + shared resource ledger off the CycleDirector ghost,
/// writes the JSON save (<see cref="SaveService"/>), then clears the flag. ServerSimulation-only, so a pure
/// (Join) client never writes. Deliberately carries NO <c>[UpdateAfter(CyclePhaseSystem)]</c> (that would risk
/// a sort-cycle); a one-tick-late autosave is irrelevant.
/// </summary>
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
public partial class SaveWriteSystem : SystemBase
{
protected override void OnCreate()
{
RequireForUpdate<SaveRequest>();
RequireForUpdate<NetworkTime>();
}
protected override void OnUpdate()
{
var dir = SystemAPI.GetSingletonEntity<SaveRequest>();
var req = SystemAPI.GetComponent<SaveRequest>(dir);
if (req.Pending == 0)
return;
req.Pending = 0;
SystemAPI.SetComponent(dir, req);
var goal = SystemAPI.HasComponent<GoalProgress>(dir)
? SystemAPI.GetComponent<GoalProgress>(dir)
: default;
// The shared ledger lives on this same CycleDirector ghost (ResourceLedger-tagged StorageEntry buffer).
var buffer = SystemAPI.GetBuffer<StorageEntry>(dir);
var rows = new LedgerRow[buffer.Length];
for (int i = 0; i < buffer.Length; i++)
rows[i] = new LedgerRow { ItemId = buffer[i].ItemId, Count = buffer[i].Count };
// M7: also persist player-built structures + their production tick-state / inventory (single shared scan).
uint nowTick = SystemAPI.GetSingleton<NetworkTime>().ServerTick.TickIndexForValidTick;
SaveStructureScan.Collect(EntityManager, nowTick, out var structures, out var structureIo);
SaveService.Save(new SaveData
{
GoalCharge = goal.Charge,
GoalTarget = goal.Target,
Ledger = rows,
Structures = structures,
StructureIo = structureIo,
SavedAtMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
});
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: a08738b815484f4499b9ab614698c3e6