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:
@@ -0,0 +1,113 @@
|
||||
using NUnit.Framework;
|
||||
using ProjectM.Client;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ProjectM.Tests
|
||||
{
|
||||
/// <summary>
|
||||
/// Pure-logic tests for the client settings model (no ECS world): JSON round-trip via JsonUtility,
|
||||
/// defensive clamping of hand-edited/corrupt values, and the GameVolume bus statics. Engine APIs
|
||||
/// (Screen/QualitySettings) resolve in EditMode, so Defaults()/Clamped() are exercisable here.
|
||||
/// </summary>
|
||||
public class GameSettingsTests
|
||||
{
|
||||
static GameSettings Sample() => new GameSettings
|
||||
{
|
||||
Version = GameSettings.CurrentVersion,
|
||||
ResWidth = 2560,
|
||||
ResHeight = 1440,
|
||||
RefreshHz = 144,
|
||||
FullScreenMode = (int)FullScreenMode.ExclusiveFullScreen,
|
||||
QualityLevel = 0,
|
||||
VSync = 2,
|
||||
TargetFps = 120,
|
||||
Master = 0.8f,
|
||||
Music = 0.5f,
|
||||
Sfx = 0.3f,
|
||||
};
|
||||
|
||||
[Test]
|
||||
public void Json_RoundTrip_PreservesAllFields()
|
||||
{
|
||||
var src = Sample();
|
||||
var json = JsonUtility.ToJson(src);
|
||||
var dst = JsonUtility.FromJson<GameSettings>(json);
|
||||
|
||||
Assert.AreEqual(src.Version, dst.Version);
|
||||
Assert.AreEqual(src.ResWidth, dst.ResWidth);
|
||||
Assert.AreEqual(src.ResHeight, dst.ResHeight);
|
||||
Assert.AreEqual(src.RefreshHz, dst.RefreshHz);
|
||||
Assert.AreEqual(src.FullScreenMode, dst.FullScreenMode);
|
||||
Assert.AreEqual(src.QualityLevel, dst.QualityLevel);
|
||||
Assert.AreEqual(src.VSync, dst.VSync);
|
||||
Assert.AreEqual(src.TargetFps, dst.TargetFps);
|
||||
Assert.AreEqual(src.Master, dst.Master, 1e-5f);
|
||||
Assert.AreEqual(src.Music, dst.Music, 1e-5f);
|
||||
Assert.AreEqual(src.Sfx, dst.Sfx, 1e-5f);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Clamped_BoundsOutOfRangeValues()
|
||||
{
|
||||
var bad = new GameSettings
|
||||
{
|
||||
ResWidth = 99999, ResHeight = -5, RefreshHz = -10,
|
||||
FullScreenMode = 99, QualityLevel = 9999, VSync = 7,
|
||||
TargetFps = 5, Master = 2f, Music = -1f, Sfx = 3f,
|
||||
};
|
||||
var c = bad.Clamped();
|
||||
|
||||
Assert.LessOrEqual(c.ResWidth, 7680);
|
||||
Assert.GreaterOrEqual(c.ResHeight, 480);
|
||||
Assert.GreaterOrEqual(c.RefreshHz, 0);
|
||||
Assert.That(c.FullScreenMode, Is.InRange(0, 3));
|
||||
Assert.That(c.VSync, Is.InRange(0, 2));
|
||||
Assert.AreEqual(1f, c.Master, 1e-5f); // 2f -> 1
|
||||
Assert.AreEqual(0f, c.Music, 1e-5f); // -1 -> 0
|
||||
Assert.AreEqual(1f, c.Sfx, 1e-5f); // 3f -> 1
|
||||
Assert.That(c.QualityLevel, Is.GreaterThanOrEqualTo(0)); // clamped to a valid level index
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Clamped_NonPositiveTargetFps_BecomesUncapped()
|
||||
{
|
||||
Assert.AreEqual(-1, new GameSettings { TargetFps = 0 }.Clamped().TargetFps);
|
||||
Assert.AreEqual(-1, new GameSettings { TargetFps = -50 }.Clamped().TargetFps);
|
||||
Assert.AreEqual(120, new GameSettings { TargetFps = 120, Master = 1f }.Clamped().TargetFps);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Defaults_AreValidAndFullVolume()
|
||||
{
|
||||
var d = GameSettings.Defaults();
|
||||
Assert.AreEqual(GameSettings.CurrentVersion, d.Version);
|
||||
Assert.Greater(d.ResWidth, 0);
|
||||
Assert.Greater(d.ResHeight, 0);
|
||||
Assert.AreEqual(1f, d.Master, 1e-5f);
|
||||
Assert.AreEqual(1f, d.Music, 1e-5f);
|
||||
Assert.AreEqual(1f, d.Sfx, 1e-5f);
|
||||
// Defaults survive a clamp unchanged (they are in-range by construction).
|
||||
var c = d.Clamped();
|
||||
Assert.AreEqual(d.Master, c.Master, 1e-5f);
|
||||
Assert.AreEqual(d.VSync, c.VSync);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GameVolume_BusesAreSettable_DefaultFull()
|
||||
{
|
||||
float prevMusic = GameVolume.Music, prevSfx = GameVolume.Sfx;
|
||||
try
|
||||
{
|
||||
GameVolume.Music = 0.25f;
|
||||
GameVolume.Sfx = 0.6f;
|
||||
Assert.AreEqual(0.25f, GameVolume.Music, 1e-5f);
|
||||
Assert.AreEqual(0.6f, GameVolume.Sfx, 1e-5f);
|
||||
}
|
||||
finally
|
||||
{
|
||||
GameVolume.Music = prevMusic;
|
||||
GameVolume.Sfx = prevSfx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5a097bb2e0683f544944c8a0cc9676ff
|
||||
@@ -0,0 +1,107 @@
|
||||
using NUnit.Framework;
|
||||
using ProjectM.Simulation;
|
||||
using Unity.Entities;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ProjectM.Tests
|
||||
{
|
||||
/// <summary>
|
||||
/// Pure tests for the save FOUNDATION: the JSON schema round-trips (JsonUtility), version handling is safe,
|
||||
/// and the born-correct ledger apply (<see cref="SaveApply.WriteLedger"/>) the server spawn system uses to
|
||||
/// overwrite a director's StorageEntry buffer from a staged PendingSave.
|
||||
/// </summary>
|
||||
public class SavePersistenceTests
|
||||
{
|
||||
[Test]
|
||||
public void SaveData_Json_RoundTrip_PreservesFields()
|
||||
{
|
||||
var data = new SaveData
|
||||
{
|
||||
GoalCharge = 42,
|
||||
GoalTarget = 10,
|
||||
Ledger = new[]
|
||||
{
|
||||
new LedgerRow { ItemId = 1, Count = 5 },
|
||||
new LedgerRow { ItemId = 2, Count = 9 },
|
||||
},
|
||||
SavedAtMs = 1234567890123L,
|
||||
};
|
||||
|
||||
var json = JsonUtility.ToJson(data);
|
||||
var back = JsonUtility.FromJson<SaveData>(json);
|
||||
|
||||
Assert.AreEqual(SaveData.CurrentVersion, back.Version);
|
||||
Assert.AreEqual(42, back.GoalCharge);
|
||||
Assert.AreEqual(10, back.GoalTarget);
|
||||
Assert.AreEqual(2, back.Ledger.Length);
|
||||
Assert.AreEqual(1, back.Ledger[0].ItemId);
|
||||
Assert.AreEqual(5, back.Ledger[0].Count);
|
||||
Assert.AreEqual(2, back.Ledger[1].ItemId);
|
||||
Assert.AreEqual(9, back.Ledger[1].Count);
|
||||
Assert.AreEqual(1234567890123L, back.SavedAtMs);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SaveData_EmptyJson_DoesNotThrow_And_EmptyLedgerRoundTrips()
|
||||
{
|
||||
Assert.DoesNotThrow(() => JsonUtility.FromJson<SaveData>("{}"));
|
||||
|
||||
var empty = new SaveData { GoalCharge = 0, GoalTarget = 10 };
|
||||
var back = JsonUtility.FromJson<SaveData>(JsonUtility.ToJson(empty));
|
||||
Assert.IsNotNull(back.Ledger);
|
||||
Assert.AreEqual(0, back.Ledger.Length);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SaveData_OldVersion_IsDetectable()
|
||||
{
|
||||
// A stale-version blob round-trips with its Version intact, so SaveService.Load rejects it (-> New Game).
|
||||
var old = new SaveData { Version = 0, GoalCharge = 7 };
|
||||
var back = JsonUtility.FromJson<SaveData>(JsonUtility.ToJson(old));
|
||||
Assert.AreEqual(0, back.Version);
|
||||
Assert.AreNotEqual(SaveData.CurrentVersion, back.Version);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void WriteLedger_Overwrites_Destination_From_Staged_Rows()
|
||||
{
|
||||
using var world = new World("SaveApplyTest");
|
||||
var em = world.EntityManager;
|
||||
var e = em.CreateEntity();
|
||||
em.AddBuffer<PendingSaveLedgerRow>(e);
|
||||
em.AddBuffer<StorageEntry>(e);
|
||||
|
||||
var src = em.GetBuffer<PendingSaveLedgerRow>(e);
|
||||
src.Add(new PendingSaveLedgerRow { ItemId = 3, Count = 7 });
|
||||
src.Add(new PendingSaveLedgerRow { ItemId = 5, Count = 12 });
|
||||
|
||||
var dest = em.GetBuffer<StorageEntry>(e);
|
||||
dest.Add(new StorageEntry { ItemId = 99, Count = 1 }); // pre-existing junk that must be cleared
|
||||
|
||||
SaveApply.WriteLedger(em.GetBuffer<PendingSaveLedgerRow>(e), em.GetBuffer<StorageEntry>(e));
|
||||
|
||||
var result = em.GetBuffer<StorageEntry>(e);
|
||||
Assert.AreEqual(2, result.Length);
|
||||
Assert.AreEqual(3, result[0].ItemId);
|
||||
Assert.AreEqual(7, result[0].Count);
|
||||
Assert.AreEqual(5, result[1].ItemId);
|
||||
Assert.AreEqual(12, result[1].Count);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void WriteLedger_EmptySource_ClearsDestination()
|
||||
{
|
||||
using var world = new World("SaveApplyEmptyTest");
|
||||
var em = world.EntityManager;
|
||||
var e = em.CreateEntity();
|
||||
em.AddBuffer<PendingSaveLedgerRow>(e);
|
||||
em.AddBuffer<StorageEntry>(e);
|
||||
var dest = em.GetBuffer<StorageEntry>(e);
|
||||
dest.Add(new StorageEntry { ItemId = 1, Count = 1 });
|
||||
|
||||
SaveApply.WriteLedger(em.GetBuffer<PendingSaveLedgerRow>(e), em.GetBuffer<StorageEntry>(e));
|
||||
|
||||
Assert.AreEqual(0, em.GetBuffer<StorageEntry>(e).Length);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f637165d4b6be6940b81f5832204e89d
|
||||
Reference in New Issue
Block a user