Skip to content

feat: request asset bundles by their digest-bearing file name#9442

Draft
dalkia wants to merge 22 commits into
devfrom
feat/ab-url-exact-deps-digest
Draft

feat: request asset bundles by their digest-bearing file name#9442
dalkia wants to merge 22 commits into
devfrom
feat/ab-url-exact-deps-digest

Conversation

@dalkia

@dalkia dalkia commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

Pull Request Description

What does this PR change?

v49+ asset-bundle manifests list their files as <hash>_<depsDigest>_<platform>, but the client was requesting the digest-less name from the CDN and threading the digest as a separate DepsDigest field through four cache layers (intention equality, disk cache, Unity web cache, GLTF container cache). This PR makes the digest part of the hash itself and, along the way, consolidates all asset-bundle naming/keying logic into AssetBundleManifestVersion.

Digest-bearing CDN requests

  • AssetBundleManifestVersion stores a single bare hash → canonical CDN file name map (cdnFiles), fed by the manifest's files[]. GetCdnRequestHash(bareHash) translates a bare hash to the digest-bearing, correctly-cased name; files the manifest doesn't list fall back to the platform-suffixed bare hash (both names verified live on the CDN).
  • Every AB request-hash construction goes through that one method: GLTF containers, ISS LOD assets, scene emotes, wearables, avatar emotes, thumbnails, and LODs. The digest travels inside intention.Hash, so the URL and every cache layer share one identity — DepsDigest is deleted.
  • The old Qm-casing fix (CheckCasing/convertedFiles) is unified into the same map: InjectContent adds bare hash → corrected-case name entries (digest entries win regardless of injection order), and the per-frame CheckCasing call in the prepare system is gone.

Cache keying

  • ComputeCacheHash(hash) lives on AssetBundleManifestVersion: manifests that received files[] (scenes only) key on version|hash — shareable across CDN republishes since the digest is in the hash — while wearables/emotes keep the byte-identical legacy buildDate+hash keying, because their bundles are republished in place and must invalidate. ComputeHashV49/ComputeHashLegacy are private implementation details.
  • ComposeCacheKey (GLTF container cache) resolves through the same map, so the SDK runtime and LOD/ISS paths land in the same cache slots.

Feature flag removed

  • DepsDigestKeyingEnabled and its wiring (bootstrap, FeaturesRegistry entry, FF string) are deleted — the flag is at 100% in production, and the manifest version published by the backend is the real rollout gate. FeatureId.AB_DEPS_DIGEST_CACHE_KEY = 63 stays as [Obsolete] to preserve enum ordering. Also removes a leftover debug log in Bootstraper.

Intentions always carry a manifest

  • GetAssetBundleIntention.AssetBundleManifest (renamed) is non-nullable; Create/FromHash require it (and Create the parentEntityID), so the scene-world PrepareAssetBundleLoadingParametersSystem no longer injects manifest/parentID per frame and lost its ISceneData dependency.
  • The nullable wire-format DTO field stays; consumers use TrimmedEntityDefinitionBase.AssetBundleManifestVersionOrFailed, which hands out the failed sentinel the pipeline already treats as a dead end.
  • Bug fix: thumbnails whose attachment has no manifest now fall back to the content-server texture path instead of spawning an AB promise doomed to fail.

Dead code removed

  • staticscene_ handling (BuildInitialSceneStateURL, the disabled ISS bundle-mode HEAD probe and its assetBundleURL dependency) — the AB type isn't currently supported.
  • AssetBundleManifestVersionExtensions folded into the class.

Known one-time cost

v49 Unity-cache keys change shape (version|hash|digestversion|hash-with-digest), so previously cached v49 entries re-download once. Legacy (pre-v49 / wearable) keys are byte-identical and keep hitting.

Test Instructions

Steps (standard run):

metaforge explorer run 9442

Expected result:
Genesis City scenes load normally. AB requests for v49 scenes hit digest-bearing URLs under the canonical prefix, e.g. https://ab-cdn.decentraland.org/v49/assets/<hash>_<32hexdigest>_mac instead of .../v49/<sceneID>/<hash>_mac.

Steps (fresh account):

metaforge account create --clear
metaforge explorer run 9442

Expected result:
Same as above from a cold cache — scenes, wearables, and emotes all load; no Manifest version must be provided or asset bundle already loaded errors in logs.

Automation (if applicable):

metaforge explorer test 9442

Prerequisites

  • No special setup — the deps-digest scheme is always on for v49+ scene manifests (the old alfa-ab-deps-digest-cache-key flag is no longer read)

Test Steps

  1. Start the client and load into Genesis City (e.g. 0,0 — currently served by a v49 manifest)
  2. Tail logs / network and confirm scene AB URLs use the {version}/assets/ prefix and contain the 32-hex digest segment for files listed with one in https://ab-cdn.decentraland.org/manifest/<sceneID>_<platform>.json
  3. Walk across several parcels; scenes with LODs and ISS should load and unload without "asset bundle already loaded" errors
  4. Equip/unequip wearables and play emotes (backpack) — these keep digest-less URLs and buildDate cache keying, and must load normally
  5. Trigger a scene emote in a scene that provides one — it should load from a digest-bearing URL
  6. Reload the same scene (walk away and back) — bundles should come from cache, not re-download

Additional Testing Notes

  • Pre-v49 scenes are untouched (no manifest map → entity-path digest-less URLs + buildDate keying); wearables/emotes also keep the entity path, where the Cloudflare Worker resolves digest-less names
  • v49 manifests legitimately mix 2-part and 3-part file names; 2-part files keep the digest-less URL by design
  • AB dependency hashes from bundle metadata pass through as-is (no client-side digest resolution)
  • First run after this change re-downloads previously cached v49 bundles once (cache keys changed shape)
  • EditMode tests rewritten: DepsDigestCacheKeyShould covers hash translation, Qm casing, injection-order precedence, cache-key dispatch and hash stability behaviorally through manifests

Quality Checklist

  • Changes have been tested locally
  • Documentation has been updated (if required)
  • Performance impact has been considered
  • For SDK features: Test scene is included

Code Review Reference

Please review our Branch & PR Standards before submitting. It explains the automated review flow, QA/DEV approval requirements, and what each label does — especially useful for first-time contributors.

🤖 Generated with Claude Code

v49+ manifests list files as <hash>_<depsDigest>_<platform>, but the
client was requesting the digest-less name and carrying the digest as a
separate field threaded through every cache layer.

Now the intention Hash itself resolves to the verbatim manifest entry,
so the URL, the in-memory AB cache, the disk cache, Unity's web cache
and the GLTF container cache all key off a single identity:

- AssetBundleManifestVersion stores bare hash -> verbatim file name and
  exposes GetHashWithDigest / TryGetFileNameWithDigest
- GLTF containers, AB dependencies, ISS LOD assets and scene emotes
  resolve their hash through the scene manifest at intention creation
- GetAssetBundleIntention.DepsDigest is gone; equality, GetHashCode and
  the disk key use Hash alone
- Unity cache keys dispatch on HasDepsDigests(): scene manifests (the
  only ones that fetch files[]) key on version|hash; wearables/emotes
  keep buildDate keying since their bundles are republished in place
- DepsDigestKeyingEnabled and its feature-flag wiring are removed; the
  manifest version published by the backend is the rollout gate.
  FeatureId.AB_DEPS_DIGEST_CACHE_KEY stays as [Obsolete] to preserve
  enum ordering
- Dead staticscene_ code removed (BuildInitialSceneStateURL, the
  disabled ISS bundle-mode HEAD probe)

Verified against production: v49 manifests mix 2-part and 3-part names,
digests are 32-hex, and the CDN serves both the digest-full and
digest-less object names, so unmapped files keep working unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@dalkia
dalkia requested review from a team as code owners July 20, 2026 17:46
@github-actions
github-actions Bot requested a review from anicalbano July 20, 2026 17:46
@github-actions

github-actions Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

@decentraland-bot decentraland-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Review — feat: request asset bundles by their digest-bearing file name

STEP 2 — Root-cause check

Problem: v49+ asset-bundle manifests list their files with an embedded deps digest (<hash>_<depsDigest>_<platform>), but the client was requesting the digest-less name from the CDN and threading the digest as a separate DepsDigest field through four cache layers.

Does the diff fix the cause? Yes. The fundamental issue is an identity mismatch: the CDN serves files by their digest-bearing name, but the client was constructing a digest-less URL and tracking the digest separately. The PR correctly makes the digest part of the hash identity at the earliest possible moment (intention creation), eliminating the need to thread it through downstream caches. This is a causal fix, not a symptom treatment.

STEP 3 — Design & integration

The PR does not introduce new long-lived units. It modifies the existing AssetBundleManifestVersion class — which already owns the manifest file-name map — to expose GetHashWithDigest() (platform-suffixed lookup) and HasDepsDigests() (presence check). This is the correct home for this logic: the manifest version is the canonical source of file name resolution.

The DepsDigest field removal from GetAssetBundleIntention is clean: all four call sites now resolve through GetHashWithDigest before constructing the intention, so the digest travels inside Hash rather than alongside it. The call sites are:

  • GetSceneEmoteFromRealmIntention.CreateAndAddPromiseToWorld — scene emotes
  • PrepareGltfAssetLoadingSystem.Prepare — GLTF containers
  • LoadAssetBundleSystem.WaitForDependencyAsync — AB dependencies
  • ResolveISSLODSystem.SpawnAssetPromises — ISS LOD assets

All four are consistent in their resolution pattern: compose hash + platform, resolve through manifest, use the result as the sole identity.

Cache dispatch change (SupportsDepsDigests()HasDepsDigests()): This is a semantic improvement. SupportsDepsDigests() is a version check (≥ v49); HasDepsDigests() checks whether the deps map was actually injected. The distinction matters because wearable/emote manifests can be v49+ but never receive files[] — they must keep buildDate keying. Dispatching on the map's presence is more precise.

Feature flag removal (DepsDigestKeyingEnabled): The flag was at 100% in production and is now architecturally unnecessary — the rollout gate is the manifest version the backend publishes. The enum value is retained with [Obsolete] to preserve ordering. Clean lifecycle management.

Dead code removal (BuildInitialSceneStateURL, IsBundleReachableAsync, assetBundleURL parameter): Confirmed no callers. The LoadISSDescriptorSystem constructor correctly drops the unused assetBundleURL parameter, and GlobalWorldFactory.cs updates the injection call.

Teardown trace: No new subscriptions, callbacks, or resources are opened. The change only modifies hash resolution at creation sites.

STEP 4 — Member audit

  • GetHashWithDigest(string hash) — 4 consumers (listed above). Multi-use, correctly placed on the manifest owner. Strips the platform suffix, looks up the bare hash in the case-insensitive map, returns the verbatim manifest entry or the input unchanged. Sound API.
  • HasDepsDigests() — 1 consumer (PrepareAssetBundleLoadingParametersSystemBase). Single-use, but it is a canonical predicate on the manifest's state (depsFiles != null), not a derived/re-checking property. Legitimate encapsulation.
  • TryGetFileNameWithDigest(string bareHash, out string fileName) — 1 consumer (ComposeCacheKey extension). Provides bare-hash lookup without platform suffix stripping — a different access pattern from GetHashWithDigest. Legitimate API.
  • ComposeCacheKey extension — 2 consumers (ResolveISSLODSystem, GltfContainerPlugin). Multi-use, correctly bridges the manifest's file-name resolution to upper-layer caches.

No single-use-merge, absent≠false, or redundant-guard issues found.

STEP 5 — Line-level review

[P2] Stale comment (outside the diff) — GltfContainerPlugin.cs line 123:
The comment at Explorer/Assets/DCL/PluginSystem/World/GltfContainerPlugin.cs:123 still says:

when the scene's manifest has a deps digest for the hash, the key becomes "hash@digest", else the bare hash.

But after this PR, ComposeCacheKey now returns the verbatim digest-bearing manifest file name (e.g., bafk..._dda1af..._mac), not the old hash@digest format. Consider updating to:

when the scene's manifest has a digest-bearing file name for the hash, the key becomes that verbatim name, else the bare hash.

Security review: No security issues found. Hash parsing uses safe _-splitting on characters that cannot appear in valid CIDs. Disk cache keys are derived through Hash128.Compute(), so raw manifest strings never become file names. The CDN manifest shares the same trust boundary as the assets it describes. No credentials or secrets are present.

STEP 6 — Complexity assessment

COMPLEX. The diff touches the asset bundle loading pipeline across multiple ECS systems, the cache keying dispatch, the manifest version abstraction, and the intention equality/hashing contract. Changes span 14 files including 4 ECS systems and the core AssetBundleManifestVersion class.

STEP 7 — QA assessment

QA is required: the changes affect runtime asset bundle loading, cache keying, and CDN URL construction — all user-facing.

STEP 8 — Non-blocking warnings

No warnings (Main scene not modified).

STEP 9 — Verdict

REVIEW_RESULT: PASS ✅
COMPLEXITY: COMPLEX
COMPLEXITY_REASON: Modifies asset bundle cache keying, manifest version abstraction, intention equality, and ECS loading systems across the AB pipeline
QA_REQUIRED: YES


Reviewed by Jarvis 🤖 · Requested by decentraland-bot via GitHub

Call sites that hold a bare hash (GLTF containers, ISS LOD assets,
scene emotes) no longer append the platform suffix and null-check the
manifest themselves — a single null-safe extension composes the
suffixed hash and resolves it to the digest-bearing manifest entry,
keyed directly by the bare hash (no append-then-strip round trip).

GetHashWithDigest stays for the one caller with an already-suffixed
hash (AB dependencies from bundle metadata).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Warnings count reduced: 14668 => 14659

Warnings/errors in files changed by this PR (166)
File Line Rule Message
Assets/DCL/FeatureFlags/FeatureFlagsStrings.cs 176 CSharpWarnings::CS0618 CS0618: Constant 'DCL.FeatureFlags.FeatureFlagsStrings.GPUI_ENABLED' is obsolete: 'GPU Instancer Pro terrain is no longer optional so the flag is not needed'
Assets/DCL/AvatarRendering/Wearables/Helpers/WearablePolymorphicBehaviour.cs 136 CSharpWarnings::CS8600 Converting null literal or possible null value into non-nullable type
Assets/DCL/AvatarRendering/Wearables/Helpers/WearablePolymorphicBehaviour.cs 139 CSharpWarnings::CS8600 Converting null literal or possible null value into non-nullable type
Assets/DCL/AvatarRendering/Wearables/Helpers/WearablePolymorphicBehaviour.cs 356 CSharpWarnings::CS8600 Converting null literal or possible null value into non-nullable type
Assets/DCL/AvatarRendering/Emotes/Systems/Load/LoadEmotesByPointersSystem.cs 238 CSharpWarnings::CS8602 Dereference of a possibly null reference
Assets/DCL/AvatarRendering/Wearables/Helpers/WearablePolymorphicBehaviour.cs 131 CSharpWarnings::CS8602 Dereference of a possibly null reference
Assets/DCL/AvatarRendering/Wearables/Helpers/WearablePolymorphicBehaviour.cs 169 CSharpWarnings::CS8602 Dereference of a possibly null reference
Assets/DCL/AvatarRendering/Wearables/Helpers/WearablePolymorphicBehaviour.cs 202 CSharpWarnings::CS8602 Dereference of a possibly null reference
Assets/DCL/AvatarRendering/Wearables/Helpers/WearablePolymorphicBehaviour.cs 258 CSharpWarnings::CS8602 Dereference of a possibly null reference
Assets/DCL/AvatarRendering/Wearables/Helpers/WearablePolymorphicBehaviour.cs 268 CSharpWarnings::CS8602 Dereference of a possibly null reference
Assets/DCL/AvatarRendering/Wearables/Helpers/WearablePolymorphicBehaviour.cs 285 CSharpWarnings::CS8602 Dereference of a possibly null reference
Assets/DCL/AvatarRendering/Wearables/Helpers/WearablePolymorphicBehaviour.cs 319 CSharpWarnings::CS8602 Dereference of a possibly null reference
Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/AssetBundles/SceneAssetBundleDigestsLoader.cs 41 CSharpWarnings::CS8602 Dereference of a possibly null reference
Assets/DCL/LOD/Systems/ResolveISSLODSystem.cs 126 CSharpWarnings::CS8602 Dereference of a possibly null reference
Assets/DCL/LOD/Systems/UpdateSceneLODInfoSystem.cs 92 CSharpWarnings::CS8602 Dereference of a possibly null reference
Assets/DCL/LOD/Tests/PlayMode/InstantiateSceneLODInfoSystemShould.cs 102 CSharpWarnings::CS8602 Dereference of a possibly null reference
Assets/DCL/LOD/Tests/PlayMode/InstantiateSceneLODInfoSystemShould.cs 126 CSharpWarnings::CS8602 Dereference of a possibly null reference
Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/AssetBundles/LoadAssetBundleSystem.cs 235 CSharpWarnings::CS8603 Possible null reference return
Assets/DCL/LOD/Systems/ResolveISSLODSystem.cs 90 CSharpWarnings::CS8604 Possible null reference argument for parameter 'asset' in 'DCL.LOD.Systems.ResolveISSLODSystem.PositionAsset'
Assets/DCL/LOD/Systems/ResolveISSLODSystem.cs 123 CSharpWarnings::CS8604 Possible null reference argument for parameter 'assetBundleData' in 'ECS.Unity.GLTFContainer.Utils.TryCreateGltfObject'
Assets/DCL/AvatarRendering/Wearables/Helpers/WearablePolymorphicBehaviour.cs 147 CSharpWarnings::CS8604 Possible null reference argument for parameter 'hash' in 'DCL.AvatarRendering.Wearables.Helpers.WearablePolymorphicBehaviour.CreatePromise'
Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/AssetBundles/SceneAssetBundleDigestsLoader.cs 35 CSharpWarnings::CS8604 Possible null reference argument for parameter 'hash' in 'ECS.StreamableLoading.AssetBundles.GetAssetBundleManifestIntention.Create'
Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/AssetBundles/PrepareAssetBundleLoadingParametersSystemBase.cs 67 CSharpWarnings::CS8604 Possible null reference argument for parameter 'hash' in 'ECS.StreamableLoading.AssetBundles.PrepareAssetBundleLoadingParametersSystemBase.GetAssetBundleURL'
Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/AssetBundles/PrepareAssetBundleLoadingParametersSystemBase.cs 44 CSharpWarnings::CS8604 Possible null reference argument for parameter 'hash' in 'ECS.StreamableLoading.AssetBundles.PrepareAssetBundleLoadingParametersSystemBase.GetStreamingAssetsUrl'
Assets/DCL/AvatarRendering/Emotes/Systems/Load/LoadEmotesByPointersSystem.cs 249 CSharpWarnings::CS8604 Possible null reference argument for parameter 'parentEntityID' in 'ECS.StreamableLoading.AssetBundles.GetAssetBundleIntention.FromHash'
Assets/DCL/AvatarRendering/Thumbnails/Utils/LoadThumbnailsUtils.cs 71 CSharpWarnings::CS8604 Possible null reference argument for parameter 'parentEntityID' in 'ECS.StreamableLoading.AssetBundles.GetAssetBundleIntention.FromHash'
Assets/DCL/AvatarRendering/Wearables/Helpers/WearablePolymorphicBehaviour.cs 232 CSharpWarnings::CS8604 Possible null reference argument for parameter 'parentEntityID' in 'ECS.StreamableLoading.AssetBundles.GetAssetBundleIntention.FromHash'
Assets/DCL/LOD/Systems/ResolveISSLODSystem.cs 97 CSharpWarnings::CS8604 Possible null reference argument for parameter 'parentEntityID' in 'ECS.StreamableLoading.AssetBundles.GetAssetBundleIntention.FromHash'
Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/AssetBundles/SceneAssetBundleDigestsLoader.cs 35 CSharpWarnings::CS8604 Possible null reference argument for parameter 'url' in 'ECS.StreamableLoading.Common.Components.CommonLoadingArguments.CommonLoadingArguments'
Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/AssetBundles/GetAssetBundleIntention.cs 64 CSharpWarnings::CS8618 Non-nullable field 'ParentEntityID' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable.
Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/AssetBundles/Tests/LoadAssetBundlePartialSystemShould.cs 41 CSharpWarnings::CS8618 Non-nullable field 'promises' is uninitialized. Consider adding the 'required' modifier or declaring the field as nullable.
Assets/DCL/LOD/Tests/PlayMode/InstantiateSceneLODInfoSystemShould.cs 41 CSharpWarnings::CS8618 Non-nullable field 'scenesCache' is uninitialized. Consider adding the 'required' modifier or declaring the field as nullable.
Assets/DCL/AvatarRendering/Wearables/Helpers/WearablePolymorphicBehaviour.cs 321 CSharpWarnings::CS8625 Cannot convert null literal to non-nullable reference type
Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/AssetBundles/GetAssetBundleIntention.cs 47 CSharpWarnings::CS8625 Cannot convert null literal to non-nullable reference type
Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/AssetBundles/GetAssetBundleIntention.cs 83 CSharpWarnings::CS8625 Cannot convert null literal to non-nullable reference type
Assets/DCL/LOD/Tests/PlayMode/InstantiateSceneLODInfoSystemShould.cs 154 CSharpWarnings::CS8625 Cannot convert null literal to non-nullable reference type
Assets/DCL/AvatarRendering/Emotes/Components/Intents/GetSceneEmoteFromRealmIntention.cs 69 CSharpWarnings::CS8629 Nullable value type can be null
Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/AssetBundles/SceneAssetBundleDigestsLoader.cs 38 CSharpWarnings::CS8629 Nullable value type can be null
Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/AssetBundles/Tests/LoadAssetBundlePartialSystemShould.cs 86 CSharpWarnings::CS8629 Nullable value type can be null
Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/AssetBundles/Tests/LoadAssetBundlePartialSystemShould.cs 117 CSharpWarnings::CS8629 Nullable value type can be null
Assets/DCL/LOD/Tests/PlayMode/InstantiateSceneLODInfoSystemShould.cs 154 CoVariantArrayConversion Co-variant array conversion from GameObject[] to Object[] can cause run-time exception on write operation
Assets/DCL/FeatureFlags/FeatureFlagsStrings.cs 116 InconsistentNaming Name 'AssetBundleFallback' does not match rule 'Enum members'. Suggested name is 'ASSET_BUNDLE_FALLBACK'.
Assets/DCL/FeatureFlags/FeatureFlagsStrings.cs 129 InconsistentNaming Name 'AuthCodeValidation' does not match rule 'Enum members'. Suggested name is 'AUTH_CODE_VALIDATION'.
Assets/DCL/FeatureFlags/FeatureFlagsStrings.cs 120 InconsistentNaming Name 'CameraReel' does not match rule 'Enum members'. Suggested name is 'CAMERA_REEL'.
Assets/DCL/FeatureFlags/FeatureFlagsStrings.cs 117 InconsistentNaming Name 'ChatHistoryLocalStorage' does not match rule 'Enum members'. Suggested name is 'CHAT_HISTORY_LOCAL_STORAGE'.
Assets/DCL/FeatureFlags/FeatureFlagsStrings.cs 131 InconsistentNaming Name 'ChatTranslation' does not match rule 'Enum members'. Suggested name is 'CHAT_TRANSLATION'.
Assets/DCL/FeatureFlags/FeatureFlagsStrings.cs 119 InconsistentNaming Name 'CommunityVoiceChat' does not match rule 'Enum members'. Suggested name is 'COMMUNITY_VOICE_CHAT'.
Assets/DCL/NetworkDefinitions/AssetBundleManifestVersion.cs 223 InconsistentNaming Name 'CreateForLOD' does not match rule 'members_should_be_pascal_case'. Suggested name is 'CreateForLod'.
Assets/DCL/NetworkDefinitions/AssetBundleManifestVersion.cs 174 InconsistentNaming Name 'CreateLSDAsset' does not match rule 'members_should_be_pascal_case'. Suggested name is 'CreateLsdAsset'.
Assets/DCL/FeatureFlags/FeatureFlagsStrings.cs 107 InconsistentNaming Name 'CsvVariant' does not match rule 'Enum members'. Suggested name is 'CSV_VARIANT'.

…and 116 more (see the csharp-lint-reports artifact).

dalkia and others added 18 commits July 20, 2026 16:40
Wearables, avatar emotes, thumbnails and LODs now compose their
platform-suffixed hash through the same extension the scene paths use,
so AB request-hash construction lives in a single place. Their
manifests carry no deps map, so the output is unchanged (bare hash +
platform suffix) — but if any of them ever ships digest-bearing file
names, the URLs follow the manifest automatically.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The method builds the exact hash requested from the CDN — the name now
says so instead of describing its composition.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pairs with GetCdnRequestHash: both produce the hash requested from the
CDN — Get composes it from a bare hash, Resolve rewrites an already
platform-suffixed one (dependency hashes from AB metadata).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both mapped a hash to the canonical CDN name — one from the manifest's
files[] (digest-bearing entries), one from Qm entity content (casing
fixes). They now share a single dictionary keyed by the digest-less
platform-suffixed hash:

- ResolveCdnRequestHash is a direct lookup (no more append-then-strip
  of the platform suffix) and fixes Qm casing too
- CheckCasing is gone: every intention creation site already resolves
  through GetCdnRequestHash/ResolveCdnRequestHash, so the Prepare-time
  correction was redundant
- HasDepsDigests is a dedicated bool so casing-only entries can never
  flip cache keying to the v49 scheme
- InjectContent uses TryAdd so digest entries win regardless of
  injection order

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
GetCdnRequestHash and ComposeCacheKey move from the extensions class
into AssetBundleManifestVersion as static methods taking the nullable
manifest, keeping the null handling and platform-suffix composition in
one place. The extensions file is deleted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…-nullable

An AB can never be requested without a manifest, so the intention now
requires one at creation instead of tolerating null until Prepare time:

- Create/FromHash take the manifest (and Create the parentEntityID), so
  the scene PrepareAssetBundleLoadingParametersSystem no longer injects
  them per frame and lost its ISceneData dependency
- TrimmedEntityDefinitionBase exposes AssetBundleManifestVersionOrFailed:
  the nullable wire-format field stays, but consumers get the failed
  sentinel the pipeline already treats as a dead end
- the null-manifest error check in PrepareCommonArguments collapses to
  the failed-manifest check
- GetCdnRequestHash / ComposeCacheKey become instance methods (callers
  always hold a manifest now), also restoring the platform suffix in
  GetCdnRequestHash's lookup
- thumbnails with no manifest now fall back to the content-server
  texture path instead of spawning an AB promise doomed to fail

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The manifest owns every input of the decision (deps map presence,
version, buildDate), so ComputeCacheHash lives there now and the
prepare system just asks for it. ComputeHashV49/ComputeHashLegacy
become private implementation details, and the tests exercise the real
dispatch through manifests instead of calling the hash builders
directly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
InjectDepsDigests no longer reassembles the platform-suffixed key from
file-name parts — the map now translates bare hash to canonical CDN
file name directly. GetCdnRequestHash and ComposeCacheKey become plain
lookups, and ResolveCdnRequestHash (suffixed-input variant) is deleted
along with its platform-suffix stripping; the suffix is only appended
in the fallback for files the manifest doesn't know.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both the org and zone CDNs only serve the nested {version}/{sceneID}/{file}
path for v49 — the flat path 404s — so v49+ must keep HasHashInPath until
the converter publishes a flat layout.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
With ASSET_REUSE the converter uploads every bundle to
{version}/assets/{file} (no entity segment); the entity path only
survives as a Cloudflare Worker rewrite. Digest-mapped manifests now
request the canonical path directly, skipping the rewrite. Manifests
without the map (wearables/emotes, pre-v49 scenes, failed digest
fetches) carry digest-less hashes that only resolve through the
Worker's entity path, so they keep the legacy URL shapes.

Verified against the production CDN: both digest-bearing and 2-part
files serve under /v49/assets/, and the entity segment is ignored by
the rewrite (any value returns the same object).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The flag signals what the converter actually guarantees: only scenes
fetch files[], and only scene bundles upload to the canonical assets/
prefix (asset-reuse is gated on entity.type === 'scene'). The name now
says so, and the flag fires for any injected files[] — a reuse-converted
scene can legitimately list only 2-part names and still live under
assets/.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
With asset reuse permanently on, a v49+ manifest's files[] are exactly
the names bundles live under on the assets/ prefix — so InjectDepsDigests
now maps every platform-suffixed entry (2-part textures/bins included),
giving all scene files verbatim manifest names and casing.

The injection also self-gates on v49+: hybrid/world flows inject files[]
for whatever version they fetched, and a pre-v49 manifest flagged as
canonical would 404 under assets/ where its entity-scoped bundles don't
exist.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…t-circuit

InjectDepsDigests self-gates on v49+ (it has a second, ungated caller in
the hybrid flow), so the loader's check only exists to skip a wasted
manifest download for pre-v49 scenes. Its null-check goes through the
OrFailed accessor instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The v49+ check lives with the callers: the digests loader already gates
(it also skips the manifest download), and the hybrid flow now gates its
injection too. InjectDepsDigests just stores what it is given.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@decentraland-bot decentraland-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Summary

STEP 2 — Root-cause check: PASS

The PR fixes the root cause: v49+ manifests list files as <hash>_<digest>_<platform>, but the client was requesting digest-less names from the CDN and threading the digest as a separate DepsDigest field through four cache layers (intention equality, disk cache, Unity web cache, GLTF container cache). The fix embeds the digest in the hash itself so URL and every cache layer share one identity. This is a genuine root-cause fix, not a workaround.

STEP 3 — Design & integration: PASS

The consolidation into AssetBundleManifestVersion is architecturally sound:

  • cdnFiles dictionary unifies the old convertedFiles HashSet (Qm casing) and depsDigests dictionary into a single bare-hash → canonical-CDN-file-name map. The two injection methods (InjectDepsDigests for manifest files[], InjectContent for Qm casing) feed the same map, with TryAdd ensuring digest-bearing entries win regardless of injection order.
  • AssetBundleManifestVersion is the natural owner — it already held CheckCasing and TryGetDepsDigest; these are now unified into GetCdnRequestHash and ComposeCacheKey.
  • No new lifecycle units introduced — this PR removes complexity (extensions class deleted, per-frame manifest injection eliminated, feature-flag wiring deleted).
  • The removal of per-frame manifest/parentID injection in PrepareAssetBundleLoadingParametersSystem is correct: intentions now carry their manifest at creation time, eliminating per-frame reconciliation that the ECS anti-pattern guidance warns against.
  • Owner search: AssetBundleManifestVersion is created in LoadHybridSceneSystemLogic.FetchRemoteAssetBundleVersionAsync and AssetBundleManifestFallbackHelper. It is populated with content/digest data at those creation points. The new methods (GetCdnRequestHash, ComputeCacheHash, ComposeCacheKey) are consumed at intention-creation sites and in PrepareCommonArguments. No new unit duplicates an existing lifecycle.

STEP 4 — Member audit

Member Consumers Assessment
GetCdnRequestHash(bareHash) 7 call sites (emotes, wearables, thumbnails, GLTF, LOD, ISS) Well-named, single responsibility. Correctly falls back to platform-suffixed bare hash when not in the map. ✓
ComposeCacheKey(hash) 2 (GltfContainerPlugin, ResolveISSLODSystem) Migrated from extension method, same semantics. ✓
ComputeCacheHash(hash) 1 (PrepareCommonArguments) Replaces the split ComputeHashV49/ComputeHashLegacy dispatch. Correctly gates on HasCanonicalAssets(). ✓
HasCanonicalAssets() 2 (URL construction, cache hash dispatch) Correctly reflects whether files[] were injected. Gated by SupportsDepsDigests() at the injection site. ✓
AssetBundleManifestVersionOrFailed 7 call sites See finding 1 below — allocates per access when null.

STEP 5 — Line-level findings

See inline comments below. All findings are P2 (minor improvements, not blocking).

Security review: PASS

No security issues found. URL construction consolidation reduces attack surface by funneling all paths through fewer methods. No secrets, injection risks, path traversal vectors, or unsafe input handling patterns introduced.

Consumer impact analysis

The GetAssetBundleIntention API changed (field renamed, DepsDigest removed, manifest now required). All consumers are internal to unity-explorer — no published package surface is affected. AssetBundleManifestVersionExtensions was deleted, but its only consumer (ResolveISSLODSystem) was migrated to the instance method. No cross-repo breakage.


REVIEW_RESULT: PASS ✅
COMPLEXITY: COMPLEX
COMPLEXITY_REASON: Modifies core AB loading pipeline — cache keying, URL construction, intention API, and feature-flag removal — across 26 files spanning ECS systems, plugins, LOD, thumbnails, and avatar rendering.
QA_REQUIRED: YES


Reviewed by Jarvis 🤖 · Requested by Juan Ignacio Molteni [Dalkia] (<@U04RC3JBQ96>) via Slack

Comment on lines +42 to +44
/// <summary>The manifest version, or a failed sentinel when none was resolved — AB intentions require a manifest, and the sentinel is the same dead end the pipeline already handles.</summary>
[JsonIgnore]
public AssetBundleManifestVersion AssetBundleManifestVersionOrFailed => assetBundleManifestVersion ?? AssetBundleManifestVersion.CreateFailed();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Repeated allocation on property access. CreateFailed() allocates three objects (AssetBundleManifestVersion + AssetBundleManifestVersionPerPlatform + PlatformInfo) on every access when assetBundleManifestVersion is null. Called from 7 per-entity-load paths (PrepareGltfAssetLoadingSystem, ResolveISSLODSystem, GltfContainerPlugin lambda, LoadEmotesByPointersSystem, WearablePolymorphicBehaviour, SceneAssetBundleDigestsLoader). In practice the manifest is always set before these paths run, but caching the sentinel is a simple safety net.

Suggested change
/// <summary>The manifest version, or a failed sentinel when none was resolved — AB intentions require a manifest, and the sentinel is the same dead end the pipeline already handles.</summary>
[JsonIgnore]
public AssetBundleManifestVersion AssetBundleManifestVersionOrFailed => assetBundleManifestVersion ?? AssetBundleManifestVersion.CreateFailed();
// Cached: CreateFailed() allocates three objects; callers invoke this from per-entity-load paths.
private static readonly AssetBundleManifestVersion FAILED_MANIFEST = AssetBundleManifestVersion.CreateFailed();
/// <summary>The manifest version, or a failed sentinel when none was resolved — AB intentions require a manifest, and the sentinel is the same dead end the pipeline already handles.</summary>
[JsonIgnore]
public AssetBundleManifestVersion AssetBundleManifestVersionOrFailed => assetBundleManifestVersion ?? FAILED_MANIFEST;

Comment on lines +110 to +113
public Hash128 ComputeCacheHash(string hash) =>
HasCanonicalAssets()
? ComputeHashV49(hash, GetAssetBundleManifestVersion()!)
: ComputeHashLegacy(hash, GetAssetBundleManifestBuildDate()!);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Redundant null-forgiving operators. GetAssetBundleManifestVersion() and GetAssetBundleManifestBuildDate() both return string (non-nullable), so the trailing ! is unnecessary. Removing them avoids misleading the reader into thinking nullability is being suppressed here.

Suggested change
public Hash128 ComputeCacheHash(string hash) =>
HasCanonicalAssets()
? ComputeHashV49(hash, GetAssetBundleManifestVersion()!)
: ComputeHashLegacy(hash, GetAssetBundleManifestBuildDate()!);
public Hash128 ComputeCacheHash(string hash) =>
HasCanonicalAssets()
? ComputeHashV49(hash, GetAssetBundleManifestVersion())
: ComputeHashLegacy(hash, GetAssetBundleManifestBuildDate());

Comment on lines +150 to +151
public string GetAssetBundleManifestVersion() =>
IPlatform.DEFAULT.Is(IPlatform.Kind.Windows) ? assets?.windows!.version! : assets?.mac!.version!;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Null-forgiving operators without justifying comment (CLAUDE.md § Nullable Reference Types). The ! after version and on windows/mac suppresses real nullable warnings — assets is nullable, so assets?.windows can be null. A brief comment explaining the safety invariant satisfies the project rule.

Suggested change
public string GetAssetBundleManifestVersion() =>
IPlatform.DEFAULT.Is(IPlatform.Kind.Windows) ? assets?.windows!.version! : assets?.mac!.version!;
// ! safe: assets is always set by factory methods (CreateFromFallback, CreateFailed, CreateManualManifest, CreateForLOD); callers gate on assetBundleManifestRequestFailed before reaching here.
public string GetAssetBundleManifestVersion() =>
IPlatform.DEFAULT.Is(IPlatform.Kind.Windows) ? assets?.windows!.version! : assets?.mac!.version!;

Comment on lines +153 to +154
private string GetAssetBundleManifestBuildDate() =>
IPlatform.DEFAULT.Is(IPlatform.Kind.Windows) ? assets?.windows!.buildDate! : assets?.mac!.buildDate!;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Same null-forgiving issue as GetAssetBundleManifestVersion() above. Add a justifying comment for the ! operators.

Suggested change
private string GetAssetBundleManifestBuildDate() =>
IPlatform.DEFAULT.Is(IPlatform.Kind.Windows) ? assets?.windows!.buildDate! : assets?.mac!.buildDate!;
// ! safe: only reached via ComputeCacheHash on manifests with known platform info (factory-set assets).
private string GetAssetBundleManifestBuildDate() =>
IPlatform.DEFAULT.Is(IPlatform.Kind.Windows) ? assets?.windows!.buildDate! : assets?.mac!.buildDate!;

if (IPlatform.DEFAULT.Is(IPlatform.Kind.Windows))
for (var i = 0; i < entityDefinitionContent.Length; i++)
convertedFiles.Add($"{entityDefinitionContent[i].hash}" + PlatformUtils.GetCurrentPlatform());
// TryAdd so digest-bearing entries from InjectDepsDigests always win, regardless of injection order.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Comment narrates caller behavior (CLAUDE.md § Anti-Patterns: "A comment must state only what the annotated code itself does or guarantees"). This comment describes what callers/other methods will experience rather than what TryAdd does here.

Suggested change
// TryAdd so digest-bearing entries from InjectDepsDigests always win, regardless of injection order.
// TryAdd keeps the first entry for each key; a digest-bearing name already stored by InjectDepsDigests is never overwritten.

- cache the failed-manifest sentinel instead of allocating per access
- drop redundant null-forgiving operators in ComputeCacheHash
- justify the remaining ! operators on the platform-info getters
- reword the TryAdd comment to describe the line, not its callers

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@dalkia
dalkia marked this pull request as draft July 23, 2026 01:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants