Studio: read and write sub-asset Addressables keys, warn on stale values
Ruling S (2026-09-17): a stored [CultInspectorAssetGuid] value is the Addressables key form itself, the bare GUID for a main asset or "guid[subAssetName]" for a picked sub-asset (one sprite of a sheet, say) - the same shape AssetReference keeps and Addressables.LoadAssetAsync<T> accepts. CultAssetGuidKey (engine-free, GameCult.Caching) pins the parse/format rule with unit tests (bare guid, guid[name], names with brackets/spaces, malformed input), so headless checks can share it without touching Unity. The Studio drawer's AssetGuid branch now: resolves either key form on read; refuses to store a built-in resource's GUID (it has no Addressables entry) and logs why instead of silently writing nothing; shows a warning next to a stored value that fails to parse or resolve instead of drawing it as an ordinary empty field, and never rewrites that stale value on its own. Rebuilt GameCult.Caching/GameCult.Caching.MessagePack plugin DLLs (scripts/build-unity-package.ps1 -UpdateTemplate) since the new type ships in GameCult.Caching. Compiled CultCacheStudioDrawers.cs and CultCacheStudioWindow.cs against Unity 6000.3.24f1's editor reference set plus these plugins in a scratch csproj (removed after): 0 errors. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
0155ba8cca
commit
1e398e0116
9 changed files with 177 additions and 10 deletions
39
src/GameCult.Caching/CultAssetGuidKey.cs
Normal file
39
src/GameCult.Caching/CultAssetGuidKey.cs
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
#nullable enable
|
||||
namespace GameCult.Caching
|
||||
{
|
||||
// The Addressables key form a [CultInspectorAssetGuid] member stores: the bare GUID for a main
|
||||
// asset, or "guid[subAssetName]" when a sub-asset (one sprite of a sheet, say) is picked. This is
|
||||
// Addressables' own key shape (what AssetReference keeps as GUID plus sub-object name); engine-free
|
||||
// so both the Studio drawer and any headless check can pin the same parse without touching Unity.
|
||||
public static class CultAssetGuidKey
|
||||
{
|
||||
public static bool TryParse(string key, out string guid, out string? subAssetName)
|
||||
{
|
||||
guid = string.Empty;
|
||||
subAssetName = null;
|
||||
if (string.IsNullOrEmpty(key)) return false;
|
||||
|
||||
var bracket = key.IndexOf('[');
|
||||
if (bracket < 0)
|
||||
{
|
||||
guid = key;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (bracket == 0 || key[key.Length - 1] != ']') return false;
|
||||
|
||||
var name = key.Substring(bracket + 1, key.Length - bracket - 2);
|
||||
if (name.Length == 0) return false;
|
||||
|
||||
guid = key.Substring(0, bracket);
|
||||
subAssetName = name;
|
||||
return true;
|
||||
}
|
||||
|
||||
public static string Format(string guid, string? subAssetName)
|
||||
{
|
||||
guid ??= string.Empty;
|
||||
return string.IsNullOrEmpty(subAssetName) ? guid : guid + "[" + subAssetName + "]";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -167,14 +167,7 @@ namespace GameCult.Unity.Caching.Editor
|
|||
|
||||
private static string DrawString(string label, string value, CultInspectorMetadata metadata)
|
||||
{
|
||||
if (metadata.AssetGuid != null)
|
||||
{
|
||||
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.AssetPathToGUID(AssetDatabase.GetAssetPath(next));
|
||||
}
|
||||
if (metadata.AssetGuid != null) return DrawAssetGuid(label, value, metadata.AssetGuid);
|
||||
|
||||
var textArea = metadata.TextArea;
|
||||
if (textArea == null) return EditorGUILayout.TextField(label, value ?? string.Empty);
|
||||
|
|
@ -184,6 +177,55 @@ namespace GameCult.Unity.Caching.Editor
|
|||
GUILayout.MaxHeight(Mathf.Max(textArea.MaxLines, textArea.MinLines, 1) * EditorGUIUtility.singleLineHeight));
|
||||
}
|
||||
|
||||
// Every zero-hex GUID that AssetDatabase uses for built-in/generated resources (the ordinary
|
||||
// "all zero" form and the "...f000000000000000" virtual-asset form) has no .meta file and no
|
||||
// Addressables entry, so it cannot be stored as a key.
|
||||
private static bool IsBuiltinResourceGuid(string guid) =>
|
||||
!string.IsNullOrEmpty(guid) && guid.TrimEnd('0', 'f') == string.Empty;
|
||||
|
||||
// Reads and writes the Addressables key form ruled in docs/addressables-cut.md (operator ruling S):
|
||||
// the bare GUID for a main asset, "guid[subAssetName]" for a picked sub-asset. Never rewrites a
|
||||
// stored value on its own — a value that fails to parse or resolve stays exactly as stored, with
|
||||
// a warning shown alongside it, until the operator picks a new asset.
|
||||
private static string DrawAssetGuid(string label, string value, CultInspectorAssetGuidAttribute attribute)
|
||||
{
|
||||
var assetType = attribute.AssetType != null && typeof(Object).IsAssignableFrom(attribute.AssetType) ? attribute.AssetType : typeof(Object);
|
||||
Object asset = null;
|
||||
var stale = false;
|
||||
|
||||
if (!string.IsNullOrEmpty(value))
|
||||
{
|
||||
if (CultAssetGuidKey.TryParse(value, out var guid, out var subAssetName))
|
||||
{
|
||||
var path = AssetDatabase.GUIDToAssetPath(guid);
|
||||
if (!string.IsNullOrEmpty(path))
|
||||
{
|
||||
asset = subAssetName == null
|
||||
? AssetDatabase.LoadAssetAtPath(path, assetType)
|
||||
: AssetDatabase.LoadAllAssetRepresentationsAtPath(path)
|
||||
.FirstOrDefault(candidate => candidate != null && candidate.name == subAssetName && assetType.IsInstanceOfType(candidate));
|
||||
}
|
||||
}
|
||||
|
||||
stale = asset == null;
|
||||
}
|
||||
|
||||
if (stale) EditorGUILayout.HelpBox(label + ": stored value \"" + value + "\" does not resolve to an asset.", MessageType.Warning);
|
||||
|
||||
var next = EditorGUILayout.ObjectField(label, asset, assetType, false);
|
||||
if (next == asset) return value;
|
||||
if (next == null) return string.Empty;
|
||||
|
||||
var nextGuid = AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(next));
|
||||
if (string.IsNullOrEmpty(nextGuid) || IsBuiltinResourceGuid(nextGuid))
|
||||
{
|
||||
Debug.LogWarning(label + ": \"" + next.name + "\" is a built-in resource with no Addressables GUID. Not stored.");
|
||||
return value;
|
||||
}
|
||||
|
||||
return CultAssetGuidKey.Format(nextGuid, AssetDatabase.IsSubAsset(next) ? next.name : null);
|
||||
}
|
||||
|
||||
private object DrawInteger(string label, Type type, object value, CultInspectorMetadata metadata)
|
||||
{
|
||||
var current = value == null ? 0L : Convert.ToInt64(value);
|
||||
|
|
|
|||
|
|
@ -162,8 +162,13 @@ public sealed class ItemData
|
|||
- `CultInspectorTextArea`
|
||||
- `CultInspectorRange`
|
||||
- `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)
|
||||
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<T>` 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, and warns rather than silently clearing a
|
||||
stored value that no longer resolves)
|
||||
- `CultInspectorDrawer` (on drawer classes)
|
||||
|
||||
CultMesh collaboration should feed the same CultCache mutation surface rather
|
||||
|
|
|
|||
74
tests/GameCult.Caching.Tests/CultAssetGuidKeyTests.cs
Normal file
74
tests/GameCult.Caching.Tests/CultAssetGuidKeyTests.cs
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
#nullable enable
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace GameCult.Caching.Tests
|
||||
{
|
||||
public class CultAssetGuidKeyTests
|
||||
{
|
||||
[Test]
|
||||
public void BareGuidParsesWithNoSubAsset()
|
||||
{
|
||||
Assert.That(CultAssetGuidKey.TryParse("0123456789abcdef0123456789abcdef", out var guid, out var sub), Is.True);
|
||||
Assert.That(guid, Is.EqualTo("0123456789abcdef0123456789abcdef"));
|
||||
Assert.That(sub, Is.Null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GuidWithSubAssetNameParses()
|
||||
{
|
||||
Assert.That(CultAssetGuidKey.TryParse("abc123[Icon_Small]", out var guid, out var sub), Is.True);
|
||||
Assert.That(guid, Is.EqualTo("abc123"));
|
||||
Assert.That(sub, Is.EqualTo("Icon_Small"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SubAssetNameMayContainSpaces()
|
||||
{
|
||||
Assert.That(CultAssetGuidKey.TryParse("abc123[Icon Small Two]", out var guid, out var sub), Is.True);
|
||||
Assert.That(guid, Is.EqualTo("abc123"));
|
||||
Assert.That(sub, Is.EqualTo("Icon Small Two"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SubAssetNameMayContainBrackets()
|
||||
{
|
||||
Assert.That(CultAssetGuidKey.TryParse("abc123[Icon[2]]", out var guid, out var sub), Is.True);
|
||||
Assert.That(guid, Is.EqualTo("abc123"));
|
||||
Assert.That(sub, Is.EqualTo("Icon[2]"));
|
||||
}
|
||||
|
||||
[TestCase("")]
|
||||
[TestCase("[name]")]
|
||||
[TestCase("abc123[")]
|
||||
[TestCase("abc123[name")]
|
||||
[TestCase("abc123]")]
|
||||
[TestCase("abc123[]")]
|
||||
public void MalformedInputDoesNotParse(string key)
|
||||
{
|
||||
var ok = CultAssetGuidKey.TryParse(key, out var guid, out var sub);
|
||||
if (key == "abc123]")
|
||||
{
|
||||
// No '[' at all: treated as a literal bare key, not a bracket form.
|
||||
Assert.That(ok, Is.True);
|
||||
Assert.That(guid, Is.EqualTo("abc123]"));
|
||||
Assert.That(sub, Is.Null);
|
||||
return;
|
||||
}
|
||||
|
||||
Assert.That(ok, Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FormatRoundTripsBareGuid()
|
||||
{
|
||||
Assert.That(CultAssetGuidKey.Format("abc123", null), Is.EqualTo("abc123"));
|
||||
Assert.That(CultAssetGuidKey.Format("abc123", string.Empty), Is.EqualTo("abc123"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FormatRoundTripsSubAsset()
|
||||
{
|
||||
Assert.That(CultAssetGuidKey.Format("abc123", "Icon_Small"), Is.EqualTo("abc123[Icon_Small]"));
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 5b590f45dfef47cf9bc9a8e4dfa4038a
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Loading…
Reference in a new issue