From b00c415c8c280ff195b12c93242408d40b872f64 Mon Sep 17 00:00:00 2001 From: MelekTaus Moribundus Date: Sun, 28 Jun 2026 00:51:59 +0200 Subject: [PATCH] Fix HUD persistence breaking across additive scene switches The HUD save key was derived from the active scene name, which now changes across the additive lobby/map loads (Main -> Lobby -> Test Map). Save and restore landed in different files, so settings appeared lost. Derive the key from the HUD's own (stable, persistent) scene instead. Brush settings also live on the avatar's painter, which is despawned and respawned on every scene switch, resetting them to prefab defaults. Mirror the brush values into an in-memory snapshot on the persistent HUD and seed the freshly bound painter from it, so in-session edits carry across the respawn. Co-Authored-By: Claude Opus 4.8 --- Assets/Scripts/PaintHud.Persistence.cs | 144 +++++++++++++++++-------- Assets/Scripts/PaintHud.cs | 8 +- 2 files changed, 105 insertions(+), 47 deletions(-) diff --git a/Assets/Scripts/PaintHud.Persistence.cs b/Assets/Scripts/PaintHud.Persistence.cs index 6d300b7..e4a5d15 100644 --- a/Assets/Scripts/PaintHud.Persistence.cs +++ b/Assets/Scripts/PaintHud.Persistence.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.IO; using UnityEngine; -using UnityEngine.SceneManagement; public partial class PaintHud { @@ -13,25 +12,40 @@ public partial class PaintHud // and scalars), so it goes to a human-readable JSON file - unlike the painter's bulk pixel maps // (MeshPainter.Persistence) which use a raw byte dump. The two saves are independent: this one // owns the HUD layout + brush values, the painter owns the canvas pixels. + // + // The brush values are also mirrored into an in-memory snapshot (_carry) on this persistent HUD, so + // they survive the avatar respawn that a scene switch (lobby -> map) triggers: the new painter gets + // the carried values instead of its prefab defaults. The save key is derived from the HUD's OWN + // scene (a stable persistent scene), not the active scene - the latter now changes across the + // additive lobby/map loads, which would split save and restore into different files. [Header("Persistence")] [Tooltip("Save the panel positions and their values on quit / play-stop and restore them on the " + "next start. Off = the HUD opens at its authored layout / painter defaults every session.")] public bool persistHud = true; - [Tooltip("Save file identifier. Empty = derived from the active scene name. Set explicitly to keep " + + [Tooltip("Save file identifier. Empty = derived from the HUD's own scene name. Set explicitly to keep " + "the layout stable across scene renames or to share one layout between scenes.")] public string hudSaveId = ""; private const int HudFileVersion = 1; private bool _hudSavedOnTeardown; // guards a double save across OnApplicationQuit + OnDestroy + // In-memory mirror of the brush values, kept on this persistent HUD so they survive the avatar + // respawn a scene switch (lobby <-> map) triggers: the freshly spawned painter is seeded with these + // instead of its prefab defaults. Single instance with mutable fields -> no per-frame allocation. + private readonly HudValues _carry = new HudValues(); + private bool _hasCarry; + private static string HudSaveDir => Path.Combine(Application.persistentDataPath, "MeshPaint"); private string HudSavePath() { + // Derive from the HUD's OWN scene (a stable persistent scene), not the active scene: the latter + // now changes across the additive lobby/map loads, so a key from it would split save and restore + // into different files and silently lose the saved layout. string key = string.IsNullOrEmpty(hudSaveId) - ? SanitizeHudKey(SceneManager.GetActiveScene().name + "_hud") + ? SanitizeHudKey(gameObject.scene.name + "_hud") : SanitizeHudKey(hudSaveId); return Path.Combine(HudSaveDir, key + ".hudlayout"); } @@ -101,28 +115,14 @@ public partial class PaintHud if (painter) { + CopyValues(painter, state.values); + state.hasValues = true; + } + else if (_hasCarry) + { + // No painter bound at teardown (e.g. quit with no avatar) -> persist the last mirrored values. + state.values = _carry; state.hasValues = true; - var v = state.values; - v.paintAlbedo = painter.paintAlbedo; - v.paintMetallicSmoothness = painter.paintMetallicSmoothness; - v.paintOcclusion = painter.paintOcclusion; - v.paintEmission = painter.paintEmission; - v.spray = painter.spray; - v.brushColor = painter.brushColor; - v.metallic = painter.metallic; - v.smoothness = painter.smoothness; - v.metallicSmoothnessOpacity = painter.metallicSmoothnessOpacity; - v.occlusion = painter.occlusion; - v.occlusionOpacity = painter.occlusionOpacity; - v.emissionColor = painter.emissionColor; - v.emissionIntensity = painter.emissionIntensity; - v.brushSize = painter.brushSize; - v.activeBrushIndex = painter.activeBrushIndex; - v.sprayDots = painter.sprayDots; - v.sprayDotSizeMin = painter.sprayDotSizeMin; - v.sprayDotSizeMax = painter.sprayDotSizeMax; - v.sprayDotAlphaMin = painter.sprayDotAlphaMin; - v.sprayDotAlphaMax = painter.sprayDotAlphaMax; } Directory.CreateDirectory(HudSaveDir); @@ -148,27 +148,7 @@ public partial class PaintHud if (painter && state.hasValues && state.values != null) { - var v = state.values; - painter.paintAlbedo = v.paintAlbedo; - painter.paintMetallicSmoothness = v.paintMetallicSmoothness; - painter.paintOcclusion = v.paintOcclusion; - painter.paintEmission = v.paintEmission; - painter.spray = v.spray; - painter.brushColor = v.brushColor; - painter.metallic = v.metallic; - painter.smoothness = v.smoothness; - painter.metallicSmoothnessOpacity = v.metallicSmoothnessOpacity; - painter.occlusion = v.occlusion; - painter.occlusionOpacity = v.occlusionOpacity; - painter.emissionColor = v.emissionColor; - painter.emissionIntensity = v.emissionIntensity; - painter.brushSize = v.brushSize; - painter.activeBrushIndex = v.activeBrushIndex; - painter.sprayDots = v.sprayDots; - painter.sprayDotSizeMin = v.sprayDotSizeMin; - painter.sprayDotSizeMax = v.sprayDotSizeMax; - painter.sprayDotAlphaMin = v.sprayDotAlphaMin; - painter.sprayDotAlphaMax = v.sprayDotAlphaMax; + ApplyValues(painter, state.values); SyncFromPainter(); } @@ -181,6 +161,80 @@ public partial class PaintHud rt.anchoredPosition = p.pos; } + // --- In-session carry: brush values survive the avatar respawn a scene switch triggers --- + + private void LateUpdate() + { + // Mirror the live painter every frame; the next BindPainter (after a respawn) seeds the fresh + // painter from this snapshot instead of its prefab defaults. Captured while the painter lives, so + // it is immune to the destroy-pending timing of the despawn. + if (painter) CaptureCarry(); + } + + /// Snapshots the bound painter's brush values into the in-memory mirror. + public void CaptureCarry() + { + if (!painter) return; + CopyValues(painter, _carry); + _hasCarry = true; + } + + /// Writes the mirrored brush values onto the (freshly bound) painter and pushes them to the widgets. + public void ApplyCarry() + { + if (!painter || !_hasCarry) return; + ApplyValues(painter, _carry); + SyncFromPainter(); + } + + private static void CopyValues(MeshPainter from, HudValues v) + { + v.paintAlbedo = from.paintAlbedo; + v.paintMetallicSmoothness = from.paintMetallicSmoothness; + v.paintOcclusion = from.paintOcclusion; + v.paintEmission = from.paintEmission; + v.spray = from.spray; + v.brushColor = from.brushColor; + v.metallic = from.metallic; + v.smoothness = from.smoothness; + v.metallicSmoothnessOpacity = from.metallicSmoothnessOpacity; + v.occlusion = from.occlusion; + v.occlusionOpacity = from.occlusionOpacity; + v.emissionColor = from.emissionColor; + v.emissionIntensity = from.emissionIntensity; + v.brushSize = from.brushSize; + v.activeBrushIndex = from.activeBrushIndex; + v.sprayDots = from.sprayDots; + v.sprayDotSizeMin = from.sprayDotSizeMin; + v.sprayDotSizeMax = from.sprayDotSizeMax; + v.sprayDotAlphaMin = from.sprayDotAlphaMin; + v.sprayDotAlphaMax = from.sprayDotAlphaMax; + } + + private static void ApplyValues(MeshPainter to, HudValues v) + { + to.paintAlbedo = v.paintAlbedo; + to.paintMetallicSmoothness = v.paintMetallicSmoothness; + to.paintOcclusion = v.paintOcclusion; + to.paintEmission = v.paintEmission; + to.spray = v.spray; + to.brushColor = v.brushColor; + to.metallic = v.metallic; + to.smoothness = v.smoothness; + to.metallicSmoothnessOpacity = v.metallicSmoothnessOpacity; + to.occlusion = v.occlusion; + to.occlusionOpacity = v.occlusionOpacity; + to.emissionColor = v.emissionColor; + to.emissionIntensity = v.emissionIntensity; + to.brushSize = v.brushSize; + to.activeBrushIndex = v.activeBrushIndex; + to.sprayDots = v.sprayDots; + to.sprayDotSizeMin = v.sprayDotSizeMin; + to.sprayDotSizeMax = v.sprayDotSizeMax; + to.sprayDotAlphaMin = v.sprayDotAlphaMin; + to.sprayDotAlphaMax = v.sprayDotAlphaMax; + } + private HudState LoadHudState() { string path = HudSavePath(); diff --git a/Assets/Scripts/PaintHud.cs b/Assets/Scripts/PaintHud.cs index 3e10ee5..1bae6ac 100644 --- a/Assets/Scripts/PaintHud.cs +++ b/Assets/Scripts/PaintHud.cs @@ -156,8 +156,12 @@ public partial class PaintHud : MonoBehaviour // Re-sync every widget when the pipette adopts a sampled material (painter -> HUD). painter.Picked += SyncFromPainter; - // Restore the saved panel positions + widget values now that the painter exists. - RestoreHudState(); + // First bind this session: restore panel positions + brush values from disk (cross-session). + // A later bind is an avatar respawn (scene switch) -> carry the in-session brush values onto the + // fresh painter instead, so they are not reset to its prefab defaults. + if (_hasCarry) ApplyCarry(); + else RestoreHudState(); + CaptureCarry(); // seed/refresh the mirror from the now-current painter UpdateButtonStates(); // enable Save now that a painter is bound }