Operator ruling: the heuristic that rebuilt unkeyed structs through a guessed constructor is removed, not guarded. Gone: the Composite value kind, CultInspectorShape.Composer, CultInspectorModel.Compose and Composite, the composite-component flag on CultInspectorMember, the IMGUI composite rebuild path, and the README text about it. An unkeyed struct is now edited in place through its public fields. A struct holding state it cannot write in place (a public readonly field or a get-only auto-property) is Unsupported with a reason naming the member, until a drawer claims it. Computed get-only properties are not state and do not lock a struct. The composite-rejection test goes with the heuristic; the replacement test pins both lock rules and fails with either removed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
5.8 KiB
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
- Add this package; it depends on
org.gamecult.cultlib. - Open
GameCult/CultCache Studio. Opena.ccstore.Directoryopens a directory store (a manifest beside its.recordsfolder; an existing one is detected without the toggle).Read Onlyopens the store read-only.Newcreates an empty writable single-file store; it is disabled whileRead OnlyorDirectoryis on and refuses a path that already exists.- Pick a document type, then a record. Edits upsert into the cache as you make
them; the toolbar shows
DirtyuntilSaveflushes 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.
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. Given a registry and the store's document 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<T> 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 unsupported with
a reason), drawer claim resolution
(CultInspectorDrawerClaims), dictionary key identity and refusal (null keys,
empty record references and duplicates), and edits that 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.
Drawers
Built in: every model shape above. A struct without [Key] members folds out
to its public fields and is edited in place; a struct with a readonly field or
a get-only auto-property is unsupported until a drawer claims it. 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
that throws draws an error row and leaves the value unchanged.
using System;
using System.Reflection;
using GameCult.Caching;
using GameCult.Unity.Caching.Editor;
using UnityEditor;
using UnityEngine;
public sealed class ColorAttribute : Attribute
{
}
[CultInspectorDrawer(typeof(ColorAttribute))]
public sealed class ColorDrawer : ICultInspectorDrawer
{
public object Draw(CultInspector inspector, string label, Type type, object value, MemberInfo member)
{
if (type != typeof(CultMath.float3))
return inspector.DrawDefault(label, type, value, member);
var v = (CultMath.float3)value;
var c = EditorGUILayout.ColorField(label, new Color(v.x, v.y, v.z));
return new CultMath.float3(c.r, c.g, c.b);
}
}
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 Annotations
The annotations live in GameCult.Caching, so headless shared models carry
them without referencing Unity.
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)]
[CultInspectorAssetPath]
public string IconPath = string.Empty;
}
CultInspectorLabelCultInspectorHiddenCultInspectorReadOnlyCultInspectorOrderCultInspectorTextAreaCultInspectorRangeCultInspectorAssetPath(optional engine assetType, named by the consumer; the Studio uses it when it is aUnityEngine.Objecttype)CultInspectorDrawer(on drawer classes)
CultMesh collaboration should feed the same CultCache mutation surface rather than becoming a second owner for document state.