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(); 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("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(); _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(); } } }