diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs index 28cb7a7..5cc8a79 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs @@ -52,6 +52,14 @@ namespace GameCult.Eve.UnityScene private readonly Dictionary> _nativeAssetMetadata = new Dictionary>(StringComparer.Ordinal); private readonly List _assetBundles = new List(); + private readonly Dictionary _assetSelections = + new Dictionary(StringComparer.Ordinal); + private readonly Dictionary> _bundleSelections = + new Dictionary>(StringComparer.Ordinal); + private readonly Dictionary _bundleVariants = + new Dictionary(StringComparer.Ordinal); + private readonly HashSet _loadedBundleUris = new HashSet(StringComparer.Ordinal); + private readonly HashSet _loadingBundleUris = new HashSet(StringComparer.Ordinal); private readonly List _assetBodyLeases = new List(); private readonly Dictionary _renderChannelLayers = new Dictionary(StringComparer.Ordinal); private readonly ConcurrentQueue _liveDocuments = new ConcurrentQueue(); @@ -382,6 +390,11 @@ namespace GameCult.Eve.UnityScene _prefabs.Clear(); _nativeAssets.Clear(); _nativeAssetMetadata.Clear(); + _assetSelections.Clear(); + _bundleSelections.Clear(); + _bundleVariants.Clear(); + _loadedBundleUris.Clear(); + _loadingBundleUris.Clear(); _mappedEntityFrameCursor?.Dispose(); _mappedEntityFrameCursor = null; _mappedEntityFrameContract = null; @@ -412,6 +425,7 @@ namespace GameCult.Eve.UnityScene public GameObject? ResolvePrefab(EveUnityPlayableWorldAssetBinding asset) { if (asset == null) throw new ArgumentNullException(nameof(asset)); + EnsureAssetLoaded(asset.AssetRef); return _prefabs.TryGetValue(asset.AssetRef, out var prefab) ? prefab : null; } @@ -419,6 +433,7 @@ namespace GameCult.Eve.UnityScene { if (asset == null) throw new ArgumentNullException(nameof(asset)); if (assetType == null) throw new ArgumentNullException(nameof(assetType)); + EnsureAssetLoaded(asset.AssetRef); return _nativeAssets.TryGetValue(asset.AssetRef, out var value) && assetType.IsInstanceOfType(value) ? value : null; } @@ -1029,15 +1044,18 @@ namespace GameCult.Eve.UnityScene _prefabs.Clear(); _nativeAssets.Clear(); _nativeAssetMetadata.Clear(); + _assetSelections.Clear(); + _bundleSelections.Clear(); + _bundleVariants.Clear(); + _loadedBundleUris.Clear(); + _loadingBundleUris.Clear(); _renderChannelLayers.Clear(); var selected = catalog.Assets - .Select(asset => new - { - Asset = asset, - Variant = asset.Variants.FirstOrDefault(variant => + .Select(asset => new SelectedAsset( + asset, + asset.Variants.FirstOrDefault(variant => string.Equals(variant.RuntimeId, "unity-scene", StringComparison.Ordinal) && - string.Equals(variant.Platform, CurrentBundlePlatform(), StringComparison.Ordinal)) - }) + string.Equals(variant.Platform, CurrentBundlePlatform(), StringComparison.Ordinal)))) .Where(selection => selection.Variant != null) .ToArray(); if (selected.Length == 0) @@ -1051,53 +1069,112 @@ namespace GameCult.Eve.UnityScene $"Advertised variants: {string.Join(", ", advertisedVariants)}"); } ReadCameraPolicies(selected.Select(selection => selection.Variant!)); - foreach (var group in selected.GroupBy(selection => selection.Variant!.Uri, StringComparer.Ordinal)) + foreach (var selection in selected) { + var variant = selection.Variant!; + _assetSelections[selection.Asset.AssetRef] = selection; + _bundleVariants[variant.Uri] = variant; + if (!_bundleSelections.TryGetValue(variant.Uri, out var group)) + { + group = new List(); + _bundleSelections[variant.Uri] = group; + } + group.Add(selection); + var metadata = MergeAssetMetadata(selection.Asset.Metadata, variant.Metadata); + _nativeAssetMetadata[selection.Asset.AssetRef] = metadata; + if (selection.Asset.Metadata.TryGetValue("presentationRole", out var role) && + !string.IsNullOrWhiteSpace(role)) + { + _assetSelections[role] = selection; + _nativeAssetMetadata[role] = metadata; + } + } + + CurrentAssetCatalogVersion = catalog.Version; + } + + private void EnsureAssetLoaded(string assetRef) + { + if (_nativeAssets.ContainsKey(assetRef) || !_assetSelections.TryGetValue(assetRef, out var selection)) + return; + EnsureBundleLoaded(selection.Variant!.Uri); + } + + private void EnsureBundleLoaded(string uri) + { + if (_loadedBundleUris.Contains(uri)) return; + if (!_bundleVariants.TryGetValue(uri, out var variant)) + throw new InvalidOperationException($"Provider asset bundle '{uri}' is not present in the selected runtime catalog."); + if (!_loadingBundleUris.Add(uri)) + throw new InvalidOperationException($"Provider asset bundle dependency cycle includes '{uri}'."); + try + { + if (variant.Metadata.TryGetValue("unity.bundleDependencyUris", out var dependencies)) + foreach (var dependency in dependencies.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries)) + EnsureBundleLoaded(dependency); + var groupElapsed = Stopwatch.StartNew(); - var variant = group.First().Variant!; - var manifest = _snapshot!.FetchDocumentsAsync( - recordKeys: new[] { variant.Uri }, + var descriptor = _snapshot!.FetchDocumentsAsync( + recordKeys: new[] { uri }, schemaIds: new[] { CultMeshCdnSchemaVersions.ArtifactManifest }) .GetAwaiter() - .GetResult(); - var descriptor = manifest.FirstOrDefault(); + .GetResult() + .FirstOrDefault(); if (descriptor == null) - throw new InvalidOperationException($"Provider asset bundle manifest '{variant.Uri}' was not available."); + throw new InvalidOperationException($"Provider asset bundle manifest '{uri}' was not available."); TraceStartup("asset-manifest", groupElapsed); var bundle = LoadVerifiedBundle(descriptor, variant); TraceStartup("asset-transfer-and-bundle-load", groupElapsed); if (bundle == null) - throw new InvalidOperationException($"Unity could not load provider asset bundle '{variant.Uri}'."); + throw new InvalidOperationException($"Unity could not load provider asset bundle '{uri}'."); _assetBundles.Add(bundle); var bundleAssetNames = bundle.GetAllAssetNames(); - foreach (var selection in group) + foreach (var selection in _bundleSelections[uri]) { var bundleAssetName = ResolveBundleAssetName(bundleAssetNames, selection.Variant!.AssetKey); - var value = bundleAssetName == null ? null : bundle.LoadAsset(bundleAssetName); + var value = bundleAssetName == null + ? null + : LoadBundleAsset(bundle, bundleAssetName, selection.Asset.AssetKind); if (value == null) throw new InvalidOperationException( $"Provider asset '{selection.Asset.AssetRef}' advertises missing Unity bundle asset " + - $"'{selection.Variant.AssetKey}'. Available assets: " + - string.Join(", ", bundleAssetNames)); + $"'{selection.Variant.AssetKey}'. Available assets: " + string.Join(", ", bundleAssetNames)); _nativeAssets[selection.Asset.AssetRef] = value; - var metadata = MergeAssetMetadata(selection.Asset.Metadata, selection.Variant.Metadata); - _nativeAssetMetadata[selection.Asset.AssetRef] = metadata; if (selection.Asset.Metadata.TryGetValue("presentationRole", out var role) && !string.IsNullOrWhiteSpace(role)) - { _nativeAssets[role] = value; - _nativeAssetMetadata[role] = metadata; - } if (value is GameObject prefab) { _prefabs[selection.Asset.AssetRef] = prefab; if (!string.IsNullOrWhiteSpace(role)) _prefabs[role] = prefab; } } + _loadedBundleUris.Add(uri); TraceStartup("bundle-assets", groupElapsed); } + finally + { + _loadingBundleUris.Remove(uri); + } + } - CurrentAssetCatalogVersion = catalog.Version; + private static UnityEngine.Object? LoadBundleAsset(AssetBundle bundle, string assetName, string assetKind) + { + return string.Equals(assetKind, "sprite", StringComparison.Ordinal) + ? bundle.LoadAsset(assetName) + : bundle.LoadAsset(assetName); + } + + private sealed class SelectedAsset + { + public SelectedAsset(EveAssetCatalogEntry asset, EveAssetVariant? variant) + { + Asset = asset; + Variant = variant; + } + + public EveAssetCatalogEntry Asset { get; } + public EveAssetVariant? Variant { get; } } private static string? ResolveBundleAssetName(IEnumerable bundleAssetNames, string advertisedAssetKey) diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityCultMeshAssetLookupTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityCultMeshAssetLookupTests.cs index dfcc159..f58e0d4 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityCultMeshAssetLookupTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityCultMeshAssetLookupTests.cs @@ -4,6 +4,8 @@ using System.Collections.Generic; using System.Diagnostics; using System.Net; using System.Net.Sockets; +using System.IO; +using GameCult.Eve.Surface; using GameCult.Eve.UnityScene; using NUnit.Framework; using UnityEngine; @@ -106,5 +108,49 @@ namespace GameCult.Eve.UnityScene.Tests Assert.That(result["unity.volume.texturePort.surfaceHeight"], Is.EqualTo("_NebulaSurfaceHeight")); Assert.That(result["unity.volume.pass.raymarch"], Is.EqualTo("0")); } + + [Test] + public void PublishingCatalogRegistersBundlesWithoutMaterializingThem() + { + var platformMethod = typeof(EveUnityCultMeshLiveProviderTransport).GetMethod( + "CurrentBundlePlatform", + BindingFlags.Static | BindingFlags.NonPublic); + var publishMethod = typeof(EveUnityCultMeshLiveProviderTransport).GetMethod( + "PublishAssetCatalog", + BindingFlags.Instance | BindingFlags.NonPublic); + var bundlesField = typeof(EveUnityCultMeshLiveProviderTransport).GetField( + "_assetBundles", + BindingFlags.Instance | BindingFlags.NonPublic); + var selectionsField = typeof(EveUnityCultMeshLiveProviderTransport).GetField( + "_assetSelections", + BindingFlags.Instance | BindingFlags.NonPublic); + Assert.That(platformMethod, Is.Not.Null); + Assert.That(publishMethod, Is.Not.Null); + + using var transport = new EveUnityCultMeshLiveProviderTransport( + Path.Combine(Path.GetTempPath(), $"eve-lazy-assets-{Guid.NewGuid():N}.cc"), + "cultnet+tcp://127.0.0.1:1", + "provider", + "surface"); + var variant = new EveAssetVariant( + "unity-scene", + (string)platformMethod!.Invoke(null, Array.Empty())!, + "unity-assetbundle", + "cdn:manifest:lazy", + "sha256:00", + 1, + "assets/prefab.prefab"); + var catalog = new EveAssetCatalogDocument( + "provider", + "catalog", + 1, + DateTimeOffset.UtcNow.ToString("O"), + new[] { new EveAssetCatalogEntry("prefab.entity", "prefab", new[] { variant }) }); + + publishMethod!.Invoke(transport, new object[] { catalog }); + + Assert.That(((System.Collections.ICollection)bundlesField!.GetValue(transport)!).Count, Is.Zero); + Assert.That(((System.Collections.IDictionary)selectionsField!.GetValue(transport)!).Count, Is.EqualTo(1)); + } } } diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index d8f8df3..f255be1 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,7 +1,7 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.3.95", + "version": "0.3.96", "unity": "6000.0", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": {