diff --git a/Explorer/Assets/DCL/AvatarRendering/Emotes/Components/Intents/GetSceneEmoteFromRealmIntention.cs b/Explorer/Assets/DCL/AvatarRendering/Emotes/Components/Intents/GetSceneEmoteFromRealmIntention.cs index 696a96dbf7b..32b833c1241 100644 --- a/Explorer/Assets/DCL/AvatarRendering/Emotes/Components/Intents/GetSceneEmoteFromRealmIntention.cs +++ b/Explorer/Assets/DCL/AvatarRendering/Emotes/Components/Intents/GetSceneEmoteFromRealmIntention.cs @@ -3,7 +3,6 @@ using CommunicationData.URLHelpers; using DCL.AvatarRendering.Loading.Components; using DCL.Ipfs; -using DCL.Utility; using ECS.Prioritization.Components; using ECS.StreamableLoading; using ECS.StreamableLoading.AssetBundles; @@ -59,11 +58,12 @@ public readonly URN NewSceneEmoteURN() => public void CreateAndAddPromiseToWorld(World world, IPartitionComponent partitionComponent, URLSubdirectory? customStreamingSubdirectory, IEmote emote) { + // Scene emotes are scene content — resolve to the digest-bearing name so URL and cache identity match the scene's ABs. var promise = AssetBundlePromise.Create(world, GetAssetBundleIntention.FromHash( - this.EmoteHash + PlatformUtils.GetCurrentPlatform(), + SceneAssetBundleManifestVersion.GetCdnRequestHash(this.EmoteHash), + SceneAssetBundleManifestVersion, typeof(GameObject), - assetBundleManifestVersion: SceneAssetBundleManifestVersion, parentEntityID: SceneId, permittedSources: this.PermittedSources, customEmbeddedSubDirectory: customStreamingSubdirectory.Value, diff --git a/Explorer/Assets/DCL/AvatarRendering/Emotes/Systems/Load/LoadEmotesByPointersSystem.cs b/Explorer/Assets/DCL/AvatarRendering/Emotes/Systems/Load/LoadEmotesByPointersSystem.cs index 56c4f391209..2f1b7c09486 100644 --- a/Explorer/Assets/DCL/AvatarRendering/Emotes/Systems/Load/LoadEmotesByPointersSystem.cs +++ b/Explorer/Assets/DCL/AvatarRendering/Emotes/Systems/Load/LoadEmotesByPointersSystem.cs @@ -11,7 +11,6 @@ using DCL.Multiplayer.Connections.DecentralandUrls; using DCL.PerformanceAndDiagnostics.Analytics; using DCL.SDKComponents.AudioSources; -using DCL.Utility; using DCL.WebRequests; using ECS.Prioritization.Components; using ECS.StreamableLoading.AssetBundles; @@ -236,15 +235,17 @@ private bool CreateAssetBundlePromiseIfRequired(IEmote component, in GetEmotesBy if (component.AssetResults[intention.BodyShape] == null) { // The resolution of the AB promise will be finalized by FinalizeEmoteAssetBundleSystem + AssetBundleManifestVersion abManifest = component.DTO.AssetBundleManifestVersionOrFailed; + var promise = AssetBundlePromise.Create( World!, GetAssetBundleIntention.FromHash( - hash! + PlatformUtils.GetCurrentPlatform(), + abManifest.GetCdnRequestHash(hash!), + abManifest, typeof(GameObject), permittedSources: intention.PermittedSources, customEmbeddedSubDirectory: customStreamingSubdirectory, cancellationTokenSource: intention.CancellationTokenSource, - assetBundleManifestVersion: component.DTO.assetBundleManifestVersion, parentEntityID: component.DTO.id ), partitionComponent diff --git a/Explorer/Assets/DCL/AvatarRendering/Thumbnails/Utils/LoadThumbnailsUtils.cs b/Explorer/Assets/DCL/AvatarRendering/Thumbnails/Utils/LoadThumbnailsUtils.cs index 65a7a2a693e..0e77f87cb61 100644 --- a/Explorer/Assets/DCL/AvatarRendering/Thumbnails/Utils/LoadThumbnailsUtils.cs +++ b/Explorer/Assets/DCL/AvatarRendering/Thumbnails/Utils/LoadThumbnailsUtils.cs @@ -4,9 +4,9 @@ using Cysharp.Threading.Tasks; using DCL.AvatarRendering.Loading.Components; using DCL.Diagnostics; +using DCL.Ipfs; using DCL.Multiplayer.Connections.DecentralandUrls; using DCL.Optimization.Pools; -using DCL.Utility; using ECS; using ECS.Prioritization.Components; using ECS.StreamableLoading.AssetBundles; @@ -47,8 +47,9 @@ public static void CreateThumbnailABPromise( return; } - var assetBundleManifestVersion = attachment.GetAssetBundleManifestVersion(); - if (assetBundleManifestVersion != null && (assetBundleManifestVersion.IsLSDAsset || assetBundleManifestVersion.assetBundleManifestRequestFailed)) + AssetBundleManifestVersion? assetBundleManifestVersion = attachment.GetAssetBundleManifestVersion(); + + if (assetBundleManifestVersion == null || assetBundleManifestVersion.IsLSDAsset || assetBundleManifestVersion.assetBundleManifestRequestFailed) { ReportHub.Log( ReportCategory.THUMBNAILS, @@ -63,10 +64,10 @@ public static void CreateThumbnailABPromise( var promise = AssetBundlePromise.Create( world, GetAssetBundleIntention.FromHash( - hash: thumbnailPath.Value + PlatformUtils.GetCurrentPlatform(), - typeof(Texture2D), - permittedSources: AssetSource.ALL, + hash: assetBundleManifestVersion.GetCdnRequestHash(thumbnailPath.Value), assetBundleManifestVersion: assetBundleManifestVersion, + expectedAssetType: typeof(Texture2D), + permittedSources: AssetSource.ALL, parentEntityID: attachment.GetEntityId(), cancellationTokenSource: cancellationTokenSource ?? new CancellationTokenSource() ), diff --git a/Explorer/Assets/DCL/AvatarRendering/Wearables/Helpers/WearablePolymorphicBehaviour.cs b/Explorer/Assets/DCL/AvatarRendering/Wearables/Helpers/WearablePolymorphicBehaviour.cs index 3652fe02df0..6859e99c45a 100644 --- a/Explorer/Assets/DCL/AvatarRendering/Wearables/Helpers/WearablePolymorphicBehaviour.cs +++ b/Explorer/Assets/DCL/AvatarRendering/Wearables/Helpers/WearablePolymorphicBehaviour.cs @@ -219,14 +219,16 @@ private static void CreatePromise( } else { + AssetBundleManifestVersion abManifest = wearable.DTO.AssetBundleManifestVersionOrFailed; + // An index is added to the promise to know to which slot of the WearableAssets it belongs to var promise = AssetBundlePromise.Create(world, GetAssetBundleIntention.FromHash( - hash + PlatformUtils.GetCurrentPlatform(), + abManifest.GetCdnRequestHash(hash), + abManifest, expectedObjectType, permittedSources: intention.PermittedSources, customEmbeddedSubDirectory: customStreamingSubdirectory, - assetBundleManifestVersion: wearable.DTO.assetBundleManifestVersion, parentEntityID: wearable.DTO.id, cancellationTokenSource: intention.CancellationTokenSource), partitionComponent); diff --git a/Explorer/Assets/DCL/FeatureFlags/FeatureFlagsStrings.cs b/Explorer/Assets/DCL/FeatureFlags/FeatureFlagsStrings.cs index bba947798b3..bbce38b5581 100644 --- a/Explorer/Assets/DCL/FeatureFlags/FeatureFlagsStrings.cs +++ b/Explorer/Assets/DCL/FeatureFlags/FeatureFlagsStrings.cs @@ -82,7 +82,6 @@ public static class FeatureFlagsStrings public const string NEARBY_VOICE_CHAT = "alfa-nearby-voice-chat"; public const string AVATAR_CONTEXT_MENU = "alfa-avatar-context-menu"; public const string DOUBLE_CLICK_WALK = "alfa-double-click-walk"; - public const string AB_DEPS_DIGEST_CACHE_KEY = "alfa-ab-deps-digest-cache-key"; public const string BYTE_WEIGHTED_LOADING_PROGRESS = "alfa-byte-weighted-loading-progress"; public const string NEW_LODS = "new-lods"; public const string FOUNDATION_COMMUNITY_ID = "alfa-foundation-community-id"; diff --git a/Explorer/Assets/DCL/FeatureFlags/FeaturesRegistry.cs b/Explorer/Assets/DCL/FeatureFlags/FeaturesRegistry.cs index 89efd221440..584371e712e 100644 --- a/Explorer/Assets/DCL/FeatureFlags/FeaturesRegistry.cs +++ b/Explorer/Assets/DCL/FeatureFlags/FeaturesRegistry.cs @@ -1,6 +1,7 @@ using CodeLess.Attributes; using Cysharp.Threading.Tasks; using Global.AppArgs; +using System; using System.Collections.Generic; using System.Threading; using UnityEngine; @@ -69,7 +70,6 @@ public FeaturesRegistry( [FeatureId.AVATAR_CONTEXT_MENU] = appArgs.ResolveFeatureFlagArg(AppArgsFlags.AVATAR_CONTEXT_MENU, featureFlags.IsEnabled(FeatureFlagsStrings.AVATAR_CONTEXT_MENU) || Application.isEditor), [FeatureId.DOUBLE_CLICK_WALK] = appArgs.ResolveFeatureFlagArg(AppArgsFlags.DOUBLE_CLICK_WALK, featureFlags.IsEnabled(FeatureFlagsStrings.DOUBLE_CLICK_WALK)), [FeatureId.PULSE] = appArgs.ResolveFeatureFlagArg(AppArgsFlags.PULSE_MULTIPLAYER, featureFlags.IsEnabled(FeatureFlagsStrings.PULSE), requireDebug: false) && !localSceneDevelopment, - [FeatureId.AB_DEPS_DIGEST_CACHE_KEY] = featureFlags.IsEnabled(FeatureFlagsStrings.AB_DEPS_DIGEST_CACHE_KEY), [FeatureId.BYTE_WEIGHTED_LOADING_PROGRESS] = appArgs.ResolveFeatureFlagArg(AppArgsFlags.BYTE_WEIGHTED_LOADING_PROGRESS, featureFlags.IsEnabled(FeatureFlagsStrings.BYTE_WEIGHTED_LOADING_PROGRESS) || isEditor), [FeatureId.HARDWARE_FINGERPRINT] = appArgs.ResolveFeatureFlagArg(AppArgsFlags.HARDWARE_FINGERPRINT, featureFlags.IsEnabled(FeatureFlagsStrings.HARDWARE_FINGERPRINT)), [FeatureId.MCP_SERVER] = appArgs.HasFlag(AppArgsFlags.MCP) || appArgs.HasFlag(AppArgsFlags.MCP_PORT), @@ -203,6 +203,7 @@ public enum FeatureId AVATAR_CONTEXT_MENU = 60, DOUBLE_CLICK_WALK = 61, NEARBY_VOICE_CHAT = 62, + [Obsolete("The v49 deps-digest scheme is always on (gated by the manifest version itself); the flag is no longer read. Kept to preserve enum ordering.")] AB_DEPS_DIGEST_CACHE_KEY = 63, BYTE_WEIGHTED_LOADING_PROGRESS = 64, PULSE = 65, diff --git a/Explorer/Assets/DCL/Infrastructure/ECS/Unity/GLTFContainer/Asset/Systems/PrepareGltfAssetLoadingSystem.cs b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/GLTFContainer/Asset/Systems/PrepareGltfAssetLoadingSystem.cs index b6d82021f82..478c326d4b1 100644 --- a/Explorer/Assets/DCL/Infrastructure/ECS/Unity/GLTFContainer/Asset/Systems/PrepareGltfAssetLoadingSystem.cs +++ b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/GLTFContainer/Asset/Systems/PrepareGltfAssetLoadingSystem.cs @@ -2,7 +2,7 @@ using Arch.System; using Arch.SystemGroups; using DCL.Diagnostics; -using DCL.Utility; +using DCL.Ipfs; using ECS.Abstract; using ECS.StreamableLoading; using ECS.StreamableLoading.AssetBundles; @@ -70,14 +70,13 @@ private void Prepare(in Entity entity, ref GetGltfContainerAssetIntention intent World.Add(entity, GetGLTFIntention.Create(intention.Name, intention.Hash)); else { - var abIntention = GetAssetBundleIntention.Create(typeof(GameObject), $"{intention.Hash}{PlatformUtils.GetCurrentPlatform()}", intention.Name); - // Pre-populate so PrepareAssetBundleLoadingParametersSystem doesn't have to look it up by the - // platform-suffixed hash (the digest map is keyed by bare hashes). - //This will go away when the urls include the depsDigest - if (sceneData.SceneEntityDefinition.assetBundleManifestVersion is { } manifest - && manifest.TryGetDepsDigest(intention.Hash, out string digest)) - abIntention.DepsDigest = digest; - World.Add(entity, abIntention); + AssetBundleManifestVersion abManifest = sceneData.SceneEntityDefinition.AssetBundleManifestVersionOrFailed; + + World.Add(entity, GetAssetBundleIntention.Create(typeof(GameObject), + abManifest.GetCdnRequestHash(intention.Hash), + intention.Name, + abManifest, + sceneData.SceneEntityDefinition.id ?? string.Empty)); } } diff --git a/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/AssetBundles/AssetBundleManifestFallbackHelper.cs b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/AssetBundles/AssetBundleManifestFallbackHelper.cs index ffbe01b0264..3438091a332 100644 --- a/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/AssetBundles/AssetBundleManifestFallbackHelper.cs +++ b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/AssetBundles/AssetBundleManifestFallbackHelper.cs @@ -58,7 +58,7 @@ private static async UniTask CheckAssetBundleManifestFallbackInternalAsync(World else { assetBundleManifest.TryLogException(); - entityDefinition.assetBundleManifestVersion = AssetBundleManifestVersion.CreateFailed(); + entityDefinition.assetBundleManifestVersion = AssetBundleManifestVersion.FAILED; } } } diff --git a/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/AssetBundles/GetAssetBundleIntention.cs b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/AssetBundles/GetAssetBundleIntention.cs index 5ecb8f2d660..294b43f755a 100644 --- a/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/AssetBundles/GetAssetBundleIntention.cs +++ b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/AssetBundles/GetAssetBundleIntention.cs @@ -1,10 +1,8 @@ using AssetManagement; using CommunicationData.URLHelpers; using DCL.Ipfs; -using DCL.Utility; using ECS.StreamableLoading.Cache.Disk.Cacheables; using ECS.StreamableLoading.Common.Components; -using SceneRunner.Scene; using System; using System.Threading; using UnityEngine; @@ -15,9 +13,11 @@ public struct GetAssetBundleIntention : ILoadingIntention, IEquatable /// If the expected object type is null we don't know which asset will be loaded. /// It's valid for dependencies for which we need to load the asset bundle itself only @@ -35,20 +35,12 @@ public struct GetAssetBundleIntention : ILoadingIntention, IEquatable internal Hash128? cacheHash; - /// - /// Per-file dependency digest from the v49+ scene asset-bundle manifest. Two scenes can request the same - /// with different dependency closures; this field disambiguates them in the cache. - /// Empty for legacy (pre-v49) entries — those keep their historical key. - /// - public string? DepsDigest; - public bool IsDependency; public bool LookForDependencies; - private GetAssetBundleIntention(Type? expectedObjectType, string? name = null, + private GetAssetBundleIntention(Type? expectedObjectType, AssetBundleManifestVersion assetBundle, string? name = null, string? hash = null, AssetSource permittedSources = AssetSource.ALL, URLSubdirectory customEmbeddedSubDirectory = default, - AssetBundleManifestVersion? assetBundleVersion = null, string parentEntityID = "", bool isDependency = false, bool lookForDependencies = false, @@ -62,10 +54,9 @@ private GetAssetBundleIntention(Type? expectedObjectType, string? name = null, CommonArguments = new CommonLoadingArguments(URLAddress.EMPTY, customEmbeddedSubDirectory, permittedSources: permittedSources, cancellationTokenSource: cancellationTokenSource); cacheHash = null; - DepsDigest = null; ParentEntityID = parentEntityID; - AssetBundleManifestVersion = assetBundleVersion; + assetBundleManifest = assetBundle; IsDependency = isDependency; LookForDependencies = lookForDependencies; } @@ -75,37 +66,35 @@ internal GetAssetBundleIntention(CommonLoadingArguments commonArguments) : this( CommonArguments = commonArguments; } + /// An AB can never be requested without a manifest: every factory sets one, and default-initialized structs (which never flow into loading) observe the failed sentinel. + public readonly AssetBundleManifestVersion AssetBundleManifest => assetBundleManifest ?? AssetBundleManifestVersion.FAILED; + + // Hash alone identifies the bundle: v49+ hashes carry the deps digest inside the file name, so two dependency closures never share a Hash. public bool Equals(GetAssetBundleIntention other) => - StringComparer.OrdinalIgnoreCase.Equals(Hash, other.Hash) - && StringComparer.OrdinalIgnoreCase.Equals(DepsDigest ?? string.Empty, other.DepsDigest ?? string.Empty); + StringComparer.OrdinalIgnoreCase.Equals(Hash, other.Hash); public CommonLoadingArguments CommonArguments { get; set; } public CancellationTokenSource CancellationTokenSource => CommonArguments.CancellationTokenSource; - public static GetAssetBundleIntention Create(Type? expectedAssetType, string hash, string name, AssetSource permittedSources = AssetSource.ALL, + public static GetAssetBundleIntention Create(Type? expectedAssetType, string hash, string name, AssetBundleManifestVersion assetBundleManifestVersion, string parentEntityID, AssetSource permittedSources = AssetSource.ALL, URLSubdirectory customEmbeddedSubDirectory = default) => - new (expectedAssetType, hash: hash, name: name, permittedSources: permittedSources, customEmbeddedSubDirectory: customEmbeddedSubDirectory); + new (expectedAssetType, assetBundleManifestVersion, hash: hash, name: name, parentEntityID: parentEntityID, permittedSources: permittedSources, customEmbeddedSubDirectory: customEmbeddedSubDirectory); - public static GetAssetBundleIntention FromHash(string hash, Type? expectedAssetType = null, AssetSource permittedSources = AssetSource.ALL, + public static GetAssetBundleIntention FromHash(string hash, AssetBundleManifestVersion assetBundleManifestVersion, Type? expectedAssetType = null, AssetSource permittedSources = AssetSource.ALL, URLSubdirectory customEmbeddedSubDirectory = default, CancellationTokenSource cancellationTokenSource = null, - AssetBundleManifestVersion? assetBundleManifestVersion = null, string parentEntityID = "", bool isDependency = false, bool lookForDependencies = false) => - new (expectedAssetType, hash: hash, assetBundleVersion: assetBundleManifestVersion, parentEntityID: parentEntityID, permittedSources: permittedSources, customEmbeddedSubDirectory: customEmbeddedSubDirectory, isDependency: isDependency, lookForDependencies: lookForDependencies, cancellationTokenSource: cancellationTokenSource); + string parentEntityID = "", bool isDependency = false, bool lookForDependencies = false) => + new (expectedAssetType, assetBundleManifestVersion, hash: hash, parentEntityID: parentEntityID, permittedSources: permittedSources, customEmbeddedSubDirectory: customEmbeddedSubDirectory, isDependency: isDependency, lookForDependencies: lookForDependencies, cancellationTokenSource: cancellationTokenSource); public override bool Equals(object obj) => obj is GetAssetBundleIntention other && Equals(other); public override int GetHashCode() => - HashCode.Combine( - StringComparer.OrdinalIgnoreCase.GetHashCode(Hash ?? string.Empty), - StringComparer.OrdinalIgnoreCase.GetHashCode(DepsDigest ?? string.Empty)); + StringComparer.OrdinalIgnoreCase.GetHashCode(Hash ?? string.Empty); public override string ToString() => $"Get Asset Bundle: {Name} ({Hash})"; - public static string BuildInitialSceneStateURL(string initialSceneStateID) => - $"staticscene_{initialSceneStateID}{PlatformUtils.GetCurrentPlatform()}"; - public class DiskHashCompute : AbstractDiskHashCompute { public static readonly DiskHashCompute INSTANCE = new (); @@ -114,11 +103,8 @@ private DiskHashCompute() { } protected override void FillPayload(IHashKeyPayload keyPayload, in GetAssetBundleIntention asset) { + // The hash alone keys the on-disk file (v49+ hashes embed the digest); digest-less hashes keep their legacy key so existing entries keep hitting. keyPayload.Put(asset.Hash ?? asset.Name!); - - // Only contribute to the disk key when present so legacy 2-part-filename entries keep their existing on-disk file. - if (!string.IsNullOrEmpty(asset.DepsDigest)) - keyPayload.Put(asset.DepsDigest); } } diff --git a/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/AssetBundles/InitialSceneState/GetISSDescriptorIntention.cs b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/AssetBundles/InitialSceneState/GetISSDescriptorIntention.cs index 60c73dcc003..500ade1bb08 100644 --- a/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/AssetBundles/InitialSceneState/GetISSDescriptorIntention.cs +++ b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/AssetBundles/InitialSceneState/GetISSDescriptorIntention.cs @@ -34,7 +34,7 @@ public GetISSDescriptorIntention(string sceneId, AssetBundleManifestVersion mani // Manifest is required: callers reach this only after the scene definition (and therefore its manifest) // has been loaded. A valid manifest should be available at this point public static GetISSDescriptorIntention For(SceneEntityDefinition definition) => - new (definition.id, definition.assetBundleManifestVersion ?? AssetBundleManifestVersion.CreateFailed()); + new (definition.id, definition.assetBundleManifestVersion ?? AssetBundleManifestVersion.FAILED); public bool Equals(GetISSDescriptorIntention other) => SceneId == other.SceneId; diff --git a/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/AssetBundles/InitialSceneState/LoadISSDescriptorSystem.cs b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/AssetBundles/InitialSceneState/LoadISSDescriptorSystem.cs index 2bd5f42d6d6..0d5bf7c3e49 100644 --- a/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/AssetBundles/InitialSceneState/LoadISSDescriptorSystem.cs +++ b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/AssetBundles/InitialSceneState/LoadISSDescriptorSystem.cs @@ -4,8 +4,6 @@ using Cysharp.Threading.Tasks; using DCL.Diagnostics; using DCL.FeatureFlags; -using DCL.Ipfs; -using DCL.Utility; using DCL.WebRequests; using ECS.Groups; using ECS.Prioritization.Components; @@ -24,10 +22,6 @@ namespace ECS.StreamableLoading.AssetBundles.InitialSceneState /// (pre-v49 manifest or missing descriptor JSON) yield a *failed* result — that way the framework /// never persists the "no ISS" outcome to disk (PutAsync is gated on success), and the consumer /// in ResolveISSDescriptorSystem calls ISSDescriptor.MarkAsNone. - /// - /// The HEAD probe that chooses between Bundle and Descriptor modes is temporarily disabled — see - /// . A later PR will re-enable Bundle mode. - /// /// [UpdateInGroup(typeof(LoadGlobalSystemGroup))] [LogCategory(ReportCategory.SCENE_LOADING)] @@ -36,16 +30,14 @@ public partial class LoadISSDescriptorSystem : LoadSystemBase cache, DiskCacheOptions? diskCacheOptions = null) : base(world, cache, diskCacheOptions) { this.webRequestController = webRequestController; - this.assetBundleURL = assetBundleURL; this.descriptorBaseUrl = descriptorBaseUrl; } @@ -68,11 +60,6 @@ protected override async UniTask> if (!metadata.HasValue) return new StreamableLoadingResult(GetReportData(), new Exception("No ISS descriptor JSON for this scene")); - // Bundle-mode HEAD probe is temporarily disabled — every ISS-capable scene goes through - // Descriptor mode for now. Kept the IsBundleReachableAsync helper below so re-enabling is - // a one-line restore in a later PR; see PR description for rationale. - // bool bundleReachable = await IsBundleReachableAsync(intention.SceneId, intention.ManifestVersion!, ct); - return new StreamableLoadingResult(metadata.Value); } @@ -96,22 +83,5 @@ protected override async UniTask> return null; } } - - // TODO (Juani): Currently unused: FlowInternalAsync forces Descriptor mode. Kept for the follow-up PR that - // re-enables Bundle-mode selection. - private async UniTask IsBundleReachableAsync(string sceneId, AssetBundleManifestVersion manifestVersion, CancellationToken ct) - { - if (manifestVersion == null || manifestVersion.assetBundleManifestRequestFailed) return false; - - string version = manifestVersion.GetAssetBundleManifestVersion(); - if (string.IsNullOrEmpty(version)) return false; - - string bundleHash = $"staticscene_{sceneId}{PlatformUtils.GetCurrentPlatform()}"; - URLAddress bundleUrl = manifestVersion.HasHashInPath() - ? assetBundleURL.Append(new URLPath($"{version}/{sceneId}/{bundleHash}")) - : assetBundleURL.Append(new URLPath($"{version}/{bundleHash}")); - - return await webRequestController.IsHeadReachableAsync(GetReportData(), bundleUrl, ct, suppressErrors: true); - } } } diff --git a/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/AssetBundles/LoadAssetBundleSystem.cs b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/AssetBundles/LoadAssetBundleSystem.cs index f0d173dda16..adc26ec1515 100644 --- a/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/AssetBundles/LoadAssetBundleSystem.cs +++ b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/AssetBundles/LoadAssetBundleSystem.cs @@ -59,7 +59,7 @@ private async UniTask LoadDependenciesAsync(GetAssetBundleInt URLSubdirectory customEmbeddedSubdirectory = parentIntent.CommonArguments.CustomEmbeddedSubDirectory; - return await UniTask.WhenAll(assetBundleMetadata.dependencies.Select(hash => WaitForDependencyAsync(hash, parentIntent.AssetBundleManifestVersion!, parentIntent.ParentEntityID, customEmbeddedSubdirectory, partition, ct))); + return await UniTask.WhenAll(assetBundleMetadata.dependencies.Select(hash => WaitForDependencyAsync(hash, parentIntent.AssetBundleManifest, parentIntent.ParentEntityID, customEmbeddedSubdirectory, partition, ct))); } protected override async UniTask> FlowInternalAsync(GetAssetBundleIntention intention, StreamableLoadingState state, IPartitionComponent partition, CancellationToken ct) @@ -144,7 +144,7 @@ protected override async UniTask> FlowI // if the type was not specified don't load any assets StreamableLoadingResult result = await CreateAssetBundleDataAsync(assetBundle, intention.ExpectedObjectType, mainAsset, loadingMutex, dependencies, GetReportData(), - intention.AssetBundleManifestVersion == null ? "" : intention.AssetBundleManifestVersion.GetAssetBundleManifestVersion(), + intention.AssetBundleManifest.GetAssetBundleManifestVersion(), source, intention.IsDependency, intention.LookForDependencies, ct); return result; } @@ -220,7 +220,6 @@ private async UniTask WaitForDependencyAsync( IPartitionComponent partition, CancellationToken ct) { // Inherit partition from the parent promise - // we don't know the type of the dependency var assetBundlePromise = AssetPromise.Create(World, GetAssetBundleIntention.FromHash(hash, assetBundleManifestVersion: assetBundleManifestVersion, parentEntityID: parentEntityID, customEmbeddedSubDirectory: customEmbeddedSubdirectory, isDependency : true), partition); try diff --git a/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/AssetBundles/PrepareAssetBundleLoadingParametersSystem.cs b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/AssetBundles/PrepareAssetBundleLoadingParametersSystem.cs index 16934586a5f..3be94871e17 100644 --- a/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/AssetBundles/PrepareAssetBundleLoadingParametersSystem.cs +++ b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/AssetBundles/PrepareAssetBundleLoadingParametersSystem.cs @@ -5,7 +5,6 @@ using DCL.Diagnostics; using ECS.Groups; using ECS.StreamableLoading.Common.Components; -using SceneRunner.Scene; namespace ECS.StreamableLoading.AssetBundles { @@ -16,12 +15,7 @@ namespace ECS.StreamableLoading.AssetBundles [LogCategory(ReportCategory.ASSET_BUNDLES)] public partial class PrepareAssetBundleLoadingParametersSystem : PrepareAssetBundleLoadingParametersSystemBase { - private readonly ISceneData sceneData; - - internal PrepareAssetBundleLoadingParametersSystem(World world, ISceneData sceneData, URLDomain streamingAssetURL, URLDomain assetBundlesURL) : base(world, streamingAssetURL, assetBundlesURL) - { - this.sceneData = sceneData; - } + internal PrepareAssetBundleLoadingParametersSystem(World world, URLDomain streamingAssetURL, URLDomain assetBundlesURL) : base(world, streamingAssetURL, assetBundlesURL) { } protected override void Update(float t) { @@ -33,9 +27,6 @@ protected override void Update(float t) // If loading is not started yet and there is no result private new void PrepareCommonArguments(in Entity entity, ref GetAssetBundleIntention assetBundleIntention, ref StreamableLoadingState state) { - assetBundleIntention.AssetBundleManifestVersion = sceneData.SceneEntityDefinition.assetBundleManifestVersion; - assetBundleIntention.ParentEntityID = sceneData.SceneEntityDefinition.id; - base.PrepareCommonArguments(in entity, ref assetBundleIntention, ref state); } diff --git a/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/AssetBundles/PrepareAssetBundleLoadingParametersSystemBase.cs b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/AssetBundles/PrepareAssetBundleLoadingParametersSystemBase.cs index a527a6dbb6c..f423fb8a4f1 100644 --- a/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/AssetBundles/PrepareAssetBundleLoadingParametersSystemBase.cs +++ b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/AssetBundles/PrepareAssetBundleLoadingParametersSystemBase.cs @@ -5,8 +5,6 @@ using ECS.StreamableLoading.Common.Components; using System; using System.Linq; -using DCL.Platforms; -using UnityEngine; using Utility; namespace ECS.StreamableLoading.AssetBundles @@ -51,7 +49,7 @@ protected void PrepareCommonArguments(in Entity entity, ref GetAssetBundleIntent // Second priority if (EnumUtils.HasFlag(assetBundleIntention.CommonArguments.PermittedSources, AssetSource.WEB)) { - if (assetBundleIntention.AssetBundleManifestVersion == null || assetBundleIntention.AssetBundleManifestVersion.assetBundleManifestRequestFailed) + if (assetBundleIntention.AssetBundleManifest.assetBundleManifestRequestFailed) { World.Add(entity, new StreamableLoadingResult (GetReportCategory(), CreateException(new ArgumentException($"Manifest version must be provided to load {assetBundleIntention.Name} from `WEB` source")))); @@ -63,23 +61,12 @@ protected void PrepareCommonArguments(in Entity entity, ref GetAssetBundleIntent ca.Attempts = StreamableLoadingDefaults.ATTEMPTS_COUNT; ca.Timeout = StreamableLoadingDefaults.TIMEOUT; ca.CurrentSource = AssetSource.WEB; - assetBundleIntention.Hash = assetBundleIntention.AssetBundleManifestVersion.CheckCasing(assetBundleIntention.Hash); - ca.URL = GetAssetBundleURL(assetBundleIntention.AssetBundleManifestVersion.HasHashInPath(), assetBundleIntention.Hash, assetBundleIntention.ParentEntityID, assetBundleIntention.AssetBundleManifestVersion.GetAssetBundleManifestVersion()); + + // Hash was already translated to the CDN file name (digest and Qm casing) at intention creation via GetCdnRequestHash. + ca.URL = assetBundlesURL.Append(new URLPath(assetBundleIntention.AssetBundleManifest.GetCdnRequestPath(assetBundleIntention.Hash, assetBundleIntention.ParentEntityID))); assetBundleIntention.CommonArguments = ca; - // DepsDigest is pre-populated upstream (e.g. PrepareGltfAssetLoadingSystem) from the bare hash, - // before the platform suffix is appended. The digest map itself is also keyed by bare hashes, so - // any path that wants digest-based keying must set DepsDigest before reaching this system. - // - // Dispatch on the manifest version, not on whether DepsDigest happens to be populated. A v49+ - // leaf AB that isn't listed in the manifest's deps map has an empty digest — routing it through - // the legacy path would key it on buildDate and prevent disk-cache sharing across CDN republishes. - assetBundleIntention.cacheHash = assetBundleIntention.AssetBundleManifestVersion.SupportsDepsDigests() - ? ComputeHashV49(assetBundleIntention.Hash, - assetBundleIntention.AssetBundleManifestVersion.GetAssetBundleManifestVersion(), - assetBundleIntention.DepsDigest ?? string.Empty) - : ComputeHashLegacy(assetBundleIntention.Hash, - assetBundleIntention.AssetBundleManifestVersion.GetAssetBundleManifestBuildDate()); + assetBundleIntention.cacheHash = assetBundleIntention.AssetBundleManifest.ComputeCacheHash(assetBundleIntention.Hash); } } @@ -91,47 +78,5 @@ private URLAddress GetStreamingAssetsUrl(string hash, URLSubdirectory customSubd ? streamingAssetURL.Append(URLPath.FromString(hash)) : streamingAssetURL.Append(customSubdirectory).Append(URLPath.FromString(hash)); - public static unsafe Hash128 ComputeHashLegacy(string hash, string buildDate) - { - // Byte-identical to the pre-v49 cache key so existing Unity-AB-cache entries keep hitting after upgrade. - // The lack of a delimiter is a known theoretical collision risk (e.g. buildDate ending in 'X' vs. hash - // starting with 'X') — accepted here, will be addressed when v49 adoption lets us retire this path. - Span hashBuilder = stackalloc char[buildDate.Length + hash.Length]; - buildDate.AsSpan().CopyTo(hashBuilder); - hash.AsSpan().CopyTo(hashBuilder[buildDate.Length..]); - - fixed (char* ptr = hashBuilder) { return Hash128.Compute(ptr, (uint)(sizeof(char) * hashBuilder.Length)); } - } - - public static unsafe Hash128 ComputeHashV49(string hash, string version, string depsDigest) - { - // The per-file deps digest replaces the buildDate sledgehammer that was previously used to invalidate - // the cache whenever a dependency might have changed. Keying on (version + hash + digest) lets the disk - // cache stay shareable across CDN republishes when the dependency closure is unchanged. depsDigest may - // be empty for leaf ABs that aren't listed in the manifest's deps map — that's a valid input here. - ReadOnlySpan hashSpan = hash.AsSpan(); - ReadOnlySpan versionSpan = version.AsSpan(); - ReadOnlySpan digestSpan = depsDigest.AsSpan(); - - Span builder = stackalloc char[versionSpan.Length + 1 + hashSpan.Length + 1 + digestSpan.Length]; - versionSpan.CopyTo(builder); - int offset = versionSpan.Length; - builder[offset++] = '|'; - hashSpan.CopyTo(builder[offset..]); - offset += hashSpan.Length; - builder[offset++] = '|'; - digestSpan.CopyTo(builder[offset..]); - - fixed (char* ptr = builder) { return Hash128.Compute(ptr, (uint)(sizeof(char) * builder.Length)); } - } - - private URLAddress GetAssetBundleURL(bool hasSceneIDInPath, string hash, string sceneID, string assetBundleManifestVersion) - { - if (hasSceneIDInPath) - return assetBundlesURL.Append(new URLPath($"{assetBundleManifestVersion}/{sceneID}/{hash}")); - - return assetBundlesURL.Append(new URLPath($"{assetBundleManifestVersion}/{hash}")); - } - } } diff --git a/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/AssetBundles/SceneAssetBundleDigestsLoader.cs b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/AssetBundles/SceneAssetBundleDigestsLoader.cs index aa576d3d1f2..51f9fcd451a 100644 --- a/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/AssetBundles/SceneAssetBundleDigestsLoader.cs +++ b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/AssetBundles/SceneAssetBundleDigestsLoader.cs @@ -22,10 +22,10 @@ public static class SceneAssetBundleDigestsLoader { public static async UniTask EnsureDepsDigestsAsync(World world, EntityDefinitionBase entityDefinition, IPartitionComponent partition, CancellationToken ct) { - AssetBundleManifestVersion? manifestVersion = entityDefinition.assetBundleManifestVersion; + AssetBundleManifestVersion manifestVersion = entityDefinition.AssetBundleManifestVersionOrFailed; - // Pre-v49 manifests have no deps digest, and CreateFailed/CreateManualManifest entries report version < v49 as well — short-circuit those. - if (manifestVersion == null || !manifestVersion.SupportsDepsDigests()) + // Pre-v49 manifests have no canonical assets/ layout — skip both the manifest download and the injection. + if (!manifestVersion.SupportsDepsDigests()) return; //Needed to use the UnityEngine.Time.realtimeSinceStartup on the intention creation diff --git a/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/AssetBundles/Tests/EditModeTests/DepsDigestCacheKeyShould.cs b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/AssetBundles/Tests/EditModeTests/DepsDigestCacheKeyShould.cs index fb67badf243..dfca17d5017 100644 --- a/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/AssetBundles/Tests/EditModeTests/DepsDigestCacheKeyShould.cs +++ b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/AssetBundles/Tests/EditModeTests/DepsDigestCacheKeyShould.cs @@ -1,4 +1,5 @@ using DCL.Ipfs; +using DCL.Utility; using ECS.StreamableLoading.Cache.Disk; using ECS.Unity.GLTFContainer.Asset.Components; using NUnit.Framework; @@ -12,261 +13,263 @@ public class DepsDigestCacheKeyShould private const string DIGEST_A = "dda1af30bdf4a19ce03e663a9a288afe"; private const string DIGEST_B = "243f68977939e1f526b4c1a05a40b43a"; - private bool depsDigestKeyingEnabledBackup; + private const string HASH_A = "bafkreif5xmg4un7cm4ouyqfoluc6ifcdouiatassnv5pykell4e4mw5xc4"; + private const string HASH_B = "bafkreice3qpeyeb4ni7fnlt6bijs57zrbuw7cwmbymzcndyllzho3hbgaa"; + private const string HASH_LEGACY = "bafybeih4xx65yycsf2vx6sari7myjho6rugqox4ocd2tzjhfam73g2trru"; - [OneTimeSetUp] - public void EnableDepsDigestKeyingForFixture() - { - // The production gate is off until the feature flag flips it from bootstrap. Tests exercise the v49 - // code paths directly, so opt in for the lifetime of the fixture and restore in teardown. - depsDigestKeyingEnabledBackup = AssetBundleManifestVersion.DepsDigestKeyingEnabled; - AssetBundleManifestVersion.DepsDigestKeyingEnabled = true; - } + private static readonly AssetBundleManifestVersion ANY_MANIFEST = AssetBundleManifestVersion.CreateFromFallback("v49", "2026-05-01"); - [OneTimeTearDown] - public void RestoreDepsDigestKeying() + private static AssetBundleManifestVersion CreateV49Manifest(params string[] files) { - AssetBundleManifestVersion.DepsDigestKeyingEnabled = depsDigestKeyingEnabledBackup; + var manifest = AssetBundleManifestVersion.CreateFromFallback("v49", "2026-05-01"); + manifest.InjectDepsDigests(files); + return manifest; } [Test] - public void InjectDepsDigestsFromThreePartFilenames() + public void TranslateBareHashToCdnRequestHash() { - string[] files = - { - "bafybeih4xx65yycsf2vx6sari7myjho6rugqox4ocd2tzjhfam73g2trru_mac", - $"bafkreif5xmg4un7cm4ouyqfoluc6ifcdouiatassnv5pykell4e4mw5xc4_{DIGEST_A}_mac", - $"bafkreice3qpeyeb4ni7fnlt6bijs57zrbuw7cwmbymzcndyllzho3hbgaa_{DIGEST_B}_mac", - }; + string platform = PlatformUtils.GetCurrentPlatform(); - var manifest = AssetBundleManifestVersion.CreateFromFallback("v49", "2026-05-01"); - manifest.InjectDepsDigests(files); + AssetBundleManifestVersion manifest = CreateV49Manifest( + $"{HASH_LEGACY}{platform}", + $"{HASH_A}_{DIGEST_A}{platform}"); + + Assert.That(manifest.GetCdnRequestHash(HASH_A), Is.EqualTo($"{HASH_A}_{DIGEST_A}{platform}")); - Assert.That(manifest.TryGetDepsDigest("bafybeih4xx65yycsf2vx6sari7myjho6rugqox4ocd2tzjhfam73g2trru", out _), Is.False, "Legacy 2-part filenames must not contribute a digest"); - Assert.That(manifest.TryGetDepsDigest("bafkreif5xmg4un7cm4ouyqfoluc6ifcdouiatassnv5pykell4e4mw5xc4", out string digestA), Is.True); - Assert.That(digestA, Is.EqualTo(DIGEST_A)); - Assert.That(manifest.TryGetDepsDigest("bafkreice3qpeyeb4ni7fnlt6bijs57zrbuw7cwmbymzcndyllzho3hbgaa", out string digestB), Is.True); - Assert.That(digestB, Is.EqualTo(DIGEST_B)); + Assert.That(manifest.GetCdnRequestHash(HASH_LEGACY), Is.EqualTo($"{HASH_LEGACY}{platform}"), + "Files listed without a digest resolve to their verbatim 2-part manifest name"); + + Assert.That(manifest.GetCdnRequestHash(HASH_B), Is.EqualTo($"{HASH_B}{platform}"), + "Hashes absent from the manifest must fall back to the platform-suffixed bare hash"); } [Test] - public void TreatIntentionsWithDifferentDigestsAsDistinct() + public void TranslateBareHashCaseInsensitivelyToManifestCasing() { - var sameHash = "bafkreif5xmg4un7cm4ouyqfoluc6ifcdouiatassnv5pykell4e4mw5xc4"; - var a = GetAssetBundleIntention.FromHash(sameHash); - a.DepsDigest = DIGEST_A; - var b = GetAssetBundleIntention.FromHash(sameHash); - b.DepsDigest = DIGEST_B; + string platform = PlatformUtils.GetCurrentPlatform(); - Assert.That(a.Equals(b), Is.False); - Assert.That(a.GetHashCode(), Is.Not.EqualTo(b.GetHashCode())); + AssetBundleManifestVersion manifest = CreateV49Manifest($"qmabrb8wisg9b4szzt6achgajdyultejpzmtwdi4rcetzv_{DIGEST_A}{platform}"); + + // The lookup is case-insensitive and returns the manifest's casing — the name that exists on the case-sensitive CDN. + Assert.That(manifest.GetCdnRequestHash("QmaBrb8WisG9b4Szzt6ACHgaJdyULTEjpzmTwDi4RCEtZV"), + Is.EqualTo($"qmabrb8wisg9b4szzt6achgajdyultejpzmtwdi4rcetzv_{DIGEST_A}{platform}")); } [Test] - public void TreatIntentionsWithSameHashAndDigestAsEqual() + public void RouteReusableAssetsThroughTheSharedAssetsPath() { - var sameHash = "bafkreif5xmg4un7cm4ouyqfoluc6ifcdouiatassnv5pykell4e4mw5xc4"; - var a = GetAssetBundleIntention.FromHash(sameHash); - a.DepsDigest = DIGEST_A; - var b = GetAssetBundleIntention.FromHash(sameHash); - b.DepsDigest = DIGEST_A; + const string SCENE_ID = "sceneId"; - Assert.That(a.Equals(b), Is.True); - Assert.That(a.GetHashCode(), Is.EqualTo(b.GetHashCode())); + // Only scenes fetch files[], and only reuse-converted bundles live under the shared assets/ prefix; + // manifests without injected files keep the entity path. + var withoutFiles = AssetBundleManifestVersion.CreateFromFallback("v49", "2026-05-01"); + Assert.That(withoutFiles.GetCdnRequestPath(HASH_A, SCENE_ID), Is.EqualTo($"v49/{SCENE_ID}/{HASH_A}")); + + AssetBundleManifestVersion withDigestFiles = CreateV49Manifest($"{HASH_A}_{DIGEST_A}_mac"); + Assert.That(withDigestFiles.GetCdnRequestPath(HASH_A, SCENE_ID), Is.EqualTo($"v49/assets/{HASH_A}")); + + // Any injected files[] counts — a reuse-converted scene can legitimately list only 2-part names. + AssetBundleManifestVersion onlyLegacyFiles = CreateV49Manifest($"{HASH_LEGACY}_mac"); + Assert.That(onlyLegacyFiles.GetCdnRequestPath(HASH_LEGACY, SCENE_ID), Is.EqualTo($"v49/assets/{HASH_LEGACY}")); } [Test] - public void PreserveLegacyEqualityWhenNoDigestPresent() + public void ProvideFailedManifestWhenDefinitionHasNone() { - // Two intentions with the same hash and no digest must still match — preserves cache hits for pre-v49 entries. - var sameHash = "bafybeih4xx65yycsf2vx6sari7myjho6rugqox4ocd2tzjhfam73g2trru"; - var a = GetAssetBundleIntention.FromHash(sameHash); - var b = GetAssetBundleIntention.FromHash(sameHash); + // AB intentions require a manifest: definitions without one hand out the failed sentinel, + // which the loading pipeline already treats as a dead end. + var definition = new SceneEntityDefinition(); - Assert.That(a.Equals(b), Is.True); - Assert.That(a.GetHashCode(), Is.EqualTo(b.GetHashCode())); + Assert.That(definition.assetBundleManifestVersion, Is.Null); + Assert.That(definition.AssetBundleManifestVersionOrFailed.assetBundleManifestRequestFailed, Is.True); } [Test] - public void ProduceDistinctDiskFilenamesForDifferentDigests() + public void ComposeCacheKey_FallsBackToBareHashWhenNoDigest() { - var sameHash = "bafkreif5xmg4un7cm4ouyqfoluc6ifcdouiatassnv5pykell4e4mw5xc4"; - var a = GetAssetBundleIntention.FromHash(sameHash); - a.DepsDigest = DIGEST_A; - var b = GetAssetBundleIntention.FromHash(sameHash); - b.DepsDigest = DIGEST_B; - - using var keyA = GetAssetBundleIntention.DiskHashCompute.INSTANCE.ComputeHash(in a); - using var keyB = GetAssetBundleIntention.DiskHashCompute.INSTANCE.ComputeHash(in b); - - string nameA = HashNamings.HashNameFrom(keyA, ".ab"); - string nameB = HashNamings.HashNameFrom(keyB, ".ab"); - - Assert.That(nameA, Is.Not.EqualTo(nameB)); + AssetBundleManifestVersion manifest = AssetBundleManifestVersion.CreateFromFallback("v49", "2026-05-01"); + Assert.That(manifest.ComposeCacheKey(HASH_A), Is.EqualTo(HASH_A)); } [Test] - public void PreserveLegacyDiskFilenameWhenNoDigest() + public void ComposeCacheKey_ReturnsVerbatimFileNameWhenPresent() { - // An intention without a digest must produce the same on-disk file name as before this change so existing - // cached entries keep hitting after upgrade. - var sameHash = "bafybeih4xx65yycsf2vx6sari7myjho6rugqox4ocd2tzjhfam73g2trru"; - var legacy = GetAssetBundleIntention.FromHash(sameHash); - var alsoLegacy = GetAssetBundleIntention.FromHash(sameHash); + string platform = PlatformUtils.GetCurrentPlatform(); + AssetBundleManifestVersion manifest = CreateV49Manifest($"{HASH_A}_{DIGEST_A}{platform}"); - using var keyA = GetAssetBundleIntention.DiskHashCompute.INSTANCE.ComputeHash(in legacy); - using var keyB = GetAssetBundleIntention.DiskHashCompute.INSTANCE.ComputeHash(in alsoLegacy); - - Assert.That(HashNamings.HashNameFrom(keyA, ".ab"), Is.EqualTo(HashNamings.HashNameFrom(keyB, ".ab"))); + Assert.That(manifest.ComposeCacheKey(HASH_A), Is.EqualTo($"{HASH_A}_{DIGEST_A}{platform}")); } [Test] - public void GltfIntentionDefaultsCacheKeyToHash() + public void ResolveQmContentCasingThroughTheSameMap() { - const string hash = "bafkreif5xmg4un7cm4ouyqfoluc6ifcdouiatassnv5pykell4e4mw5xc4"; - var intention = new GetGltfContainerAssetIntention("model.glb", hash, new CancellationTokenSource()); + string platform = PlatformUtils.GetCurrentPlatform(); + var manifest = AssetBundleManifestVersion.CreateFromFallback("v35", "2026-05-01"); + + manifest.InjectContent("Qmf7DaJZRygoayfNn5Jq6QAykrhFpQUr2us2VFvjREiajk", + new[] { new ContentDefinition { file = "model.glb", hash = "QmaBrb8WisG9b4Szzt6ACHgaJdyULTEjpzmTwDi4RCEtZV" } }); + + Assert.That(manifest.GetCdnRequestPath(HASH_A, "sceneId"), Is.EqualTo($"v35/sceneId/{HASH_A}"), + "Content casing entries must not switch the URL shape or cache keying to the reusable scheme"); - Assert.That(intention.CacheKey, Is.EqualTo(hash), "Legacy callers that don't supply a cache key must default to the bare hash"); + Assert.That(manifest.GetCdnRequestHash("QmaBrb8WisG9b4Szzt6ACHgaJdyULTEjpzmTwDi4RCEtZV"), + Is.EqualTo($"qmabrb8wisg9b4szzt6achgajdyultejpzmtwdi4rcetzv{platform}").IgnoreCase); } [Test] - public void GltfIntentionStoresPassedCacheKeyVerbatim() + public void PreferDigestEntriesOverContentCasingEntries() { - const string hash = "bafkreif5xmg4un7cm4ouyqfoluc6ifcdouiatassnv5pykell4e4mw5xc4"; - string customKey = $"{hash}@{DIGEST_A}"; - var intention = new GetGltfContainerAssetIntention("model.glb", hash, new CancellationTokenSource(), customKey); - - Assert.That(intention.CacheKey, Is.EqualTo(customKey)); + string platform = PlatformUtils.GetCurrentPlatform(); + const string QM_ENTITY_ID = "Qmf7DaJZRygoayfNn5Jq6QAykrhFpQUr2us2VFvjREiajk"; + const string QM_HASH = "qmabrb8wisg9b4szzt6achgajdyultejpzmtwdi4rcetzv"; + + // Regardless of injection order, the digest-bearing manifest entry wins over the casing entry. + var contentFirst = AssetBundleManifestVersion.CreateFromFallback("v49", "2026-05-01"); + contentFirst.InjectContent(QM_ENTITY_ID, new[] { new ContentDefinition { file = "model.glb", hash = QM_HASH } }); + contentFirst.InjectDepsDigests(new[] { $"{QM_HASH}_{DIGEST_A}{platform}" }); + Assert.That(contentFirst.GetCdnRequestHash(QM_HASH), Is.EqualTo($"{QM_HASH}_{DIGEST_A}{platform}")); + + var digestsFirst = AssetBundleManifestVersion.CreateFromFallback("v49", "2026-05-01"); + digestsFirst.InjectDepsDigests(new[] { $"{QM_HASH}_{DIGEST_A}{platform}" }); + digestsFirst.InjectContent(QM_ENTITY_ID, new[] { new ContentDefinition { file = "model.glb", hash = QM_HASH } }); + Assert.That(digestsFirst.GetCdnRequestHash(QM_HASH), Is.EqualTo($"{QM_HASH}_{DIGEST_A}{platform}")); } [Test] - public void GltfIntentionsWithDifferentCacheKeysAreDistinct() + public void TreatIntentionsWithDifferentDigestBearingHashesAsDistinct() { - const string hash = "bafkreif5xmg4un7cm4ouyqfoluc6ifcdouiatassnv5pykell4e4mw5xc4"; - var a = new GetGltfContainerAssetIntention("model.glb", hash, new CancellationTokenSource(), $"{hash}@{DIGEST_A}"); - var b = new GetGltfContainerAssetIntention("model.glb", hash, new CancellationTokenSource(), $"{hash}@{DIGEST_B}"); + // The digest is part of the Hash itself, so two dependency closures of the same bare hash never collide. + var a = GetAssetBundleIntention.FromHash($"{HASH_A}_{DIGEST_A}_mac", ANY_MANIFEST); + var b = GetAssetBundleIntention.FromHash($"{HASH_A}_{DIGEST_B}_mac", ANY_MANIFEST); - Assert.That(a.CacheKey, Is.Not.EqualTo(b.CacheKey)); Assert.That(a.Equals(b), Is.False); + Assert.That(a.GetHashCode(), Is.Not.EqualTo(b.GetHashCode())); } [Test] - public void ComposeCacheKey_FallsBackToBareHashWhenManifestIsNull() + public void TreatIntentionsWithSameHashAsEqual() { - AssetBundleManifestVersion? manifest = null; - Assert.That(manifest.ComposeCacheKey("X"), Is.EqualTo("X")); + var a = GetAssetBundleIntention.FromHash($"{HASH_A}_{DIGEST_A}_mac", ANY_MANIFEST); + var b = GetAssetBundleIntention.FromHash($"{HASH_A}_{DIGEST_A}_mac", ANY_MANIFEST); + + Assert.That(a.Equals(b), Is.True); + Assert.That(a.GetHashCode(), Is.EqualTo(b.GetHashCode())); } [Test] - public void ComposeCacheKey_FallsBackToBareHashWhenNoDigest() + public void ProduceDistinctDiskFilenamesForDifferentDigestBearingHashes() { - var manifest = AssetBundleManifestVersion.CreateFromFallback("v49", "2026-05-01"); - Assert.That(manifest.ComposeCacheKey("X"), Is.EqualTo("X")); + var a = GetAssetBundleIntention.FromHash($"{HASH_A}_{DIGEST_A}_mac", ANY_MANIFEST); + var b = GetAssetBundleIntention.FromHash($"{HASH_A}_{DIGEST_B}_mac", ANY_MANIFEST); + + using var keyA = GetAssetBundleIntention.DiskHashCompute.INSTANCE.ComputeHash(in a); + using var keyB = GetAssetBundleIntention.DiskHashCompute.INSTANCE.ComputeHash(in b); + + Assert.That(HashNamings.HashNameFrom(keyA, ".ab"), Is.Not.EqualTo(HashNamings.HashNameFrom(keyB, ".ab"))); } [Test] - public void ComposeCacheKey_AppendsDigestWhenPresent() + public void PreserveLegacyDiskFilenameForDigestLessHashes() { - var manifest = AssetBundleManifestVersion.CreateFromFallback("v49", "2026-05-01"); - manifest.InjectDepsDigests(new[] { $"X_{DIGEST_A}_mac" }); + // A digest-less hash must keep its pre-scheme on-disk file name so existing cached entries keep hitting. + var legacy = GetAssetBundleIntention.FromHash(HASH_LEGACY, ANY_MANIFEST); + var alsoLegacy = GetAssetBundleIntention.FromHash(HASH_LEGACY, ANY_MANIFEST); - Assert.That(manifest.ComposeCacheKey("X"), Is.EqualTo($"X@{DIGEST_A}")); + using var keyA = GetAssetBundleIntention.DiskHashCompute.INSTANCE.ComputeHash(in legacy); + using var keyB = GetAssetBundleIntention.DiskHashCompute.INSTANCE.ComputeHash(in alsoLegacy); + + Assert.That(HashNamings.HashNameFrom(keyA, ".ab"), Is.EqualTo(HashNamings.HashNameFrom(keyB, ".ab"))); } [Test] - public void V49HashIsStableForSameInputs() + public void GltfIntentionDefaultsCacheKeyToHash() { - const string hash = "bafkreif5xmg4un7cm4ouyqfoluc6ifcdouiatassnv5pykell4e4mw5xc4"; - Hash128 a = PrepareAssetBundleLoadingParametersSystemBase.ComputeHashV49(hash, "v49", DIGEST_A); - Hash128 b = PrepareAssetBundleLoadingParametersSystemBase.ComputeHashV49(hash, "v49", DIGEST_A); + var intention = new GetGltfContainerAssetIntention("model.glb", HASH_A, new CancellationTokenSource()); - Assert.That(a, Is.EqualTo(b)); + Assert.That(intention.CacheKey, Is.EqualTo(HASH_A), "Legacy callers that don't supply a cache key must default to the bare hash"); } [Test] - public void V49HashAcceptsEmptyDigestForLeafBundles() + public void GltfIntentionStoresPassedCacheKeyVerbatim() { - // v49+ leaf ABs that aren't listed in the manifest's deps map carry an empty digest. They must still - // produce a deterministic key — and crucially, one that doesn't depend on buildDate. - const string hash = "bafkreif5xmg4un7cm4ouyqfoluc6ifcdouiatassnv5pykell4e4mw5xc4"; - Hash128 a = PrepareAssetBundleLoadingParametersSystemBase.ComputeHashV49(hash, "v49", string.Empty); - Hash128 b = PrepareAssetBundleLoadingParametersSystemBase.ComputeHashV49(hash, "v49", string.Empty); + string customKey = $"{HASH_A}_{DIGEST_A}_mac"; + var intention = new GetGltfContainerAssetIntention("model.glb", HASH_A, new CancellationTokenSource(), customKey); - Assert.That(a, Is.EqualTo(b)); + Assert.That(intention.CacheKey, Is.EqualTo(customKey)); } [Test] - public void V49HashDiffersWhenDigestDiffers() + public void GltfIntentionsWithDifferentCacheKeysAreDistinct() { - const string hash = "bafkreif5xmg4un7cm4ouyqfoluc6ifcdouiatassnv5pykell4e4mw5xc4"; - Hash128 a = PrepareAssetBundleLoadingParametersSystemBase.ComputeHashV49(hash, "v49", DIGEST_A); - Hash128 b = PrepareAssetBundleLoadingParametersSystemBase.ComputeHashV49(hash, "v49", DIGEST_B); + var a = new GetGltfContainerAssetIntention("model.glb", HASH_A, new CancellationTokenSource(), $"{HASH_A}_{DIGEST_A}_mac"); + var b = new GetGltfContainerAssetIntention("model.glb", HASH_A, new CancellationTokenSource(), $"{HASH_A}_{DIGEST_B}_mac"); - Assert.That(a, Is.Not.EqualTo(b)); + Assert.That(a.CacheKey, Is.Not.EqualTo(b.CacheKey)); + Assert.That(a.Equals(b), Is.False); } [Test] - public void V49HashDiffersBetweenEmptyAndNonEmptyDigest() + public void ComputeStableCacheHashForSameInputs() { - // A v49+ leaf AB (no digest) and a v49+ AB with a digest must not share a cache key, even though - // both go through the v49 path — the digest is a real discriminator and absence is meaningful. - const string hash = "bafkreif5xmg4un7cm4ouyqfoluc6ifcdouiatassnv5pykell4e4mw5xc4"; - Hash128 leaf = PrepareAssetBundleLoadingParametersSystemBase.ComputeHashV49(hash, "v49", string.Empty); - Hash128 withDigest = PrepareAssetBundleLoadingParametersSystemBase.ComputeHashV49(hash, "v49", DIGEST_A); + AssetBundleManifestVersion sceneManifest = CreateV49Manifest($"{HASH_A}_{DIGEST_A}_mac"); + Assert.That(sceneManifest.ComputeCacheHash($"{HASH_A}_{DIGEST_A}_mac"), Is.EqualTo(sceneManifest.ComputeCacheHash($"{HASH_A}_{DIGEST_A}_mac"))); - Assert.That(leaf, Is.Not.EqualTo(withDigest)); + AssetBundleManifestVersion legacyManifest = AssetBundleManifestVersion.CreateFromFallback("v48", "2026-05-01"); + Assert.That(legacyManifest.ComputeCacheHash(HASH_LEGACY), Is.EqualTo(legacyManifest.ComputeCacheHash(HASH_LEGACY))); } [Test] - public void V49HashDiffersWhenHashDiffers() + public void ComputeDifferentCacheHashWhenDigestBearingHashDiffers() { - Hash128 a = PrepareAssetBundleLoadingParametersSystemBase.ComputeHashV49("hashA", "v49", DIGEST_A); - Hash128 b = PrepareAssetBundleLoadingParametersSystemBase.ComputeHashV49("hashB", "v49", DIGEST_A); + // The digest travels inside the hash, so two dependency closures produce different Unity-cache keys. + AssetBundleManifestVersion manifest = CreateV49Manifest($"{HASH_A}_{DIGEST_A}_mac", $"{HASH_A}_{DIGEST_B}_mac"); - Assert.That(a, Is.Not.EqualTo(b)); + Assert.That(manifest.ComputeCacheHash($"{HASH_A}_{DIGEST_A}_mac"), Is.Not.EqualTo(manifest.ComputeCacheHash($"{HASH_A}_{DIGEST_B}_mac"))); } [Test] - public void V49HashDiffersWhenVersionDiffers() + public void ComputeDifferentCacheHashWhenVersionDiffers() { - const string hash = "bafkreif5xmg4un7cm4ouyqfoluc6ifcdouiatassnv5pykell4e4mw5xc4"; - Hash128 a = PrepareAssetBundleLoadingParametersSystemBase.ComputeHashV49(hash, "v49", DIGEST_A); - Hash128 b = PrepareAssetBundleLoadingParametersSystemBase.ComputeHashV49(hash, "v50", DIGEST_A); + AssetBundleManifestVersion v49 = CreateV49Manifest($"{HASH_A}_{DIGEST_A}_mac"); + + var v50 = AssetBundleManifestVersion.CreateFromFallback("v50", "2026-05-01"); + v50.InjectDepsDigests(new[] { $"{HASH_A}_{DIGEST_A}_mac" }); - Assert.That(a, Is.Not.EqualTo(b)); + Assert.That(v49.ComputeCacheHash($"{HASH_A}_{DIGEST_A}_mac"), Is.Not.EqualTo(v50.ComputeCacheHash($"{HASH_A}_{DIGEST_A}_mac"))); } [Test] - public void V49DelimiterPreventsBoundaryCollisions() + public void ComputeDifferentCacheHashAcrossVersionHashBoundary() { - // Without delimiters, (version="v4", hash="9foo") and (version="v49", hash="foo") would produce the - // same byte stream. - Hash128 a = PrepareAssetBundleLoadingParametersSystemBase.ComputeHashV49("9foo", "v4", string.Empty); - Hash128 b = PrepareAssetBundleLoadingParametersSystemBase.ComputeHashV49("foo", "v49", string.Empty); + // Without the delimiter, (version="v49", hash="0foo") and (version="v490", hash="foo") would produce the same byte stream. + AssetBundleManifestVersion v49 = CreateV49Manifest($"0foo_{DIGEST_A}_mac"); - Assert.That(a, Is.Not.EqualTo(b)); + var v490 = AssetBundleManifestVersion.CreateFromFallback("v490", "2026-05-01"); + v490.InjectDepsDigests(new[] { $"foo_{DIGEST_A}_mac" }); + + Assert.That(v49.ComputeCacheHash("0foo"), Is.Not.EqualTo(v490.ComputeCacheHash("foo"))); } [Test] - public void LegacyHashIsStableForSameInputs() + public void ComputeCacheHashFromBuildDateWithoutDepsMap() { - const string hash = "bafybeih4xx65yycsf2vx6sari7myjho6rugqox4ocd2tzjhfam73g2trru"; - Hash128 a = PrepareAssetBundleLoadingParametersSystemBase.ComputeHashLegacy(hash, "2026-05-01"); - Hash128 b = PrepareAssetBundleLoadingParametersSystemBase.ComputeHashLegacy(hash, "2026-05-01"); + // Manifests without a deps map (wearables/emotes, pre-v49 scenes) key on buildDate — a republish must flush their cache. + var a = AssetBundleManifestVersion.CreateFromFallback("v49", "2026-05-01"); + var b = AssetBundleManifestVersion.CreateFromFallback("v49", "2026-05-02"); - Assert.That(a, Is.EqualTo(b)); + Assert.That(a.ComputeCacheHash(HASH_LEGACY), Is.Not.EqualTo(b.ComputeCacheHash(HASH_LEGACY))); } [Test] - public void LegacyHashChangesWithBuildDate() + public void ComputeDistinctCacheHashesAcrossKeyingSchemes() { - // Pre-v49 ABs have no per-file freshness signal, so buildDate is the only thing that flushes the cache - // when dependencies might have changed — verify it is actually contributing to the key. - const string hash = "bafybeih4xx65yycsf2vx6sari7myjho6rugqox4ocd2tzjhfam73g2trru"; - Hash128 a = PrepareAssetBundleLoadingParametersSystemBase.ComputeHashLegacy(hash, "2026-05-01"); - Hash128 b = PrepareAssetBundleLoadingParametersSystemBase.ComputeHashLegacy(hash, "2026-05-02"); + // A mapped and an unmapped manifest must not produce the same key for the same hash, even when the + // legacy buildDate happens to equal the v49 version string. + AssetBundleManifestVersion mapped = CreateV49Manifest($"{HASH_A}_{DIGEST_A}_mac"); + var unmapped = AssetBundleManifestVersion.CreateFromFallback("v49", "v49"); - Assert.That(a, Is.Not.EqualTo(b)); + Assert.That(mapped.ComputeCacheHash(HASH_A), Is.Not.EqualTo(unmapped.ComputeCacheHash(HASH_A))); } [Test] @@ -275,12 +278,11 @@ public void VersionPredicates_DoNotThrowOnNonVNVersions() //Check that when handling a LOD it doesn't throw var lodManifest = AssetBundleManifestVersion.CreateForLOD("LOD/0", "dummyDate"); - Assert.That(lodManifest.HasHashInPath(), Is.False); + Assert.That(lodManifest.GetCdnRequestPath(HASH_A, "sceneId"), Is.EqualTo($"LOD/0/{HASH_A}"), "Unparseable versions must keep the flat legacy path"); Assert.That(lodManifest.SupportsDepsDigests(), Is.False); var manualManifest = AssetBundleManifestVersion.CreateManualManifest(); - Assert.That(manualManifest.HasHashInPath(), Is.False); Assert.That(manualManifest.SupportsDepsDigests(), Is.False); var wrongString = AssetBundleManifestVersion.CreateFromFallback("v", "dummyDate"); @@ -289,48 +291,5 @@ public void VersionPredicates_DoNotThrowOnNonVNVersions() var nonNumeric = AssetBundleManifestVersion.CreateFromFallback("vfoo", "dummyDate"); Assert.That(nonNumeric.SupportsDepsDigests(), Is.False); } - - [Test] - public void V49AndLegacyDoNotCollideForSameHash() - { - // A v49+ leaf AB with no digest must not accidentally produce the same Hash128 as the legacy path for - // the same bare hash, even if the legacy buildDate happens to equal the v49 version string. - const string hash = "bafkreif5xmg4un7cm4ouyqfoluc6ifcdouiatassnv5pykell4e4mw5xc4"; - Hash128 legacy = PrepareAssetBundleLoadingParametersSystemBase.ComputeHashLegacy(hash, "v49"); - Hash128 v49 = PrepareAssetBundleLoadingParametersSystemBase.ComputeHashV49(hash, "v49", string.Empty); - - Assert.That(legacy, Is.Not.EqualTo(v49)); - } - - [Test] - public void Gate_DisablesV49DigestSupportEvenForV49Manifests() - { - // With the kill-switch off, every manifest must report SupportsDepsDigests() == false so the dispatch - // in PrepareCommonArguments takes the legacy path for v49 traffic too. - AssetBundleManifestVersion.DepsDigestKeyingEnabled = false; - try - { - var manifest = AssetBundleManifestVersion.CreateFromFallback("v49", "2026-05-01"); - Assert.That(manifest.SupportsDepsDigests(), Is.False); - } - finally { AssetBundleManifestVersion.DepsDigestKeyingEnabled = true; } - } - - [Test] - public void Gate_SuppressesDigestLookupsAndInjection() - { - // With the kill-switch off, TryGetDepsDigest must not return digests even if InjectDepsDigests is - // called — guarantees ComposeCacheKey returns the bare hash and GLTF/AB cache keys stay legacy-shaped. - AssetBundleManifestVersion.DepsDigestKeyingEnabled = false; - try - { - var manifest = AssetBundleManifestVersion.CreateFromFallback("v49", "2026-05-01"); - manifest.InjectDepsDigests(new[] { $"X_{DIGEST_A}_mac" }); - - Assert.That(manifest.TryGetDepsDigest("X", out _), Is.False); - Assert.That(manifest.ComposeCacheKey("X"), Is.EqualTo("X")); - } - finally { AssetBundleManifestVersion.DepsDigestKeyingEnabled = true; } - } } } diff --git a/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/AssetBundles/Tests/LoadAssetBundlePartialSystemShould.cs b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/AssetBundles/Tests/LoadAssetBundlePartialSystemShould.cs index 29419f65ac2..0a1d106b358 100644 --- a/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/AssetBundles/Tests/LoadAssetBundlePartialSystemShould.cs +++ b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/AssetBundles/Tests/LoadAssetBundlePartialSystemShould.cs @@ -1,4 +1,5 @@ using AssetManagement; +using DCL.Ipfs; using Cysharp.Threading.Tasks; using DCL.DebugUtilities.UIBindings; using DCL.Multiplayer.Connections.DecentralandUrls; @@ -137,7 +138,7 @@ private ABPromise NewABPromiseRemoteAsset(int index) private ABPromise NewABPromise() { - var intention = GetAssetBundleIntention.FromHash("bafkreid3xecd44iujaz5qekbdrt5orqdqj3wivg5zc5mya3zkorjhyrkda", typeof(GameObject), permittedSources: AssetSource.WEB); + var intention = GetAssetBundleIntention.FromHash("bafkreid3xecd44iujaz5qekbdrt5orqdqj3wivg5zc5mya3zkorjhyrkda", AssetBundleManifestVersion.CreateManualManifest(), typeof(GameObject), permittedSources: AssetSource.WEB); var partition = PartitionComponent.TOP_PRIORITY; var assetPromise = ABPromise.Create(world, intention, partition); world.Get(assetPromise.Entity).SetAllowed(Substitute.For()); diff --git a/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/Bootstraper.cs b/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/Bootstraper.cs index 0d89dbe1791..351574143c4 100644 --- a/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/Bootstraper.cs +++ b/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/Bootstraper.cs @@ -5,7 +5,6 @@ using DCL.DebugUtilities; using DCL.Diagnostics; using DCL.FeatureFlags; -using DCL.Ipfs; using DCL.Multiplayer.Connections.DecentralandUrls; using DCL.Notifications.NewNotification; using DCL.Optimization.PerformanceBudgeting; @@ -223,10 +222,6 @@ public async UniTask InitializeFeatureFlagsAsync(IWeb3Identity? identity, IDecen public void InitializeFeaturesRegistry() { FeaturesRegistry.Initialize(new FeaturesRegistry(appArgs, realmLaunchSettings.CurrentMode is LaunchMode.LocalSceneDevelopment)); - - // Gate the v49 deps-digest cache-keying scheme behind the feature flag. Off by default means every - // manifest reports SupportsDepsDigests() == false and the entire pipeline takes the legacy code path. - AssetBundleManifestVersion.DepsDigestKeyingEnabled = FeaturesRegistry.Instance.IsEnabled(FeatureId.AB_DEPS_DIGEST_CACHE_KEY); } public GlobalWorld CreateGlobalWorld( diff --git a/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/GlobalWorldFactory.cs b/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/GlobalWorldFactory.cs index d33ee5e0a7b..231705f8f89 100644 --- a/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/GlobalWorldFactory.cs +++ b/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/GlobalWorldFactory.cs @@ -163,7 +163,7 @@ public GlobalWorld Create(ISceneFactory sceneFactory, Entity playerEntity) var assetBundleCdnUrl = URLDomain.FromString(urlsSource.Url(DecentralandUrl.AssetBundlesCDN)); var lodGeneratorCdnUrl = URLDomain.FromString(urlsSource.Url(DecentralandUrl.LodGeneratorCDN)); - LoadISSDescriptorSystem.InjectToWorld(ref builder, webRequestController, assetBundleCdnUrl, lodGeneratorCdnUrl, + LoadISSDescriptorSystem.InjectToWorld(ref builder, webRequestController, lodGeneratorCdnUrl, new NoCache(false, false), new DiskCacheOptions(staticContainer.ISSDescriptorDiskCache, GetISSDescriptorIntention.DiskHashCompute.INSTANCE, "iss.json")); diff --git a/Explorer/Assets/DCL/Infrastructure/SceneLifeCycle/SceneFacade/Systems/LoadHybridSceneSystemLogic.cs b/Explorer/Assets/DCL/Infrastructure/SceneLifeCycle/SceneFacade/Systems/LoadHybridSceneSystemLogic.cs index 62605e63b38..b4710ddeff7 100644 --- a/Explorer/Assets/DCL/Infrastructure/SceneLifeCycle/SceneFacade/Systems/LoadHybridSceneSystemLogic.cs +++ b/Explorer/Assets/DCL/Infrastructure/SceneLifeCycle/SceneFacade/Systems/LoadHybridSceneSystemLogic.cs @@ -82,7 +82,10 @@ private async UniTask FetchRemoteAssetBundleVersionAsync(SceneEntityDefinition d definition.assetBundleManifestVersion = AssetBundleManifestVersion.CreateFromFallback(manifest.Version, manifest.Date); definition.assetBundleManifestVersion.InjectContent(sceneId, definition.content); - definition.assetBundleManifestVersion.InjectDepsDigests(manifest.files); + + // Pre-v49 bundles are entity-scoped — injecting their files[] would wrongly flag canonical assets. + if (definition.assetBundleManifestVersion.SupportsDepsDigests()) + definition.assetBundleManifestVersion.InjectDepsDigests(manifest.files); } catch (OperationCanceledException) { } catch (Exception e) { ReportHub.LogException(e, ReportCategory.SCENE_LOADING); } diff --git a/Explorer/Assets/DCL/LOD/Systems/ResolveISSLODSystem.cs b/Explorer/Assets/DCL/LOD/Systems/ResolveISSLODSystem.cs index 919574b85b5..163711c2caa 100644 --- a/Explorer/Assets/DCL/LOD/Systems/ResolveISSLODSystem.cs +++ b/Explorer/Assets/DCL/LOD/Systems/ResolveISSLODSystem.cs @@ -5,7 +5,6 @@ using DCL.Ipfs; using DCL.LOD.Components; using DCL.Optimization.PerformanceBudgeting; -using DCL.Utility; using ECS.Abstract; using System.Collections.Generic; using ECS.Prioritization.Components; @@ -71,13 +70,13 @@ private void ResolveInitialSceneStateLODDescriptor(ref SceneLODInfo sceneLODInfo /// private void SpawnAssetPromises(InitialSceneStateLOD initialSceneStateLOD, IReadOnlyList assets, SceneDefinitionComponent sceneDefinition, ISSDescriptor issDescriptor) { - AssetBundleManifestVersion? manifest = sceneDefinition.Definition.assetBundleManifestVersion; + AssetBundleManifestVersion manifest = sceneDefinition.Definition.AssetBundleManifestVersionOrFailed; for (var i = 0; i < assets.Count; i++) { ISSDescriptorAsset entry = assets[i]; - // The GLTF container cache is keyed by "hash@digest" (see AssetBundleManifestVersionExtensions.ComposeCacheKey). + // The GLTF container cache is keyed by the canonical CDN file name (see AssetBundleManifestVersion.ComposeCacheKey). // Looking up by bare hash misses any bridged entry the SDK runtime left behind, so the LOD spawns a // second instance of an asset that's already resident — that's the visible "double" overlap. string cacheKey = manifest.ComposeCacheKey(entry.hash); @@ -92,20 +91,11 @@ private void SpawnAssetPromises(InitialSceneStateLOD initialSceneStateLOD, IRead continue; } - // Descriptor mode: fetch each asset's own bundle — must include the platform suffix to match the deployed AB filename. - string promiseHash = $"{entry.hash}{PlatformUtils.GetCurrentPlatform()}"; - - var intent = GetAssetBundleIntention.FromHash(promiseHash, + // Resolving through the manifest lands this promise in the same AssetBundleCache slot as the SDK runtime — a diverging Hash races two loads of the same bundle ("asset bundle already loaded"). + var intent = GetAssetBundleIntention.FromHash(manifest.GetCdnRequestHash(entry.hash), assetBundleManifestVersion: manifest, parentEntityID: sceneDefinition.Definition.id); - // Mirror the digest populated by PrepareGltfAssetLoadingSystem so this promise lands in the same - // AssetBundleCache slot as the SDK runtime would. Without it the (Hash, DepsDigest) key diverges - // and two parallel LoadAssetBundleSystem flows race for the same physical bundle, which Unity - // refuses with "asset bundle already loaded". The digest map is keyed by bare CID. - if (manifest != null && manifest.TryGetDepsDigest(entry.hash, out string digest)) - intent.DepsDigest = digest; - AssetBundlePromise promise = AssetBundlePromise.Create(World, intent, PartitionComponent.TOP_PRIORITY); ISSAssetCreationHelper assetCreationHelper = new ISSAssetCreationHelper(initialSceneStateLOD, entry, cacheKey); diff --git a/Explorer/Assets/DCL/LOD/Systems/UpdateSceneLODInfoSystem.cs b/Explorer/Assets/DCL/LOD/Systems/UpdateSceneLODInfoSystem.cs index da6363c1ac9..91f1b685a4d 100644 --- a/Explorer/Assets/DCL/LOD/Systems/UpdateSceneLODInfoSystem.cs +++ b/Explorer/Assets/DCL/LOD/Systems/UpdateSceneLODInfoSystem.cs @@ -86,14 +86,14 @@ private void StartLODPromise(ref SceneLODInfo sceneLODInfo, ref PartitionCompone // descriptor in None state — no ISS for this scene; fall through to legacy LOD. } - string platformLODKey = $"{sceneDefinitionComponent.Definition.id.ToLower()}_{level.ToString()}{PlatformUtils.GetCurrentPlatform()}"; + AssetBundleManifestVersion lodManifest = AssetBundleManifestVersion.CreateForLOD($"LOD/{level.ToString()}", "dummyDate"); var assetBundleIntention = GetAssetBundleIntention.FromHash( - platformLODKey, + lodManifest.GetCdnRequestHash($"{sceneDefinitionComponent.Definition.id.ToLower()}_{level.ToString()}"), + lodManifest, typeof(GameObject), permittedSources: AssetSource.ALL, customEmbeddedSubDirectory: LODUtils.LOD_EMBEDDED_SUBDIRECTORIES, - assetBundleManifestVersion: AssetBundleManifestVersion.CreateForLOD($"LOD/{level.ToString()}", "dummyDate"), lookForDependencies: true ); diff --git a/Explorer/Assets/DCL/LOD/Tests/PlayMode/InstantiateSceneLODInfoSystemShould.cs b/Explorer/Assets/DCL/LOD/Tests/PlayMode/InstantiateSceneLODInfoSystemShould.cs index 864c81bcc10..4f66edad571 100644 --- a/Explorer/Assets/DCL/LOD/Tests/PlayMode/InstantiateSceneLODInfoSystemShould.cs +++ b/Explorer/Assets/DCL/LOD/Tests/PlayMode/InstantiateSceneLODInfoSystemShould.cs @@ -136,7 +136,7 @@ public void ResolveFailedPromise() private Promise GenerateFailedPromise() { var promise = Promise.Create(world, - GetAssetBundleIntention.FromHash("Cube", typeof(GameObject)), + GetAssetBundleIntention.FromHash("Cube", AssetBundleManifestVersion.CreateForLOD("LOD/0", "dummyDate"), typeof(GameObject)), new PartitionComponent()); world.Add(promise.Entity, @@ -148,7 +148,7 @@ private Promise GenerateFailedPromise() private (AssetBundleData, Promise) GenerateSuccessfullPromise() { var promise = Promise.Create(world, - GetAssetBundleIntention.FromHash("Cube", typeof(GameObject)), + GetAssetBundleIntention.FromHash("Cube", AssetBundleManifestVersion.CreateForLOD("LOD/0", "dummyDate"), typeof(GameObject)), new PartitionComponent()); var fakeAssetBundleData = new AssetBundleData(null, new []{GameObject.CreatePrimitive(PrimitiveType.Cube)}, diff --git a/Explorer/Assets/DCL/NetworkDefinitions/AssetBundleManifestVersion.cs b/Explorer/Assets/DCL/NetworkDefinitions/AssetBundleManifestVersion.cs index 9779234b50f..e2dba0a30e5 100644 --- a/Explorer/Assets/DCL/NetworkDefinitions/AssetBundleManifestVersion.cs +++ b/Explorer/Assets/DCL/NetworkDefinitions/AssetBundleManifestVersion.cs @@ -2,9 +2,9 @@ using DCL.Utility; using System; using System.Collections.Generic; +using UnityEngine; using Utility; -// ReSharper disable once CheckNamespace namespace DCL.Ipfs { public class AssetBundleManifestVersion @@ -23,13 +23,10 @@ public class AssetBundleManifestVersion public static readonly int AB_MIN_SUPPORTED_VERSION_WINDOWS = 15; public static readonly int AB_MIN_SUPPORTED_VERSION_MAC = 16; - private static readonly char[] FILE_NAME_SEPARATOR = { '_' }; + //Shared sentinel for paths that require a manifest but have none; injections no-op on failed manifests, so the instance stays immutable. + public static readonly AssetBundleManifestVersion FAILED = CreateFailed(); - // Global kill-switch for the v49 deps-digest cache-keying scheme. Default off so the build is byte-identical - // to legacy behavior until the feature flag is rolled out. Flipped from bootstrap based on FeaturesRegistry. - // When false: SupportsDepsDigests reports false for every manifest, TryGetDepsDigest never returns a digest, - // and every downstream call site (cache dispatch, GLTF key compose, digest fetch) falls back to legacy. - public static bool DepsDigestKeyingEnabled; + private static readonly char[] FILE_NAME_SEPARATOR = { '_' }; private bool? HasHashInPathValue; @@ -39,23 +36,21 @@ public class AssetBundleManifestVersion public bool IsLSDAsset; public AssetBundleManifestVersionPerPlatform? assets; - private HashSet? convertedFiles; - private IReadOnlyDictionary? depsDigests; + //Bare hash → CDN file name; fed by InjectDepsDigests (digest-bearing names) and InjectContent (Qm casing fixes). + private Dictionary? cdnFiles; + + //Set when the manifest's files[] were injected — only scenes fetch them. Reusable bundles live under the shared assets/ prefix and cache-key on version+hash; wearables/emotes stay entity-scoped and keep buildDate keying. + private bool hasReusableAssets; - public bool HasHashInPath() + private bool HasHashInPath() { HasHashInPathValue ??= TryParseVersionNumber(GetAssetBundleManifestVersion(), out int version) && version >= ASSET_BUNDLE_VERSION_REQUIRES_HASH; return HasHashInPathValue.Value; } - /// - /// True when the manifest's version is v49 or newer — i.e. when the per-file deps-digest scheme is - /// in use for cache keying. This is purely a version check; an individual asset may still have an - /// empty digest (leaf ABs that aren't listed in the manifest's deps map). - /// + /// True when the manifest's version is v49 or newer — a pure version check; individual files may still carry no digest. public bool SupportsDepsDigests() { - if (!DepsDigestKeyingEnabled) return false; SupportsDepsDigestsValue ??= TryParseVersionNumber(GetAssetBundleManifestVersion(), out int version) && version >= ASSET_BUNDLE_VERSION_SUPPORTS_DEPS_DIGEST; return SupportsDepsDigestsValue.Value; } @@ -82,53 +77,103 @@ private static bool TryParseVersionNumber(string? version, out int parsed) } /// - /// Parses the manifest's files[] entries and stores the per-file deps digest map. - /// Expects v49+ filenames in the form <hash>_<depsDigest>_<platform>; - /// legacy 2-part filenames split into fewer parts and are skipped. + /// Stores the manifest's files[] — the verbatim names bundles live under on the CDN's shared + /// assets/ prefix — keyed by the bare hash. Callers gate on : + /// pre-v49 bundles are entity-scoped and must not be flagged reusable. /// public void InjectDepsDigests(string[]? files) { - if (!DepsDigestKeyingEnabled || files == null || files.Length == 0) - { - depsDigests = null; - return; - } - - Dictionary? map = null; + if (files == null || files.Length == 0) return; + hasReusableAssets = true; foreach (string file in files) { if (string.IsNullOrEmpty(file)) continue; - string[] parts = file.Split(FILE_NAME_SEPARATOR, 3); - if (parts.Length < 3) continue; + // Non-suffixed entries (build logs, folders) carry no hash to key by. + string[] parts = file.Split(FILE_NAME_SEPARATOR, 2); + if (parts.Length < 2) continue; - map ??= new Dictionary(new UrlHashComparer()); - map[parts[0]] = parts[1]; + cdnFiles ??= new Dictionary(new UrlHashComparer()); + cdnFiles[parts[0]] = file; } + } + + /// Translates a bare hash to the hash requested from the CDN: the canonical manifest file name when known (digest-bearing, correctly cased), otherwise the platform-suffixed bare hash. + public string GetCdnRequestHash(string bareHash) => + TryGetCdnFileName(bareHash, out string fileName) ? fileName : $"{bareHash}{PlatformUtils.GetCurrentPlatform()}"; + + /// Composes the upper-layer cache key (GLTF container, etc.): the canonical CDN file name when known, otherwise the bare hash. + public string ComposeCacheKey(string hash) => + TryGetCdnFileName(hash, out string fileName) ? fileName : hash; + + /// Computes the Unity-cache key for a CDN request hash: reusable bundles key on version+hash — the digest travels inside the hash, so the cache is shareable across republishes — while wearables/emotes keep buildDate keying, as their bundles are republished in place. + public Hash128 ComputeCacheHash(string hash) => + hasReusableAssets + ? ComputeHashV49(hash, GetAssetBundleManifestVersion()) + : ComputeHashLegacy(hash, GetAssetBundleManifestBuildDate()); + + /// Builds the CDN-relative path for a request hash: reusable bundles live under the shared assets/ prefix (no entity segment), entity-scoped bundles (wearables/emotes, pre-v49 scenes) keep the legacy shapes. + public string GetCdnRequestPath(string hash, string sceneID) + { + string version = GetAssetBundleManifestVersion(); + + if (hasReusableAssets) + return $"{version}/assets/{hash}"; + + if (HasHashInPath()) + return $"{version}/{sceneID}/{hash}"; + + return $"{version}/{hash}"; + } + + private static unsafe Hash128 ComputeHashV49(string hash, string version) + { + // The digest embedded in the hash replaces buildDate-based invalidation, keeping the cache shareable across CDN republishes; the delimiter prevents version/hash boundary collisions. + ReadOnlySpan hashSpan = hash.AsSpan(); + ReadOnlySpan versionSpan = version.AsSpan(); - depsDigests = map; + Span builder = stackalloc char[versionSpan.Length + 1 + hashSpan.Length]; + versionSpan.CopyTo(builder); + builder[versionSpan.Length] = '|'; + hashSpan.CopyTo(builder[(versionSpan.Length + 1)..]); + + fixed (char* ptr = builder) { return Hash128.Compute(ptr, (uint)(sizeof(char) * builder.Length)); } } - public bool TryGetDepsDigest(string hash, out string digest) + private static unsafe Hash128 ComputeHashLegacy(string hash, string buildDate) { - if (DepsDigestKeyingEnabled && depsDigests != null && depsDigests.TryGetValue(hash, out digest!)) + // Byte-identical to the pre-v49 cache key so existing Unity-AB-cache entries keep hitting after upgrade. + // The lack of a delimiter is a known theoretical collision risk (e.g. buildDate ending in 'X' vs. hash + // starting with 'X') — accepted here, will be addressed when v49 adoption lets us retire this path. + Span hashBuilder = stackalloc char[buildDate.Length + hash.Length]; + buildDate.AsSpan().CopyTo(hashBuilder); + hash.AsSpan().CopyTo(hashBuilder[buildDate.Length..]); + + fixed (char* ptr = hashBuilder) { return Hash128.Compute(ptr, (uint)(sizeof(char) * hashBuilder.Length)); } + } + + private bool TryGetCdnFileName(string bareHash, out string fileName) + { + if (cdnFiles != null && cdnFiles.TryGetValue(bareHash, out fileName!)) return true; - digest = string.Empty; + fileName = string.Empty; return false; } - public string? GetAssetBundleManifestVersion() => - IPlatform.DEFAULT.Is(IPlatform.Kind.Windows) ? assets?.windows!.version : assets?.mac!.version; + //! safe: every factory (CreateFromFallback, CreateFailed, CreateManualManifest, CreateForLOD) sets the current platform's info, and deserialized manifests carry both platforms. + public string GetAssetBundleManifestVersion() => + IPlatform.DEFAULT.Is(IPlatform.Kind.Windows) ? assets?.windows!.version! : assets?.mac!.version!; - public string? GetAssetBundleManifestBuildDate() => - IPlatform.DEFAULT.Is(IPlatform.Kind.Windows) ? assets?.windows!.buildDate : assets?.mac!.buildDate; + //! safe: same factory invariant as GetAssetBundleManifestVersion. + private string GetAssetBundleManifestBuildDate() => + IPlatform.DEFAULT.Is(IPlatform.Kind.Windows) ? assets?.windows!.buildDate! : assets?.mac!.buildDate!; public bool IsEmpty() => assets?.IsEmpty() ?? true; - public static AssetBundleManifestVersion CreateFailed() + private static AssetBundleManifestVersion CreateFailed() { //All AB requests will fail when this occurs; its a dead end var failedAssets = new AssetBundleManifestVersionPerPlatform(); @@ -200,19 +245,11 @@ public static AssetBundleManifestVersion CreateForLOD(string assetBundleManifest return assetBundleManifestVersion; } - public string CheckCasing(string inputHash) - { - if (convertedFiles == null || convertedFiles.Count == 0) - return inputHash; - - if (convertedFiles.TryGetValue(inputHash, out string convertedFile)) - return convertedFile; - - return inputHash; - } - public void InjectContent(string entityID, ContentDefinition[] entityDefinitionContent) { + // A failed manifest serves no file names — and it can be the shared FAILED sentinel, which must never be mutated. + if (assetBundleManifestRequestFailed) return; + // TODO (JUANI): hack, for older Qm. Doesnt happen with bafk because they are all lowercase // This has a long due capitalization problem. The hash in Mac which is requested should always be lower case, since the output files are lowercase and the // request to S3 is case sensitive. @@ -224,15 +261,16 @@ public void InjectContent(string entityID, ContentDefinition[] entityDefinitionC // Maybe one day, when `Qm` deployments dont exist anymore, this method can be removed if (!AssetBundleManifestHelper.IsQmEntity(entityID)) return; - convertedFiles = new HashSet(new UrlHashComparer()); + cdnFiles ??= new Dictionary(new UrlHashComparer()); + string platformSuffix = PlatformUtils.GetCurrentPlatform(); + bool lowerCase = IPlatform.DEFAULT.Is(IPlatform.Kind.Mac); - if (IPlatform.DEFAULT.Is(IPlatform.Kind.Mac)) - for (var i = 0; i < entityDefinitionContent.Length; i++) - convertedFiles.Add($"{entityDefinitionContent[i].hash.ToLowerInvariant()}" + PlatformUtils.GetCurrentPlatform()); - - if (IPlatform.DEFAULT.Is(IPlatform.Kind.Windows)) - for (var i = 0; i < entityDefinitionContent.Length; i++) - convertedFiles.Add($"{entityDefinitionContent[i].hash}" + PlatformUtils.GetCurrentPlatform()); + // TryAdd keeps the first entry for each key; a digest-bearing name already stored by InjectDepsDigests is never overwritten. + for (var i = 0; i < entityDefinitionContent.Length; i++) + { + string hash = entityDefinitionContent[i].hash; + cdnFiles.TryAdd(hash, (lowerCase ? hash.ToLowerInvariant() : hash) + platformSuffix); + } } } diff --git a/Explorer/Assets/DCL/NetworkDefinitions/AssetBundleManifestVersionExtensions.cs b/Explorer/Assets/DCL/NetworkDefinitions/AssetBundleManifestVersionExtensions.cs deleted file mode 100644 index 3c1894e81b6..00000000000 --- a/Explorer/Assets/DCL/NetworkDefinitions/AssetBundleManifestVersionExtensions.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace DCL.Ipfs -{ - public static class AssetBundleManifestVersionExtensions - { - /// - /// Composes the cache key for an asset bundle: hash@depsDigest when the manifest has a digest entry - /// for the bare hash, or just the hash when it doesn't. Used by upper-layer caches (GLTF container, etc.) - /// to differentiate two scenes that share an AB hash but resolve different dependency closures. - /// - public static string ComposeCacheKey(this AssetBundleManifestVersion? manifest, string hash) => - manifest != null && manifest.TryGetDepsDigest(hash, out string digest) ? $"{hash}@{digest}" : hash; - } -} diff --git a/Explorer/Assets/DCL/NetworkDefinitions/AssetBundleManifestVersionExtensions.cs.meta b/Explorer/Assets/DCL/NetworkDefinitions/AssetBundleManifestVersionExtensions.cs.meta deleted file mode 100644 index e92dcd04ef1..00000000000 --- a/Explorer/Assets/DCL/NetworkDefinitions/AssetBundleManifestVersionExtensions.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: b4d0c335cc9ed810663c85f25335a904 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Explorer/Assets/DCL/NetworkDefinitions/EntityDefinitionBase.cs b/Explorer/Assets/DCL/NetworkDefinitions/EntityDefinitionBase.cs index 03dc1a8ab87..57a0ba07c9c 100644 --- a/Explorer/Assets/DCL/NetworkDefinitions/EntityDefinitionBase.cs +++ b/Explorer/Assets/DCL/NetworkDefinitions/EntityDefinitionBase.cs @@ -39,6 +39,10 @@ public class TrimmedEntityDefinitionBase [JsonProperty("versions")] public AssetBundleManifestVersion? assetBundleManifestVersion; + /// The manifest version, or the failed sentinel when none was resolved — AB intentions require a manifest, and the sentinel is the same dead end the pipeline already handles. + [JsonIgnore] + public AssetBundleManifestVersion AssetBundleManifestVersionOrFailed => assetBundleManifestVersion ?? AssetBundleManifestVersion.FAILED; + [JsonProperty("status")] public AssetBundleRegistryEnum assetBundleRegistryEnum; } diff --git a/Explorer/Assets/DCL/PluginSystem/World/AssetBundlesPlugin.cs b/Explorer/Assets/DCL/PluginSystem/World/AssetBundlesPlugin.cs index 4ea8651e942..26df1adaf01 100644 --- a/Explorer/Assets/DCL/PluginSystem/World/AssetBundlesPlugin.cs +++ b/Explorer/Assets/DCL/PluginSystem/World/AssetBundlesPlugin.cs @@ -62,7 +62,7 @@ public AssetBundlesPlugin(IReportsHandlingSettings reportsHandlingSettings, Cach public void InjectToWorld(ref ArchSystemsWorldBuilder builder, in ECSWorldInstanceSharedDependencies sharedDependencies, in SystemsDependencies systemsDependencies, in PersistentEntities persistentEntities, List finalizeWorldSystems, List sceneIsCurrentListeners) { // Asset Bundles - PrepareAssetBundleLoadingParametersSystem.InjectToWorld(ref builder, sharedDependencies.SceneData, STREAMING_ASSETS_URL, assetBundleURL); + PrepareAssetBundleLoadingParametersSystem.InjectToWorld(ref builder, STREAMING_ASSETS_URL, assetBundleURL); bool byteWeightedProgress = FeaturesRegistry.Instance.IsEnabled(FeatureId.BYTE_WEIGHTED_LOADING_PROGRESS); diff --git a/Explorer/Assets/DCL/PluginSystem/World/GltfContainerPlugin.cs b/Explorer/Assets/DCL/PluginSystem/World/GltfContainerPlugin.cs index a25d75efe2e..00d2479fc17 100644 --- a/Explorer/Assets/DCL/PluginSystem/World/GltfContainerPlugin.cs +++ b/Explorer/Assets/DCL/PluginSystem/World/GltfContainerPlugin.cs @@ -120,10 +120,10 @@ public void InjectToWorld(ref ArchSystemsWorldBuilder builder, // GLTF Container // Bridge GLTF cache keying to the AB layer without leaking AB symbols into LoadGltfContainerSystem: - // when the scene's manifest has a deps digest for the hash, the key becomes "hash@digest", else the bare hash. + // when the scene's manifest knows the hash, the key becomes the canonical CDN file name, else the bare hash. var sceneData = sharedDependencies.SceneData; LoadGltfContainerSystem.InjectToWorld(ref builder, buffer, sceneData, sharedDependencies.EntityCollidersSceneCache, - hash => sceneData.SceneEntityDefinition.assetBundleManifestVersion.ComposeCacheKey(hash)); + hash => sceneData.SceneEntityDefinition.AssetBundleManifestVersionOrFailed.ComposeCacheKey(hash)); FinalizeGltfContainerLoadingSystem.InjectToWorld(ref builder, persistentEntities.SceneRoot, globalDeps.FrameTimeBudget, sharedDependencies.EntityCollidersSceneCache, sharedDependencies.SceneData, buffer);