# GameCult CultCache Unity Tools `org.gamecult.caching.unity` is the Unity editor surface over CultCache: open a store, browse its documents, edit them, save. CultCache owns document identity, schema descriptors, persistence, dirty state and flush. `CultInspectorModel` (in `GameCult.Caching`) owns what an inspector shows and which edits it may make. The Studio window owns selection, the toolbar and IMGUI widgets. It references `GameCult.Caching` and `GameCult.Caching.MessagePack` at compile time through the `org.gamecult.cultlib` package (assembly `GameCult.CultLib`). ## Use 1. Add this package; it depends on `org.gamecult.cultlib`. 2. Open `GameCult/CultCache Studio`. 3. `Open` a `.cc` store. `Directory` opens a directory store (a manifest beside its `.records` folder; an existing one is detected without the toggle). `Read Only` opens the store read-only. `New` creates an empty writable single-file store; it is disabled while `Read Only` or `Directory` is on and refuses a path that already exists. 4. Pick a document type, then a record. Edits upsert into the cache as you make them; the toolbar shows `Dirty` until `Save` flushes the store. Game data stores do not live under the project's `Assets` folder: Unity would import and manage them, and CultCache exists so Unity does not manage game data. `New` refuses a path under `Assets`, `Open` warns about one, and the file dialogs start in the last store's folder or the project root. Document types come from the cache's `CultDocumentRegistry`. A type lists its own records and those of its subclasses, by `[CultName]` with the key as fallback. `Add`, `Duplicate` and `Delete` act on the selected type and record. A `[CultGlobal]` type with no record shows as absent with a `Create` action; the Studio never creates one on its own. A document class marked `[CultInspectorGroupBy(nameof(Kind), nameof(Owner))]` lists its records as foldouts nested in that order, one per value in use; a derived type listed under a marked base groups the same way. Each foldout has a `Create` button that makes a new record with the grouped members preset to that foldout's values, so it lands there. A declaration the model refuses (an unknown, hidden, read-only, repeated or ungroupable member, or a global type) lists the records flat under a warning that says why. A variant sits in the foldout its resolved values name. Drag a record row onto any record-reference value in the inspector (a member, a list element, a dictionary key or value) to set it. The drop is accepted exactly when the reference's picker would offer that record; it is refused in a read-only store, for the record the reference already holds, and for a record from another Studio window. The reference picker itself is not grouped. Opening a store never writes it, directory stores included. Closing or reloading scripts drops unsaved edits. ## Inspection Model `CultInspectorModel` is engine-free and lives beside the inspector attributes in `GameCult.Caching`; `CultCacheMessagePack.CreateInspectorModel` builds it over a `.cc` store's codec. It gives the member list of a type (registry catalog slots, `CultInspector*` metadata, whether a member is assignable), the shape of any value (string, integer, float, bool, enum, `CultRecordRef` with its candidate records, rank-1 list or array, dictionary, `[Union]` with only its declared subtypes, nested keyed object, struct edited in place through its public fields, or its settable properties when it has none, or unsupported with a reason), drawer claim resolution (`CultInspectorDrawerClaims`), and the edit decisions: the key a dictionary entry keeps and the notice when a key is refused (null, an empty record reference, a duplicate: serialized the same as another key under the owning document's serializer or equal by the rebuilt dictionary type's default comparer, or a key that document cannot serialize), the fresh key an added entry gets, what a new list element, dictionary value or union pick may be, and how an integer edit clamps to its type. Edits work on a copy (`CultInspectorEdit`): a refused upsert leaves the cached object unchanged. The Studio is one lowering of that model and owns only widgets and layout; a runtime CultUI panel needs only its own renderer and drawers to show and edit the same documents under the same rules. Grouping is the model's too: `GroupingOf` resolves a listed type's `[CultInspectorGroupBy]` members (or the notice for a refused declaration), `GroupRecords` returns the tree of nodes (id, label, depth, values, children, records) that a lowering draws, and `CreateInGroup` makes the record for a node. `IsRecordCandidate` is the one test for whether a record may be a reference's value, shared by the picker's candidates and a drop, and `RecordRefLabel` is the one spelling of `None`, a target's label and `Missing`. ## Drawers Built in: every model shape above. A struct without `[Key]` members is edited in place. With public fields it folds out to those fields only (readonly ones read-only), so aliasing properties such as `Quaternion.eulerAngles` or `Rect.min` add no rows; without public fields it folds out to its public properties with a setter, `init` ones read-only (record structs and CultMath matrices work as written). Get-only properties are never shown. Unity object references draw as object fields. A value no drawer claims and no shape covers shows a red error row with the model's reason, as does a multi-dimensional array. A project adds or overrides a drawer with an editor class. `Claimed` is either a value type (an open generic definition claims every closed form) or an attribute type, which claims every member carrying it. Attribute claims win over type claims, which win over built-in drawing. A claim two drawers make is used by neither: both are logged and the member shows the error row. A drawer returns the new value and lets its IMGUI controls set `GUI.changed`; only a frame that reports a change is saved. Mutating the value in place without reporting a change is not saved reliably. A drawer that throws shows an error row and discards that frame's edit copy, so nothing edited in that frame is saved. ```csharp using System; using System.Reflection; using GameCult.Caching; using GameCult.Unity.Caching.Editor; using UnityEditor; using UnityEngine; // Lives beside the documents; an attribute needs no Unity reference. public sealed class ColorAttribute : Attribute { } // Draws a "#RRGGBBAA" string member as a color field. [CultInspectorDrawer(typeof(ColorAttribute))] public sealed class ColorDrawer : ICultInspectorDrawer { public object Draw(CultInspector inspector, string label, Type type, object value, MemberInfo member) { if (type != typeof(string)) return inspector.DrawDefault(label, type, value, member); ColorUtility.TryParseHtmlString(value as string ?? string.Empty, out var color); var next = EditorGUILayout.ColorField(label, color); return next == color ? value : "#" + ColorUtility.ToHtmlStringRGBA(next); } } ``` `Draw` receives the value's declared type and the member it belongs to, also 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 them without referencing Unity. ```csharp using GameCult.Caching; using MessagePack; [MessagePackObject] [CultDocument("game.item", "game.item.v1")] public sealed class ItemData { [Key(0)] [CultName] [CultInspectorLabel("Display Name")] public string Name = string.Empty; [Key(1)] [CultInspectorRange(0, 999)] public int Value; [Key(2)] [CultInspectorTextArea] public string Notes = string.Empty; [Key(3)] [CultInspectorAssetGuid] public string IconGuid = string.Empty; } ``` - `CultInspectorLabel` - `CultInspectorHidden` - `CultInspectorReadOnly` - `CultInspectorOrder` - `CultInspectorTextArea` - `CultInspectorRange` - `CultInspectorAssetGuid` (a string member holding an engine asset's stable identity: in Unity, the `.meta` GUID for a main asset, or `guid[subAssetName]` when a sub-asset such as one sprite of a sheet is picked — the same key form `AssetReference` keeps and `Addressables.LoadAssetAsync` accepts; optional engine asset `Type`, named by the consumer; the Studio uses it when it is a `UnityEngine.Object` type, refuses to store a built-in resource's GUID since it has no Addressables entry (checked by asking AssetDatabase whether the picked object is a main or sub-asset under `Assets/` or `Packages/`, not by guessing from the GUID's hex digits), and never silently clears a stored value that no longer resolves — it warns and offers a Clear button instead) - `CultInspectorDrawer` (on drawer classes) CultMesh collaboration should feed the same CultCache mutation surface rather than becoming a second owner for document state.