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>
325 lines
14 KiB
C#
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();
|
|
}
|
|
}
|
|
}
|