diff --git a/src/GameCult.Caching/CultInspectorModel.cs b/src/GameCult.Caching/CultInspectorModel.cs index c457031f..aaa7bbaf 100644 --- a/src/GameCult.Caching/CultInspectorModel.cs +++ b/src/GameCult.Caching/CultInspectorModel.cs @@ -238,14 +238,24 @@ namespace GameCult.Caching // A private copy of one stored document. A lowering draws into Document; the cached object is never touched. public sealed class CultInspectorEdit { - internal CultInspectorEdit(CultStoredDocument source, object document) + private readonly Func _snapshot; + private object? _record; + + internal CultInspectorEdit(CultStoredDocument source, object document, Func snapshot) { Source = source; Document = document; + _snapshot = snapshot; } public CultStoredDocument Source { get; } public object Document { get; } + + // The record being drawn, as stored when this edit began, for drawers that read the document they sit in. It is a + // second copy, made on first read: not the cached object, and not Document, which a lowering commits. Nothing + // reads it back, so writing to it saves nothing and changes neither the edit nor the cache. + public object Record => _record ??= _snapshot(); + public bool IsSpent { get; private set; } public bool IsFor(CultStoredDocument record) => !IsSpent && ReferenceEquals(Source, record); @@ -342,7 +352,8 @@ namespace GameCult.Caching public CultInspectorEdit BeginEdit(CultStoredDocument record) { if (record == null) throw new ArgumentNullException(nameof(record)); - return new CultInspectorEdit(record, Clone(record.Document, record.Descriptor.DocumentType)); + var type = record.Descriptor.DocumentType; + return new CultInspectorEdit(record, Clone(record.Document, type), () => Clone(record.Document, type)); } // Whether CreateDefault makes a value: a string, struct, list or dictionary, or a concrete class with a parameterless constructor. diff --git a/src/GameCult.Unity/Assets/Caching/Editor/CultCacheStudioDrawers.cs b/src/GameCult.Unity/Assets/Caching/Editor/CultCacheStudioDrawers.cs index 12eed25e..a421a476 100644 --- a/src/GameCult.Unity/Assets/Caching/Editor/CultCacheStudioDrawers.cs +++ b/src/GameCult.Unity/Assets/Caching/Editor/CultCacheStudioDrawers.cs @@ -16,7 +16,7 @@ namespace GameCult.Unity.Caching.Editor // frame's edit copy, so nothing it or any other drawer did that frame is saved. type is the value's declared type. // member is the member the value belongs to, also for list elements and dictionary keys and values, and null for a // bare value. inspector.DrawDefault hands the value back to built-in drawing; inspector.DrawValue draws a sub-value - // with claims. + // with claims. inspector.Record is the record being drawn, read-only: a copy nothing saves. public interface ICultInspectorDrawer { object Draw(CultInspector inspector, string label, Type type, object value, MemberInfo member); @@ -33,7 +33,7 @@ namespace GameCult.Unity.Caching.Editor private readonly Dictionary _foldouts = new Dictionary(StringComparer.Ordinal); private readonly Dictionary _notices = new Dictionary(StringComparer.Ordinal); private string _path = string.Empty; - private Type _document; + private CultInspectorEdit _edit; private bool _drawerFailed; internal CultInspector(CultInspectorModel model) @@ -45,6 +45,9 @@ namespace GameCult.Unity.Caching.Editor public IReadOnlyList Records { get; internal set; } = Array.Empty(); + // The document being drawn, as the model's CultInspectorEdit.Record: a copy for reading, never saved. + public object Record => _edit?.Record; + private static GUIStyle ErrorStyle => _errorStyle ??= new GUIStyle(EditorStyles.label) { normal = { textColor = new Color(1f, .35f, .3f) } }; private static CultInspectorDrawerClaims Claims @@ -62,7 +65,7 @@ namespace GameCult.Unity.Caching.Editor internal bool DrawDocument(CultInspectorEdit edit) { _drawerFailed = false; - _document = edit.Document.GetType(); + _edit = edit; DrawMembers(edit.Document, edit.Source.Descriptor.DocumentType, edit.Source.Key.Value); return !_drawerFailed; } @@ -242,7 +245,7 @@ namespace GameCult.Unity.Caching.Editor var item = DrawValue("Value", shape.ValueType, entries[i].Value, member, path + "{" + i + "}.value"); if (EditorGUI.EndChangeCheck()) { - key = Model.ReplaceKey(_document, shape.Type, entries.Select(e => e.Key).ToArray(), i, key, out var notice); + key = Model.ReplaceKey(_edit.Document.GetType(),shape.Type, entries.Select(e => e.Key).ToArray(), i, key, out var notice); Notice(path, notice); entries[i] = new KeyValuePair(key, item); changed = true; @@ -261,7 +264,7 @@ namespace GameCult.Unity.Caching.Editor if (Add(shape.ValueType, path, out var created)) { - var fresh = Model.FreshKey(_document, shape.Type, entries.Select(e => e.Key).ToArray(), Records, out var notice); + var fresh = Model.FreshKey(_edit.Document.GetType(),shape.Type, entries.Select(e => e.Key).ToArray(), Records, out var notice); Notice(path, notice); if (fresh != null) { diff --git a/src/GameCult.Unity/Assets/Caching/README.md b/src/GameCult.Unity/Assets/Caching/README.md index a5ea3d92..d96f674f 100644 --- a/src/GameCult.Unity/Assets/Caching/README.md +++ b/src/GameCult.Unity/Assets/Caching/README.md @@ -114,6 +114,15 @@ for list elements and dictionary keys and values, so an attribute drawer checks the type it is handed. `inspector.DrawDefault` hands a value back to built-in drawing; `inspector.DrawValue` draws a sub-value with claims applied. +`inspector.Record` is the whole document the value sits in, for drawers that +read sibling members (a texture path, a hull's tints). It is read-only by +contract: the model's `CultInspectorEdit.Record`, a second copy of the stored +document as the edit began, made on first read. It is neither the cached +object nor the edit copy the Studio saves, so writing to it saves nothing; an +edit reaches the record only through the value `Draw` returns. Within one frame +it does not show edits other drawers made earlier in that frame; after a save +the next frame's edit starts from the saved record. + ## Inspector Annotations The annotations live in `GameCult.Caching`, so headless shared models carry diff --git a/tests/GameCult.Caching.Tests/CultInspectorModelTests.cs b/tests/GameCult.Caching.Tests/CultInspectorModelTests.cs index c5fd8cb1..1de5eaf8 100644 --- a/tests/GameCult.Caching.Tests/CultInspectorModelTests.cs +++ b/tests/GameCult.Caching.Tests/CultInspectorModelTests.cs @@ -216,6 +216,11 @@ namespace GameCult.Caching.Tests var record = readOnly.AllStoredDocuments.Single(); var edit = model.BeginEdit(record); Assert.That(edit.Document, Is.Not.SameAs(record.Document)); + var drawn = (InspectItem)edit.Record; + Assert.That(drawn, Is.Not.SameAs(record.Document).And.Not.SameAs(edit.Document)); + Assert.That(edit.Record, Is.SameAs(drawn), "one record copy per edit"); + drawn.Name = "a drawer wrote here"; + Assert.That(((InspectItem)edit.Document).Name, Is.EqualTo("original"), "the record copy is not the committed copy"); ((InspectItem)edit.Document).Name = "changed"; ((InspectItem)edit.Document).Numbers.Add(3);