Studio: refuse built-ins by asset identity, not GUID digits; add Clear for stale keys
TrimEnd('0','f') missed Library/unity default resources built-ins whose GUID
has no all-zero or all-f suffix (e.g. 0000000000000000e000000000000000, the
LegacyRuntime font / Cube mesh in 6000.3.24f1). Ask AssetDatabase instead: a
real project asset is always a main or sub-asset under Assets/ or Packages/;
built-ins are neither. Also add a Clear button beside the stale-value warning
so a non-resolving stored key can be cleared without picking a substitute
asset first (drawing still never rewrites a stored value on its own).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
0c9cb57bea
commit
6c7cfa7a94
3 changed files with 86 additions and 10 deletions
|
|
@ -177,11 +177,20 @@ 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;
|
||||
// A GUID-string heuristic ("all zero", "...f000...") cannot cover every built-in Unity ships:
|
||||
// "Library/unity default resources" built-ins (verified in 6000.3.24f1, e.g. LegacyRuntime font
|
||||
// and the Cube mesh under GUID 0000000000000000e000000000000000) carry ordinary-looking hex GUIDs
|
||||
// with no all-zero or all-f suffix. Ask AssetDatabase about the object instead: a real project
|
||||
// asset is always either the main asset at its path or one of its sub-assets, and always lives
|
||||
// under Assets/ or Packages/. Built-ins are neither (verified False for both IsMainAsset and
|
||||
// IsSubAsset) and resolve to no path under either root.
|
||||
private static bool IsBuiltinResource(Object asset)
|
||||
{
|
||||
if (asset == null) return false;
|
||||
if (AssetDatabase.IsMainAsset(asset) || AssetDatabase.IsSubAsset(asset)) return false;
|
||||
var path = AssetDatabase.GetAssetPath(asset);
|
||||
return string.IsNullOrEmpty(path) || !(path.StartsWith("Assets/", StringComparison.Ordinal) || path.StartsWith("Packages/", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
// 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
|
||||
|
|
@ -210,19 +219,26 @@ namespace GameCult.Unity.Caching.Editor
|
|||
stale = asset == null;
|
||||
}
|
||||
|
||||
if (stale) EditorGUILayout.HelpBox(label + ": stored value \"" + value + "\" does not resolve to an asset.", MessageType.Warning);
|
||||
if (stale)
|
||||
{
|
||||
using (new EditorGUILayout.HorizontalScope())
|
||||
{
|
||||
EditorGUILayout.HelpBox(label + ": stored value \"" + value + "\" does not resolve to an asset.", MessageType.Warning);
|
||||
if (GUILayout.Button("Clear", GUILayout.Width(48), GUILayout.Height(EditorGUIUtility.singleLineHeight))) return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
if (IsBuiltinResource(next))
|
||||
{
|
||||
Debug.LogWarning(label + ": \"" + next.name + "\" is a built-in resource with no Addressables GUID. Not stored.");
|
||||
return value;
|
||||
}
|
||||
|
||||
var nextGuid = AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(next));
|
||||
return CultAssetGuidKey.Format(nextGuid, AssetDatabase.IsSubAsset(next) ? next.name : null);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -167,8 +167,10 @@ public sealed class ItemData
|
|||
`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)
|
||||
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
|
||||
|
|
|
|||
|
|
@ -70,5 +70,63 @@ namespace GameCult.Caching.Tests
|
|||
{
|
||||
Assert.That(CultAssetGuidKey.Format("abc123", "Icon_Small"), Is.EqualTo("abc123[Icon_Small]"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SubAssetNameWithLeadingAndTrailingSpacesRoundTrips()
|
||||
{
|
||||
// A name.Trim() mutant would strip the spaces on the way out; Addressables sub-asset
|
||||
// names are taken verbatim, so the exact bytes between the brackets must survive.
|
||||
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 FormatThenTryParseRoundTripsBareGuid()
|
||||
{
|
||||
var key = CultAssetGuidKey.Format("abc123", null);
|
||||
Assert.That(CultAssetGuidKey.TryParse(key, out var guid, out var sub), Is.True);
|
||||
Assert.That(guid, Is.EqualTo("abc123"));
|
||||
Assert.That(sub, Is.Null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FormatThenTryParseRoundTripsSubAsset()
|
||||
{
|
||||
var key = CultAssetGuidKey.Format("abc123", "Icon[2]");
|
||||
Assert.That(CultAssetGuidKey.TryParse(key, out var guid, out var sub), Is.True);
|
||||
Assert.That(guid, Is.EqualTo("abc123"));
|
||||
Assert.That(sub, Is.EqualTo("Icon[2]"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FormatOfNullOrEmptyGuidReturnsEmptyString()
|
||||
{
|
||||
Assert.That(CultAssetGuidKey.Format(null!, null), Is.EqualTo(string.Empty));
|
||||
Assert.That(CultAssetGuidKey.Format(string.Empty, null), Is.EqualTo(string.Empty));
|
||||
Assert.That(CultAssetGuidKey.Format(null!, "Icon"), Is.EqualTo("[Icon]"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void BareGuidPreservesCase()
|
||||
{
|
||||
// No lowercasing: a mutant that normalizes case would still pass every other test here
|
||||
// since the fixtures above are already lowercase.
|
||||
Assert.That(CultAssetGuidKey.TryParse("ABC123DEF456", out var guid, out _), Is.True);
|
||||
Assert.That(guid, Is.EqualTo("ABC123DEF456"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FormatOfSubAssetNameContainingBracketsMatchesAddressablesSplit()
|
||||
{
|
||||
// Documents the split ruled for Addressables-shaped keys: "g[Icon[2]]" parses back to
|
||||
// name "Icon[2]", i.e. Format takes everything up to the FIRST '[' as the guid and
|
||||
// everything between it and the LAST ']' as the name, brackets included.
|
||||
var key = CultAssetGuidKey.Format("g", "Icon[2]");
|
||||
Assert.That(key, Is.EqualTo("g[Icon[2]]"));
|
||||
Assert.That(CultAssetGuidKey.TryParse(key, out var guid, out var sub), Is.True);
|
||||
Assert.That(guid, Is.EqualTo("g"));
|
||||
Assert.That(sub, Is.EqualTo("Icon[2]"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue