Replace the asset path attribute with a stable GUID identity

CultInspectorAssetPathAttribute stored an engine asset path, which breaks
when the asset moves. CultInspectorAssetGuidAttribute stores the asset's
stable identity (in Unity, the .meta GUID) instead. The Studio drawer now
resolves the stored GUID to a path only to load the preview object, and
writes back AssetPathToGUID on a change. No path form survives for the
next field to pick up.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-09-17 18:43:03 +02:00
commit 95a3c32250
5 changed files with 34 additions and 14 deletions

View file

@ -64,12 +64,13 @@ namespace GameCult.Caching
public float Max { get; }
}
// A string member holding an engine asset path. AssetType is the engine's asset type, named by the consumer
// (typeof(UnityEngine.Texture2D) in a Unity assembly); a lowering that does not know the type ignores it.
// A string member holding an engine asset's stable identity; in Unity, the `.meta` GUID. AssetType is the
// engine's asset type, named by the consumer (typeof(UnityEngine.Texture2D) in a Unity assembly); a lowering
// that does not know the type ignores it.
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = true)]
public sealed class CultInspectorAssetPathAttribute : Attribute
public sealed class CultInspectorAssetGuidAttribute : Attribute
{
public CultInspectorAssetPathAttribute(Type? assetType = null)
public CultInspectorAssetGuidAttribute(Type? assetType = null)
{
AssetType = assetType;
}

View file

@ -41,7 +41,7 @@ namespace GameCult.Caching
Order = Find<CultInspectorOrderAttribute>()?.Order;
Range = Find<CultInspectorRangeAttribute>();
TextArea = Find<CultInspectorTextAreaAttribute>();
AssetPath = Find<CultInspectorAssetPathAttribute>();
AssetGuid = Find<CultInspectorAssetGuidAttribute>();
}
public IReadOnlyList<Attribute> Attributes { get; }
@ -53,7 +53,7 @@ namespace GameCult.Caching
public int? Order { get; }
public CultInspectorRangeAttribute? Range { get; }
public CultInspectorTextAreaAttribute? TextArea { get; }
public CultInspectorAssetPathAttribute? AssetPath { get; }
public CultInspectorAssetGuidAttribute? AssetGuid { get; }
public T? Find<T>() where T : Attribute => Attributes.OfType<T>().FirstOrDefault();
}

View file

@ -167,12 +167,13 @@ namespace GameCult.Unity.Caching.Editor
private static string DrawString(string label, string value, CultInspectorMetadata metadata)
{
if (metadata.AssetPath != null)
if (metadata.AssetGuid != null)
{
var assetType = metadata.AssetPath.AssetType != null && typeof(Object).IsAssignableFrom(metadata.AssetPath.AssetType) ? metadata.AssetPath.AssetType : typeof(Object);
var asset = string.IsNullOrEmpty(value) ? null : AssetDatabase.LoadAssetAtPath(value, assetType);
var assetType = metadata.AssetGuid.AssetType != null && typeof(Object).IsAssignableFrom(metadata.AssetGuid.AssetType) ? metadata.AssetGuid.AssetType : typeof(Object);
var path = string.IsNullOrEmpty(value) ? string.Empty : AssetDatabase.GUIDToAssetPath(value);
var asset = string.IsNullOrEmpty(path) ? null : AssetDatabase.LoadAssetAtPath(path, assetType);
var next = EditorGUILayout.ObjectField(label, asset, assetType, false);
return next == asset ? value : next == null ? string.Empty : AssetDatabase.GetAssetPath(next);
return next == asset ? value : next == null ? string.Empty : AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(next));
}
var textArea = metadata.TextArea;

View file

@ -150,8 +150,8 @@ public sealed class ItemData
public string Notes = string.Empty;
[Key(3)]
[CultInspectorAssetPath]
public string IconPath = string.Empty;
[CultInspectorAssetGuid]
public string IconGuid = string.Empty;
}
```
@ -161,8 +161,9 @@ public sealed class ItemData
- `CultInspectorOrder`
- `CultInspectorTextArea`
- `CultInspectorRange`
- `CultInspectorAssetPath` (optional engine asset `Type`, named by the
consumer; the Studio uses it when it is a `UnityEngine.Object` type)
- `CultInspectorAssetGuid` (a string member holding an engine asset's stable
identity, in Unity the `.meta` GUID; optional engine asset `Type`, named by
the consumer; the Studio uses it when it is a `UnityEngine.Object` type)
- `CultInspectorDrawer` (on drawer classes)
CultMesh collaboration should feed the same CultCache mutation surface rather

View file

@ -42,6 +42,15 @@ namespace GameCult.Caching.Tests
Assert.That(hidden.ValueType, Is.EqualTo(typeof(string)));
}
[Test]
public void AssetGuidMemberExposesItsAttributeAndDeclaredType()
{
var member = Model().MembersOf(typeof(InspectAsset)).Single();
Assert.That(member.Metadata.AssetGuid, Is.Not.Null);
Assert.That(member.Metadata.AssetGuid!.AssetType, Is.EqualTo(typeof(Uri)));
}
[Test]
public void ClaimsResolveAttributeThenTypeThenBuiltIn()
{
@ -321,6 +330,14 @@ namespace GameCult.Caching.Tests
public string Name = string.Empty;
}
[CultDocument("tests.inspect_asset", "tests.inspect_asset.v1")]
[MessagePackObject]
public sealed class InspectAsset
{
[Key(0)] [CultInspectorAssetGuid(typeof(Uri))]
public string Guid = string.Empty;
}
[CultDocument("tests.inspect_fixed", "tests.inspect_fixed.v1")]
[MessagePackObject]
public sealed class InspectFixed