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,116 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ProjectM.Client
|
||||
{
|
||||
/// <summary>
|
||||
/// Client-local settings persistence + application. Loads <c>settings.json</c> from
|
||||
/// <c>Application.persistentDataPath</c> at boot (<see cref="RuntimeInitializeOnLoadMethod"/>) and applies
|
||||
/// Graphics (Screen / QualitySettings / targetFrameRate) + Audio (<c>AudioListener.volume = Master</c>;
|
||||
/// <see cref="GameVolume"/> Music/Sfx bus trims). The single source of truth for the UITK
|
||||
/// <c>SettingsScreen</c> shared by the main menu + the in-game pause overlay. Saves are atomic
|
||||
/// (temp file + <c>File.Replace</c>). <c>JsonUtility</c> keeps it asmdef-ref-free. Never replicated.
|
||||
/// </summary>
|
||||
public static class SettingsService
|
||||
{
|
||||
public static GameSettings Current { get; private set; } = GameSettings.Defaults();
|
||||
|
||||
static string FilePath => Path.Combine(Application.persistentDataPath, "settings.json");
|
||||
|
||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
|
||||
static void Boot()
|
||||
{
|
||||
Load();
|
||||
Apply(Current);
|
||||
}
|
||||
|
||||
/// <summary>Read settings from disk (or defaults if absent/corrupt). Returns the loaded value.</summary>
|
||||
public static GameSettings Load()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(FilePath))
|
||||
{
|
||||
var json = File.ReadAllText(FilePath);
|
||||
var s = JsonUtility.FromJson<GameSettings>(json);
|
||||
if (s.Version != GameSettings.CurrentVersion)
|
||||
s = Migrate(s);
|
||||
Current = s.Clamped();
|
||||
}
|
||||
else
|
||||
{
|
||||
Current = GameSettings.Defaults();
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogWarning($"[SettingsService] Load failed ({e.Message}); using defaults.");
|
||||
Current = GameSettings.Defaults();
|
||||
}
|
||||
return Current;
|
||||
}
|
||||
|
||||
/// <summary>Clamp + version-stamp + atomically write to disk; updates <see cref="Current"/>.</summary>
|
||||
public static void Save(GameSettings settings)
|
||||
{
|
||||
settings = settings.Clamped();
|
||||
settings.Version = GameSettings.CurrentVersion;
|
||||
Current = settings;
|
||||
try
|
||||
{
|
||||
var json = JsonUtility.ToJson(settings, true);
|
||||
var tmp = FilePath + ".tmp";
|
||||
File.WriteAllText(tmp, json);
|
||||
if (File.Exists(FilePath)) File.Replace(tmp, FilePath, null);
|
||||
else File.Move(tmp, FilePath);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogWarning($"[SettingsService] Save failed: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Apply settings to the live engine. Master rides AudioListener.volume; buses are per-call trims.</summary>
|
||||
public static void Apply(GameSettings s)
|
||||
{
|
||||
s = s.Clamped();
|
||||
|
||||
// Graphics
|
||||
QualitySettings.SetQualityLevel(s.QualityLevel, true);
|
||||
QualitySettings.vSyncCount = s.VSync;
|
||||
var mode = (FullScreenMode)s.FullScreenMode;
|
||||
if (s.RefreshHz > 0)
|
||||
Screen.SetResolution(s.ResWidth, s.ResHeight, mode, new RefreshRate { numerator = (uint)s.RefreshHz, denominator = 1 });
|
||||
else
|
||||
Screen.SetResolution(s.ResWidth, s.ResHeight, mode);
|
||||
Application.targetFrameRate = s.TargetFps; // -1 = uncapped (ignored while vSync > 0)
|
||||
|
||||
// Audio
|
||||
AudioListener.volume = s.Master;
|
||||
GameVolume.Music = s.Music;
|
||||
GameVolume.Sfx = s.Sfx;
|
||||
}
|
||||
|
||||
/// <summary>Convenience for the settings UI: persist then apply.</summary>
|
||||
public static void SaveAndApply(GameSettings s)
|
||||
{
|
||||
Save(s);
|
||||
Apply(Current);
|
||||
}
|
||||
|
||||
// Forward-compatible migration: fill from current defaults, preserve any recognizable old values.
|
||||
// Additive-only as the schema grows (never throws on an unknown version).
|
||||
static GameSettings Migrate(GameSettings old)
|
||||
{
|
||||
var def = GameSettings.Defaults();
|
||||
if (old.ResWidth > 0) def.ResWidth = old.ResWidth;
|
||||
if (old.ResHeight > 0) def.ResHeight = old.ResHeight;
|
||||
if (old.Master > 0f) def.Master = old.Master;
|
||||
if (old.Music > 0f) def.Music = old.Music;
|
||||
if (old.Sfx > 0f) def.Sfx = old.Sfx;
|
||||
def.Version = GameSettings.CurrentVersion;
|
||||
return def;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user