Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
e799713
feat: request asset bundles by their digest-bearing file name
dalkia Jul 20, 2026
a279b5f
refactor: compose platform hash inside GetPlatformHashWithDigest
dalkia Jul 20, 2026
0b22120
refactor: build all AB request hashes through GetPlatformHashWithDigest
dalkia Jul 20, 2026
c7cacd4
refactor: rename GetPlatformHashWithDigest to GetCdnRequestHash
dalkia Jul 20, 2026
82d1e8a
refactor: rename GetHashWithDigest to ResolveCdnRequestHash
dalkia Jul 20, 2026
bea0f21
style: condense multi-line comments to single lines
dalkia Jul 20, 2026
1682f0e
refactor: unify convertedFiles and depsFiles into one cdnFiles map
dalkia Jul 20, 2026
5674343
refactor: fold extension methods into AssetBundleManifestVersion
dalkia Jul 20, 2026
ade3952
Inling
dalkia Jul 20, 2026
46ca877
refactor: make GetAssetBundleIntention.AssetBundleManifestVersion non…
dalkia Jul 20, 2026
25a2185
refactor: move cache-hash computation into AssetBundleManifestVersion
dalkia Jul 20, 2026
7475faf
AssetBundleManifest renaming
dalkia Jul 20, 2026
c0c615b
Merge branch 'dev' into feat/ab-url-exact-deps-digest
dalkia Jul 20, 2026
037a811
refactor: key the cdnFiles map by bare hash
dalkia Jul 20, 2026
5d49d2a
fix: keep the sceneID path segment for v49+ asset bundle URLs
dalkia Jul 20, 2026
166c9a3
feat: request digest-mapped bundles from the canonical assets/ prefix
dalkia Jul 21, 2026
4a2d7ff
refactor: rename HasDepsDigests to HasCanonicalAssets
dalkia Jul 21, 2026
af6169b
refactor: treat v49+ files[] as the complete canonical listing
dalkia Jul 21, 2026
ebcc05e
refactor: clarify the digest loader's version check as a network shor…
dalkia Jul 21, 2026
a3c1fa9
refactor: gate InjectDepsDigests at the call sites
dalkia Jul 21, 2026
bafeeab
Remove wrong Resharper disable
dalkia Jul 21, 2026
077fdef
fix: address review comments
dalkia Jul 21, 2026
c4c6ef7
Merge branch 'dev' into feat/ab-url-exact-deps-digest
dalkia Jul 23, 2026
d6dde5a
Update Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading…
dalkia Jul 23, 2026
869a81f
refactor: back the intention manifest with a failed-sentinel fallback
dalkia Jul 23, 2026
8836074
refactor: move CDN path building into AssetBundleManifestVersion
dalkia Jul 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand All @@ -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()
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -219,14 +219,16 @@ private static void CreatePromise<T>(
}
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);
Expand Down
1 change: 0 additions & 1 deletion Explorer/Assets/DCL/FeatureFlags/FeatureFlagsStrings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,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";
Expand Down
3 changes: 2 additions & 1 deletion Explorer/Assets/DCL/FeatureFlags/FeaturesRegistry.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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)),
// Note: COMMUNITIES feature is not cached here because it depends on user identity
Expand Down Expand Up @@ -202,6 +202,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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ private static async UniTask CheckAssetBundleManifestFallbackInternalAsync(World
else
{
assetBundleManifest.TryLogException();
entityDefinition.assetBundleManifestVersion = AssetBundleManifestVersion.CreateFailed();
entityDefinition.assetBundleManifestVersion = AssetBundleManifestVersion.FAILED;
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -15,9 +13,11 @@ public struct GetAssetBundleIntention : ILoadingIntention, IEquatable<GetAssetBu
{
public string? Hash;

public AssetBundleManifestVersion? AssetBundleManifestVersion;
public string ParentEntityID;

//Backing for AssetBundleManifest — nullable because default-initialized structs zero reference fields; set by every factory.
private AssetBundleManifestVersion? assetBundleManifest;

/// <summary>
/// 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
Expand All @@ -35,20 +35,12 @@ public struct GetAssetBundleIntention : ILoadingIntention, IEquatable<GetAssetBu
/// </summary>
internal Hash128? cacheHash;

/// <summary>
/// Per-file dependency digest from the v49+ scene asset-bundle manifest. Two scenes can request the same
/// <see cref="Hash"/> with different dependency closures; this field disambiguates them in the cache.
/// Empty for legacy (pre-v49) entries — those keep their historical key.
/// </summary>
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,
Expand All @@ -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;
}
Expand All @@ -75,37 +66,35 @@ internal GetAssetBundleIntention(CommonLoadingArguments commonArguments) : this(
CommonArguments = commonArguments;
}

/// <summary>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.</summary>
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<GetAssetBundleIntention>
{
public static readonly DiskHashCompute INSTANCE = new ();
Expand All @@ -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);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading