using System.IO; using UnityEngine; using UnityEngine.SceneManagement; public partial class MeshPainter { // --- Persistence: save the painted maps to disk and reload them next session --- [Header("Persistence")] [Tooltip("Save the painted maps to disk on quit / play-stop and reload them on the next " + "start, so the painting persists across sessions. Off = paint is volatile (as before).")] public bool persistPaint = true; [Tooltip("Save file identifier. Empty = derived from the active scene + object name. Set " + "explicitly to keep a save stable across object renames or to share one save.")] public string saveId = ""; // Raw, bit-exact map dump: 'CHMP' magic + version + width + height + map count, then each // map's canonical Color32 bytes. Raw (not PNG) avoids any sRGB/alpha-premultiply ambiguity // and round-trips the exact canonical bytes ReadCanonical produces. private const uint PaintFileMagic = 0x504D4843; // 'CHMP' little-endian private const int PaintFileVersion = 1; private bool _savedOnTeardown; // guards against a double save across OnApplicationQuit + OnDestroy private static string PaintSaveDir => Path.Combine(Application.persistentDataPath, "MeshPaint"); private string PaintSavePath() { string key = string.IsNullOrEmpty(saveId) ? SanitizeKey(SceneManager.GetActiveScene().name + "_" + gameObject.name) : SanitizeKey(saveId); return Path.Combine(PaintSaveDir, key + ".paint"); } private static string SanitizeKey(string s) { foreach (char c in Path.GetInvalidFileNameChars()) s = s.Replace(c, '_'); return s.Replace(' ', '_'); } /// Reads all maps from the active backend (canonical layout) and writes them to the save file. public void SavePaint() { if (!persistPaint || !_setupValid) return; try { Directory.CreateDirectory(PaintSaveDir); int n = textureSize * textureSize; using (var fs = new FileStream(PaintSavePath(), FileMode.Create, FileAccess.Write)) using (var w = new BinaryWriter(fs)) { w.Write(PaintFileMagic); w.Write(PaintFileVersion); w.Write(textureSize); w.Write(textureSize); w.Write(MAP_COUNT); var bytes = new byte[n * 4]; for (int mi = 0; mi < MAP_COUNT; mi++) { Color32[] px = ReadCanonical(mi); int count = Mathf.Min(n, px.Length); for (int i = 0; i < count; i++) { int o = i * 4; bytes[o] = px[i].r; bytes[o + 1] = px[i].g; bytes[o + 2] = px[i].b; bytes[o + 3] = px[i].a; } w.Write(bytes, 0, count * 4); } } } catch (System.Exception e) { Debug.LogWarning($"[MeshPainter] Saving paint failed: {e.Message}", this); } } /// /// Loads the saved maps in canonical layout for seeding, or null if none / incompatible. /// Per-map null entries are tolerated by both backends (they fall back to the material seed). /// private Color32[][] LoadPaintMaps() { if (!persistPaint) return null; string path = PaintSavePath(); if (!File.Exists(path)) return null; try { using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read)) using (var r = new BinaryReader(fs)) { if (r.ReadUInt32() != PaintFileMagic) return null; int ver = r.ReadInt32(); int w = r.ReadInt32(); int h = r.ReadInt32(); int maps = r.ReadInt32(); // Resolution / version change -> ignore the save rather than smear mismatched // data; the brush re-seeds from the material instead. if (ver != PaintFileVersion || w != textureSize || h != textureSize) { Debug.LogWarning($"[MeshPainter] Saved paint ignored (got {w}x{h} v{ver}, expected " + $"{textureSize}x{textureSize} v{PaintFileVersion}). Re-seeding from material.", this); return null; } int n = textureSize * textureSize; var result = new Color32[MAP_COUNT][]; int load = Mathf.Min(maps, MAP_COUNT); for (int mi = 0; mi < load; mi++) { byte[] raw = r.ReadBytes(n * 4); if (raw.Length != n * 4) break; // truncated file -> remaining maps stay seeded var px = new Color32[n]; for (int i = 0; i < n; i++) { int o = i * 4; px[i] = new Color32(raw[o], raw[o + 1], raw[o + 2], raw[o + 3]); } result[mi] = px; } return result; } } catch (System.Exception e) { Debug.LogWarning($"[MeshPainter] Loading paint failed: {e.Message}", this); return null; } } /// Saves once during teardown (OnApplicationQuit and OnDestroy both call this; only the first writes). private void SavePaintOnTeardown() { if (_savedOnTeardown) return; _savedOnTeardown = true; SavePaint(); } /// Deletes this painter's save file (next start re-seeds from the material). Editor convenience. [ContextMenu("Clear Saved Paint")] public void ClearSavedPaint() { try { string path = PaintSavePath(); if (File.Exists(path)) File.Delete(path); Debug.Log($"[MeshPainter] Cleared saved paint: {path}", this); } catch (System.Exception e) { Debug.LogWarning($"[MeshPainter] Clearing saved paint failed: {e.Message}", this); } } }