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:
@@ -29,7 +29,9 @@ namespace ProjectM.Server
|
||||
|
||||
cfgRef.ValueRW.Requested = false;
|
||||
|
||||
var endpoint = NetworkEndpoint.AnyIpv4.WithPort(cfgRef.ValueRO.Port);
|
||||
var endpoint = cfgRef.ValueRO.Address.ToString() == "127.0.0.1"
|
||||
? NetworkEndpoint.LoopbackIpv4.WithPort(cfgRef.ValueRO.Port) // single-player: bind loopback only (no firewall prompt)
|
||||
: NetworkEndpoint.AnyIpv4.WithPort(cfgRef.ValueRO.Port); // host: accept LAN peers
|
||||
|
||||
var ecb = new EntityCommandBuffer(Allocator.Temp);
|
||||
var req = ecb.CreateEntity();
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6aa6ed6723d75a64796d926f11f63236
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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 => NO Burst) that reacts to the
|
||||
/// <see cref="SaveRequest"/> flag the Bursted <c>CyclePhaseSystem</c> raises on the Siege->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
|
||||
@@ -58,6 +58,24 @@ namespace ProjectM.Server
|
||||
});
|
||||
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.
|
||||
|
||||
@@ -99,6 +99,9 @@ namespace ProjectM.Server
|
||||
var goal = SystemAPI.GetComponent<GoalProgress>(cycleEntity);
|
||||
goal.Charge += 1;
|
||||
SystemAPI.SetComponent(cycleEntity, goal);
|
||||
// Autosave checkpoint: a survived siege is a natural save point (host-only writer consumes the flag).
|
||||
if (SystemAPI.HasComponent<SaveRequest>(cycleEntity))
|
||||
SystemAPI.SetComponent(cycleEntity, new SaveRequest { Pending = 1 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user