CultLib/src/GameCult.Unity/Assets/Caching/Editor/CultCacheStudioWindow.cs
Meta Vens 8be174b82c Reference CultCache from the Studio at compile time; add drawers
Cut 6. CultCache Studio 1.0.0 -> 1.1.0.

Deleted: the CultCacheBridge string-reflection bridge (type lookups by
name, GetMethod/GetProperty invocation, PullOnOpen, FlushAsync(bool)),
its DescriptorInfo/CatalogMemberInfo/RecordInfo shadows, and the
disabled-text-field fallback in DrawValue. The window calls
CultCacheMessagePack.Create, UpsertAsync(Type, object, key), Remove,
FlushAsync, IsDirty, Registry.AllDescriptors, AllStoredDocuments and
BackingStores directly through GameCult.CultLib.

Window: single-file and directory stores, optional read-only open with
edits disabled and the reason shown, document types from the registry,
records by [CultName] with key fallback including subclasses, Add,
Duplicate (MessagePack round trip), Delete, immediate upsert on edit,
Save = flush, Dirty indicator. An absent [CultGlobal] shows as absent
with an explicit Create; nothing is invented. Opening and closing never
writes.

Drawers (new CultCacheStudioDrawers.cs): dictionaries with recursive
keys and values, abstract/interface members through their MessagePack
[Union] declarations only, CultRecordRef<T> record picker plus raw key,
CultMath float2/3/4, double2/3, int2, bool2, quaternion, rect, Color32,
enums including flags, lists and arrays with add/remove, nested keyed
types, a red error row for unclaimed types, and the
[CultInspectorDrawer(typeof(T))] extension point discovered through
TypeCache (ICultInspectorDrawer). Existing inspector attributes keep
working.

package.json depends on org.gamecult.cultlib 1.0.57 (Cut 7) and
org.gamecult.cultmath 0.1.2. No Unity.Mathematics reference.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 13:02:49 +02:00

325 lines
14 KiB
C#

using System;
using System.IO;
using System.Linq;
using GameCult.Caching;
using GameCult.Caching.MessagePack;
using UnityEditor;
using UnityEngine;
namespace GameCult.Unity.Caching.Editor
{
public sealed class CultCacheStudioWindow : EditorWindow
{
private const string LastPathKey = "GameCult.CultCacheStudio.LastPath";
private CultCache _cache;
private CultInspector _inspector;
private CultStoredDocument[] _records = Array.Empty<CultStoredDocument>();
private string _path = string.Empty;
private bool _directory;
private bool _readOnly;
private string _search = string.Empty;
private Type _selectedType;
private string _selectedKey;
private Vector2 _typeScroll;
private Vector2 _recordScroll;
private Vector2 _inspectorScroll;
private string _status = "Open a CultCache store to begin.";
private MessageType _statusType = MessageType.Info;
private bool ReadOnly => _cache.BackingStores.Any(store => store.IsReadOnly);
[MenuItem("GameCult/CultCache Studio")]
public static void Open()
{
GetWindow<CultCacheStudioWindow>("CultCache Studio");
}
private void OnEnable()
{
_path = EditorPrefs.GetString(LastPathKey, string.Empty);
}
// Disposing never flushes; unsaved edits are dropped on close and on script reload.
private void OnDisable()
{
CloseStore();
}
private void OnGUI()
{
DrawToolbar();
if (!string.IsNullOrEmpty(_status)) EditorGUILayout.HelpBox(_status, _statusType);
if (_cache == null) return;
if (ReadOnly)
EditorGUILayout.HelpBox("Read-only store: " + _path + " was opened read-only. Adding, deleting, editing and saving are disabled.", MessageType.Warning);
_records = _cache.AllStoredDocuments.ToArray();
_inspector.Records = _records;
using (new EditorGUILayout.HorizontalScope())
{
DrawTypes();
DrawRecords();
DrawInspector();
}
}
private void DrawToolbar()
{
using (new EditorGUILayout.HorizontalScope(EditorStyles.toolbar))
{
_path = GUILayout.TextField(_path, EditorStyles.toolbarTextField, GUILayout.MinWidth(180));
_directory = GUILayout.Toggle(_directory, "Directory", EditorStyles.toolbarButton, GUILayout.Width(62));
_readOnly = GUILayout.Toggle(_readOnly, "Read Only", EditorStyles.toolbarButton, GUILayout.Width(66));
if (GUILayout.Button("Open", EditorStyles.toolbarButton, GUILayout.Width(44))) Browse(false);
if (GUILayout.Button("New", EditorStyles.toolbarButton, GUILayout.Width(40))) Browse(true);
using (new EditorGUI.DisabledScope(_cache == null))
{
if (GUILayout.Button("Reload", EditorStyles.toolbarButton, GUILayout.Width(52)) && ConfirmDiscard()) OpenStore(_path, false);
using (new EditorGUI.DisabledScope(_cache == null || ReadOnly || !_cache.IsDirty))
{
if (GUILayout.Button("Save", EditorStyles.toolbarButton, GUILayout.Width(40))) Save();
}
if (GUILayout.Button("Close", EditorStyles.toolbarButton, GUILayout.Width(44)) && ConfirmDiscard())
{
CloseStore();
SetStatus("Closed " + _path + ".", MessageType.Info);
GUIUtility.ExitGUI();
}
}
GUILayout.FlexibleSpace();
if (_cache != null)
GUILayout.Label(ReadOnly ? "Read Only" : _cache.IsDirty ? "Dirty" : "Saved", EditorStyles.miniBoldLabel);
}
}
private void DrawTypes()
{
using (new EditorGUILayout.VerticalScope(GUILayout.Width(240)))
{
_search = EditorGUILayout.TextField(_search, EditorStyles.toolbarSearchField);
var counts = _records.GroupBy(r => r.Descriptor.DocumentType).ToDictionary(g => g.Key, g => g.Count());
_typeScroll = EditorGUILayout.BeginScrollView(_typeScroll);
foreach (var descriptor in _cache.Registry.AllDescriptors)
{
var type = descriptor.DocumentType;
if (_search.Length > 0 &&
descriptor.SchemaName.IndexOf(_search, StringComparison.OrdinalIgnoreCase) < 0 &&
type.Name.IndexOf(_search, StringComparison.OrdinalIgnoreCase) < 0)
continue;
var count = counts.Where(c => type.IsAssignableFrom(c.Key)).Sum(c => c.Value);
var label = descriptor.SchemaName + (descriptor.IsGlobal ? count == 0 ? " (absent)" : " (global)" : " (" + count + ")");
if (GUILayout.Toggle(_selectedType == type, label, EditorStyles.miniButton) && _selectedType != type)
{
_selectedType = type;
_selectedKey = null;
}
}
EditorGUILayout.EndScrollView();
}
}
private void DrawRecords()
{
using (new EditorGUILayout.VerticalScope(GUILayout.Width(260)))
{
var descriptor = _selectedType == null ? null : _cache.Registry.GetRequired(_selectedType);
var selected = Selected();
var constructible = _selectedType != null && _selectedType.GetConstructor(Type.EmptyTypes) != null;
using (new EditorGUILayout.HorizontalScope())
{
using (new EditorGUI.DisabledScope(descriptor == null || descriptor.IsGlobal || ReadOnly || !constructible))
{
if (GUILayout.Button("Add", EditorStyles.miniButtonLeft)) Add();
}
using (new EditorGUI.DisabledScope(selected == null || selected.Descriptor.IsGlobal || ReadOnly))
{
if (GUILayout.Button("Duplicate", EditorStyles.miniButtonMid)) Duplicate(selected);
}
using (new EditorGUI.DisabledScope(selected == null || ReadOnly))
{
if (GUILayout.Button("Delete", EditorStyles.miniButtonRight)) Delete(selected);
}
}
_recordScroll = EditorGUILayout.BeginScrollView(_recordScroll);
if (descriptor == null)
{
EditorGUILayout.LabelField("Select a document type.");
}
else
{
var records = _records.Where(r => _selectedType.IsAssignableFrom(r.Descriptor.DocumentType))
.Select(r => (Record: r, Label: CultInspector.RecordLabel(r)))
.OrderBy(r => r.Label, StringComparer.OrdinalIgnoreCase)
.ToArray();
if (descriptor.IsGlobal && records.Length == 0)
{
using (new EditorGUILayout.HorizontalScope())
{
EditorGUILayout.LabelField("Global is absent.");
using (new EditorGUI.DisabledScope(ReadOnly || !constructible))
{
if (GUILayout.Button("Create", GUILayout.Width(56))) Add();
}
}
}
foreach (var (record, label) in records)
{
var text = record.Descriptor.DocumentType == _selectedType ? label : label + " <" + record.Descriptor.DocumentType.Name + ">";
if (GUILayout.Toggle(_selectedKey == record.Key.Value, text, EditorStyles.miniButton)) _selectedKey = record.Key.Value;
}
}
EditorGUILayout.EndScrollView();
}
}
private void DrawInspector()
{
using (new EditorGUILayout.VerticalScope(GUILayout.ExpandWidth(true)))
{
var record = Selected();
if (record == null)
{
EditorGUILayout.LabelField("Select a record.");
return;
}
EditorGUILayout.LabelField(record.Descriptor.SchemaName + " " + record.Descriptor.DocumentType.Name, EditorStyles.boldLabel);
EditorGUILayout.SelectableLabel("Key " + record.Key.Value + " Stored " + record.StoredAt, EditorStyles.miniLabel, GUILayout.Height(EditorGUIUtility.singleLineHeight));
_inspectorScroll = EditorGUILayout.BeginScrollView(_inspectorScroll);
var readOnly = ReadOnly;
using (new EditorGUI.DisabledScope(readOnly))
{
EditorGUI.BeginChangeCheck();
_inspector.DrawDocument(record);
if (EditorGUI.EndChangeCheck() && !readOnly)
Run(null, () => _cache.UpsertAsync(record.Descriptor.DocumentType, record.Document, record.Key).GetAwaiter().GetResult());
}
EditorGUILayout.EndScrollView();
}
}
private CultStoredDocument Selected()
{
return _selectedKey == null ? null : _records.FirstOrDefault(r => r.Key.Value == _selectedKey);
}
private void Browse(bool create)
{
if (!ConfirmDiscard()) return;
var directory = string.IsNullOrEmpty(_path) ? Application.dataPath : Path.GetDirectoryName(_path);
var path = create
? EditorUtility.SaveFilePanel("New CultCache store", directory, "GameData", "cc")
: EditorUtility.OpenFilePanel("Open CultCache store", directory, _directory ? "" : "cc");
if (!string.IsNullOrEmpty(path)) OpenStore(path, create);
GUIUtility.ExitGUI();
}
private void OpenStore(string path, bool create)
{
CloseStore();
if (!create && !File.Exists(path))
{
SetStatus("No store at " + path + ".", MessageType.Error);
return;
}
try
{
var existed = File.Exists(path);
_cache = CultCacheMessagePack.Create(path, new CultCacheOpenOptions { ReadOnly = _readOnly, UseDirectoryStore = _directory });
if (create && !existed && !_readOnly) _cache.BackingStores[0].PushAll();
_inspector = new CultInspector(_cache);
_path = path;
EditorPrefs.SetString(LastPathKey, path);
SetStatus((create && !existed ? "Created " : "Opened ") + path + (_cache.BackingStores[0] is DirectoryMessagePackBackingStore ? " (directory store)." : "."), MessageType.Info);
}
catch (Exception exception)
{
CloseStore();
SetStatus("Failed to open " + path + ": " + exception.GetBaseException().Message, MessageType.Error);
}
}
private void CloseStore()
{
_cache?.Dispose();
_cache = null;
_inspector = null;
_records = Array.Empty<CultStoredDocument>();
_selectedKey = null;
}
private bool ConfirmDiscard()
{
return _cache == null || !_cache.IsDirty ||
EditorUtility.DisplayDialog("CultCache Studio", "Discard unsaved changes to " + _path + "?", "Discard", "Cancel");
}
private void Save()
{
Run("Saved " + _path + ".", () => _cache.FlushAsync().GetAwaiter().GetResult());
}
private void Add()
{
var type = _selectedType;
Run("Added " + type.Name + ".", () => _selectedKey = _cache.UpsertAsync(type, Activator.CreateInstance(type)).GetAwaiter().GetResult().Value);
GUIUtility.ExitGUI();
}
private void Duplicate(CultStoredDocument record)
{
var type = record.Descriptor.DocumentType;
Run("Duplicated " + CultInspector.RecordLabel(record) + ".", () =>
{
var bytes = CultDocumentMessagePackSerialization.SerializeUntyped(record.Document, type, _cache.Registry);
var copy = CultDocumentMessagePackSerialization.DeserializeUntyped(type, bytes, _cache.Registry);
_selectedKey = _cache.UpsertAsync(type, copy).GetAwaiter().GetResult().Value;
});
GUIUtility.ExitGUI();
}
private void Delete(CultStoredDocument record)
{
var label = CultInspector.RecordLabel(record);
if (!EditorUtility.DisplayDialog("CultCache Studio", "Delete " + label + "?", "Delete", "Cancel")) return;
Run("Deleted " + label + ".", () =>
{
_cache.Remove(record.Key);
_selectedKey = null;
});
GUIUtility.ExitGUI();
}
private void Run(string success, Action action)
{
try
{
action();
if (success != null) SetStatus(success, MessageType.Info);
}
catch (Exception exception)
{
SetStatus(exception.GetBaseException().Message, MessageType.Error);
}
}
private void SetStatus(string message, MessageType type)
{
_status = message;
_statusType = type;
Repaint();
}
}
}