Skip to content

Commit d60b33e

Browse files
dalkiaclaude
andcommitted
feat: load local asset bundles in local scene development (lsd-local-ab)
Adds an lsd-local-ab app arg / launch-settings toggle so local scene development can load the scene's asset bundles from a locally running abgen server (JIT converter) instead of raw GLTFs, previewing scenes with production-grade assets. - New AppArgsFlags.LSD_LOCAL_AB + RealmLaunchSettings.useLocalAssetBundles (editor toggle under Localhost), exposed as FeatureId.LSD_LOCAL_ASSET_BUNDLES - In this mode the scene's AB manifest is fetched from optimized-assets-url (defaults to http://127.0.0.1:5147, abgen's default port) instead of the manual LSD manifest; a failed fetch degrades the whole scene to raw GLTFs - Skips the CDN scene-code override in LSD so scene code always comes from the live preview server - Disables Unity's AB disk cache in LSD (path-based dev-server hashes make any disk cache stale-prone) - Takes precedence over lsd-use-remote-ab when both are set (with warning) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 89f53c0 commit d60b33e

19 files changed

Lines changed: 187 additions & 22 deletions

File tree

Explorer/Assets/DCL/FeatureFlags/FeaturesRegistry.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ public partial class FeaturesRegistry
2424

2525
public FeaturesRegistry(
2626
IAppArgs appArgs,
27-
bool localSceneDevelopment)
27+
bool localSceneDevelopment,
28+
bool useLocalAssetBundles = false)
2829
{
2930
FeatureFlagsConfiguration featureFlags = FeatureFlagsConfiguration.Instance;
3031
bool isEditor = Application.isEditor;
@@ -37,6 +38,7 @@ public FeaturesRegistry(
3738
[FeatureId.FRIENDS_ONLINE_STATUS] = appArgs.HasFlag(AppArgsFlags.FRIENDS_ONLINE_STATUS) || featureFlags.IsEnabled(FeatureFlagsStrings.FRIENDS_ONLINE_STATUS),
3839
[FeatureId.PROFILE_NAME_EDITOR] = appArgs.ResolveFeatureFlagArg(AppArgsFlags.PROFILE_NAME_EDITOR, featureFlags.IsEnabled(FeatureFlagsStrings.PROFILE_NAME_EDITOR) || Application.isEditor),
3940
[FeatureId.LOCAL_SCENE_DEVELOPMENT] = localSceneDevelopment,
41+
[FeatureId.LSD_LOCAL_ASSET_BUNDLES] = localSceneDevelopment && useLocalAssetBundles,
4042
[FeatureId.CHAT_MESSAGE_RATE_LIMIT] = appArgs.ResolveFeatureFlagArg(AppArgsFlags.CHAT_MESSAGE_RATE_LIMIT, featureFlags.IsEnabled(FeatureFlagsStrings.CHAT_MESSAGE_RATE_LIMIT)),
4143
[FeatureId.CHAT_MESSAGE_BUFFER] = appArgs.ResolveFeatureFlagArg(AppArgsFlags.CHAT_MESSAGE_BUFFER, featureFlags.IsEnabled(FeatureFlagsStrings.CHAT_MESSAGE_BUFFER_CONFIG)),
4244
[FeatureId.MARKETPLACE_CREDITS] = featureFlags.IsEnabled(FeatureFlagsStrings.MARKETPLACE_CREDITS),
@@ -207,5 +209,6 @@ public enum FeatureId
207209
HARDWARE_FINGERPRINT = 66,
208210
USER_CREDITS = 67,
209211
CREDITS_WEARABLE_PURCHASE = 68,
212+
LSD_LOCAL_ASSET_BUNDLES = 69,
210213
}
211214
}

Explorer/Assets/DCL/Infrastructure/ECS/Unity/GLTFContainer/Asset/Systems/PrepareGltfAssetLoadingSystem.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,11 @@ private void Prepare(in Entity entity, ref GetGltfContainerAssetIntention intent
6262
{
6363
if (options.UseRemoteAssetBundles)
6464
loadRawGltf |= sceneData.SceneContent.IsRawAsset(intention.Name);
65+
else if (options.UseLocalAssetBundles)
66+
67+
// Whole-scene degrade: when the manifest could not be fetched from the local
68+
// asset-bundle server every AB request would dead-end, so load raw GLTFs instead.
69+
loadRawGltf |= sceneData.SceneEntityDefinition.assetBundleManifestVersion is not { assetBundleManifestRequestFailed: false };
6570
else
6671
loadRawGltf = true;
6772
}
@@ -85,6 +90,7 @@ public struct Options
8590
{
8691
public bool LocalSceneDevelopment;
8792
public bool UseRemoteAssetBundles;
93+
public bool UseLocalAssetBundles;
8894
public bool PreviewingBuilderCollection;
8995
}
9096
}

Explorer/Assets/DCL/Infrastructure/ECS/Unity/GLTFContainer/Asset/Tests/PrepareGltfAssetLoadingSystemShould.cs

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,81 @@ public void CreateGltfIntentionForRawAssetWhenLsdWithRemoteAb()
114114
Assert.That(world.Has<GetGLTFIntention>(e), Is.True);
115115
}
116116

117+
[Test]
118+
public void CreateAssetBundleIntentionWhenLsdWithLocalAb()
119+
{
120+
//Arrange
121+
sceneData.SceneEntityDefinition.Returns(new SceneEntityDefinition
122+
{
123+
assetBundleManifestVersion = AssetBundleManifestVersion.CreateFromFallback("v49", "1"),
124+
});
125+
126+
BuildSystem(new PrepareGltfAssetLoadingSystem.Options
127+
{
128+
LocalSceneDevelopment = true,
129+
UseLocalAssetBundles = true,
130+
});
131+
132+
var intent = new GetGltfContainerAssetIntention("TEST", "TEST_HASH", new CancellationTokenSource());
133+
Entity e = world.Create(intent);
134+
135+
//Act
136+
system.Update(0);
137+
138+
//Assert
139+
Assert.That(world.Has<GetGLTFIntention>(e), Is.False);
140+
Assert.That(world.TryGet(e, out GetAssetBundleIntention result), Is.True);
141+
Assert.That(result.Hash, Is.EqualTo($"TEST_HASH{PlatformUtils.GetCurrentPlatform()}"));
142+
}
143+
144+
[Test]
145+
public void CreateGltfIntentionWhenLsdWithLocalAbAndManifestFailed()
146+
{
147+
//Arrange
148+
sceneData.SceneEntityDefinition.Returns(new SceneEntityDefinition
149+
{
150+
assetBundleManifestVersion = AssetBundleManifestVersion.CreateFailed(),
151+
});
152+
153+
BuildSystem(new PrepareGltfAssetLoadingSystem.Options
154+
{
155+
LocalSceneDevelopment = true,
156+
UseLocalAssetBundles = true,
157+
});
158+
159+
var intent = new GetGltfContainerAssetIntention("TEST", "TEST_HASH", new CancellationTokenSource());
160+
Entity e = world.Create(intent);
161+
162+
//Act
163+
system.Update(0);
164+
165+
//Assert
166+
Assert.That(world.Has<GetAssetBundleIntention>(e), Is.False);
167+
Assert.That(world.Has<GetGLTFIntention>(e), Is.True,
168+
"A failed manifest fetch must degrade the whole scene to raw GLTF loading");
169+
}
170+
171+
[Test]
172+
public void CreateGltfIntentionWhenLsdWithLocalAbAndNoManifest()
173+
{
174+
//Arrange: default SceneEntityDefinition carries no manifest at all
175+
BuildSystem(new PrepareGltfAssetLoadingSystem.Options
176+
{
177+
LocalSceneDevelopment = true,
178+
UseLocalAssetBundles = true,
179+
});
180+
181+
var intent = new GetGltfContainerAssetIntention("TEST", "TEST_HASH", new CancellationTokenSource());
182+
Entity e = world.Create(intent);
183+
184+
//Act
185+
system.Update(0);
186+
187+
//Assert
188+
Assert.That(world.Has<GetAssetBundleIntention>(e), Is.False);
189+
Assert.That(world.Has<GetGLTFIntention>(e), Is.True);
190+
}
191+
117192
[Test]
118193
public void LoadFromCache()
119194
{

Explorer/Assets/DCL/Infrastructure/Global/AppArgs/AppArgsFlags.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ public static class AppArgsFlags
120120
public const string LSD_USE_REMOTE_AB = "lsd-use-remote-ab";
121121
public const string LSD_REMOTE_AB_SERVER = "lsd-remote-ab-server";
122122
public const string LSD_REMOTE_AB_WORLD = "lsd-remote-ab-world";
123+
public const string LSD_LOCAL_AB = "lsd-local-ab";
123124

124125
public const string OPTIMIZED_ASSETS_URL = "optimized-assets-url";
125126

Explorer/Assets/DCL/Infrastructure/Global/Dynamic/BootstrapContainer.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ public class BootstrapContainer : DCLGlobalContainer<BootstrapSettings>
6060
public IAppArgs AppArgs { get; private set; }
6161
public ILaunchMode LaunchMode { get; private set; }
6262
public bool UseRemoteAssetBundles { get; private set; }
63+
public bool UseLocalAssetBundles { get; private set; }
6364
public DecentralandEnvironment Environment { get; private set; }
6465
public RealmClock RealmClock { get; } = new ();
6566
public WebRequestsContainer WebRequestsContainer { get; private set; }
@@ -106,6 +107,7 @@ public static async UniTask<BootstrapContainer> CreateAsync(
106107
WebBrowser = browser,
107108
LaunchMode = realmLaunchSettings,
108109
UseRemoteAssetBundles = realmLaunchSettings.useRemoteAssetsBundles,
110+
UseLocalAssetBundles = realmLaunchSettings.useLocalAssetBundles,
109111
AppArgs = applicationParametersParser,
110112
DebugSettings = debugSettings,
111113
VolumeBus = new VolumeBus(),
@@ -129,7 +131,11 @@ await bootstrapContainer.InitializeContainerAsync<BootstrapContainer, BootstrapS
129131
};
130132

131133
var cdpClient = ChromeDevToolHandler.New(applicationParametersParser.HasFlag(AppArgsFlags.LAUNCH_CDP_MONITOR_ON_START), applicationParametersParser);
132-
WebRequestsContainer? webRequestsContainer = await WebRequestsContainer.CreateAsync(settingsContainer, identityCache, debugContainer.Builder, decentralandUrlsSource, cdpClient, container.DiagnosticsContainer.SentrySampler, container.RealmClock, ct);
134+
// LSD dev-server hashes are path-based, not content-based, so any disk-level AB cache can
135+
// serve stale bundles across reloads and sessions; the custom disk caches are already
136+
// disabled in this mode, this closes the Unity Caching-backed one.
137+
WebRequestsContainer? webRequestsContainer = await WebRequestsContainer.CreateAsync(settingsContainer, identityCache, debugContainer.Builder, decentralandUrlsSource, cdpClient, container.DiagnosticsContainer.SentrySampler, container.RealmClock, ct,
138+
disableABCache: realmLaunchSettings.CurrentMode == DCL.Utility.LaunchMode.LocalSceneDevelopment);
133139
container.WebRequestsContainer = webRequestsContainer;
134140
var realmUrls = new RealmUrls(realmLaunchSettings, new RealmNamesMap(webRequestsContainer.WebRequestController), decentralandUrlsSource);
135141

Explorer/Assets/DCL/Infrastructure/Global/Dynamic/Bootstraper.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ await StaticContainer.CreateAsync(
122122
bootstrapContainer.CompositeWeb3Provider!,
123123
bootstrapContainer.LaunchMode,
124124
bootstrapContainer.UseRemoteAssetBundles,
125+
bootstrapContainer.UseLocalAssetBundles,
125126
world,
126127
playerEntity,
127128
memoryCap,
@@ -222,7 +223,7 @@ public async UniTask InitializeFeatureFlagsAsync(IWeb3Identity? identity, IDecen
222223

223224
public void InitializeFeaturesRegistry()
224225
{
225-
FeaturesRegistry.Initialize(new FeaturesRegistry(appArgs, realmLaunchSettings.CurrentMode is LaunchMode.LocalSceneDevelopment));
226+
FeaturesRegistry.Initialize(new FeaturesRegistry(appArgs, realmLaunchSettings.CurrentMode is LaunchMode.LocalSceneDevelopment, realmLaunchSettings.useLocalAssetBundles));
226227

227228
// Gate the v49 deps-digest cache-keying scheme behind the feature flag. Off by default means every
228229
// manifest reports SupportsDepsDigests() == false and the entire pipeline takes the legacy code path.

Explorer/Assets/DCL/Infrastructure/Global/Dynamic/GlobalWorldFactory.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ public class GlobalWorldFactory
7373
private readonly ISceneReadinessReportQueue sceneReadinessReportQueue;
7474
private readonly HybridSceneParams hybridSceneParams;
7575
private readonly bool localSceneDevelopment;
76+
private readonly bool useLocalAssetBundles;
7677
private readonly IProfileRepository profileRepository;
7778
private readonly bool useRemoteAssetBundles;
7879
private readonly HashSet<Vector2Int> roadCoordinates;
@@ -123,6 +124,7 @@ public GlobalWorldFactory(in StaticContainer staticContainer,
123124
this.currentSceneInfo = currentSceneInfo;
124125
this.lodCache = lodCache;
125126
this.localSceneDevelopment = FeaturesRegistry.Instance.IsEnabled(FeatureId.LOCAL_SCENE_DEVELOPMENT);
127+
this.useLocalAssetBundles = FeaturesRegistry.Instance.IsEnabled(FeatureId.LSD_LOCAL_ASSET_BUNDLES);
126128
this.sceneReadinessReportQueue = sceneReadinessReportQueue;
127129
this.sceneRoomStatus = sceneRoomStatus;
128130
this.world = world;
@@ -155,8 +157,8 @@ public GlobalWorld Create(ISceneFactory sceneFactory, Entity playerEntity)
155157

156158
IReleasablePerformanceBudget sceneBudget = new ConcurrentLoadingPerformanceBudget(staticSettings.ScenesLoadingBudget);
157159

158-
LoadSceneDefinitionListSystem.InjectToWorld(ref builder, webRequestController, localSceneDevelopment, NoCache<SceneDefinitions, GetSceneDefinitionList>.INSTANCE, entitiesAnalytics);
159-
LoadSceneDefinitionSystem.InjectToWorld(ref builder, webRequestController, localSceneDevelopment, NoCache<SceneEntityDefinition, GetSceneDefinition>.INSTANCE);
160+
LoadSceneDefinitionListSystem.InjectToWorld(ref builder, webRequestController, localSceneDevelopment, useLocalAssetBundles, NoCache<SceneDefinitions, GetSceneDefinitionList>.INSTANCE, entitiesAnalytics);
161+
LoadSceneDefinitionSystem.InjectToWorld(ref builder, webRequestController, localSceneDevelopment, useLocalAssetBundles, NoCache<SceneEntityDefinition, GetSceneDefinition>.INSTANCE);
160162

161163
LoadSceneSystemLogicBase loadSceneSystemLogic;
162164

@@ -179,7 +181,7 @@ public GlobalWorld Create(ISceneFactory sceneFactory, Entity playerEntity)
179181
loadSceneSystemLogic = new LoadHybridSceneSystemLogic(webRequestController, assetBundleCdnUrl, hybridSceneParams, worldContentServerContentsUrl, worldContentServerBaseUrl);
180182
}
181183
else
182-
loadSceneSystemLogic = new LoadSceneSystemLogic(webRequestController, assetBundleCdnUrl);
184+
loadSceneSystemLogic = new LoadSceneSystemLogic(webRequestController, assetBundleCdnUrl, localSceneDevelopment);
183185

184186
LoadSceneSystem.InjectToWorld(ref builder,
185187
loadSceneSystemLogic,

Explorer/Assets/DCL/Infrastructure/Global/Dynamic/MainSceneLoader.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,10 @@ private async UniTask InitializeFlowAsync(CancellationToken ct)
232232
applicationParametersParser.TryGetValue(AppArgsFlags.GATEKEEPER_URL, out string? cliGatekeeperUrl);
233233
applicationParametersParser.TryGetValue(AppArgsFlags.OPTIMIZED_ASSETS_URL, out string? cliOptimizedAssetsUrl);
234234

235+
// Local asset bundles default to a local abgen on its default port unless an explicit URL is given
236+
if (string.IsNullOrEmpty(cliOptimizedAssetsUrl) && launchSettings.useLocalAssetBundles)
237+
cliOptimizedAssetsUrl = RealmLaunchSettings.DEFAULT_LOCAL_ASSET_BUNDLES_URL;
238+
235239
var decentralandUrlsSource = new GatewayUrlsSource(
236240
decentralandEnvironment,
237241
realmData,

Explorer/Assets/DCL/Infrastructure/Global/Dynamic/RealmLaunchSettings.cs

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using DCL.CommunicationData.URLHelpers;
2+
using DCL.Diagnostics;
23
using ECS.SceneLifeCycle.Realm;
34
using Global.AppArgs;
45
using System;
@@ -19,6 +20,11 @@ namespace Global.Dynamic
1920
[Serializable]
2021
public class RealmLaunchSettings : ILaunchMode
2122
{
23+
/// <summary>
24+
/// abgen's default port; used for local asset bundles when no explicit --optimized-assets-url is given.
25+
/// </summary>
26+
public const string DEFAULT_LOCAL_ASSET_BUNDLES_URL = "http://127.0.0.1:5147";
27+
2228
[Serializable]
2329
public struct PredefinedScenes
2430
{
@@ -35,6 +41,9 @@ public struct PredefinedScenes
3541
[SerializeField] internal string remoteHibridWorld = "MetadyneLabs.dcl.eth";
3642
[SerializeField] internal HybridSceneContentServer remoteHybridSceneContentServer = HybridSceneContentServer.Goerli;
3743
[SerializeField] internal bool useRemoteAssetsBundles;
44+
[SerializeField] [Tooltip("Local scene development only: load the scene's asset bundles from a locally running abgen instead of loading "
45+
+ "raw GLTFs. The server URL comes from --optimized-assets-url, defaulting to " + DEFAULT_LOCAL_ASSET_BUNDLES_URL
46+
+ " (abgen's default port) when not provided")] internal bool useLocalAssetBundles;
3847
[SerializeField] [Tooltip("In Worlds there is one LiveKit room for all scenes so it's possible to communicate changes outside of the scene. "
3948
+ "In Genesis City there are individual LiveKit rooms and only one connection at a time is maintained. "
4049
+ "Toggle this flag to equalize this behavior")] internal bool isolateSceneCommunication;
@@ -75,7 +84,7 @@ public HybridSceneParams CreateHybridSceneParams()
7584
{
7685
return new HybridSceneParams
7786
{
78-
EnableHybridScene = useRemoteAssetsBundles,
87+
EnableHybridScene = useRemoteAssetsBundles && !useLocalAssetBundles,
7988
HybridSceneContentServer = remoteHybridSceneContentServer,
8089
World = remoteHybridSceneContentServer.Equals(HybridSceneContentServer.World)
8190
? remoteHibridWorld
@@ -105,7 +114,17 @@ private void ParseRealmAppParameter(IAppArgs appParameters, string realmParamVal
105114

106115
if (isLocalSceneDevelopment)
107116
{
108-
SetLocalSceneDevelopmentRealm(realmParamValue, appParameters.HasFlag(AppArgsFlags.LSD_USE_REMOTE_AB) || useRemoteAssetsBundles);
117+
bool useLocalAB = appParameters.HasFlag(AppArgsFlags.LSD_LOCAL_AB) || useLocalAssetBundles;
118+
bool useRemoteAB = appParameters.HasFlag(AppArgsFlags.LSD_USE_REMOTE_AB) || useRemoteAssetsBundles;
119+
120+
if (useLocalAB && useRemoteAB)
121+
{
122+
ReportHub.LogWarning(ReportCategory.ASSET_BUNDLES, $"Both '{AppArgsFlags.LSD_LOCAL_AB}' and '{AppArgsFlags.LSD_USE_REMOTE_AB}' were provided: local asset bundles take precedence");
123+
useRemoteAB = false;
124+
}
125+
126+
useLocalAssetBundles = useLocalAB;
127+
SetLocalSceneDevelopmentRealm(realmParamValue, useRemoteAB);
109128

110129
if (appParameters.TryGetValue(AppArgsFlags.LSD_REMOTE_AB_SERVER, out string? serverValue))
111130
ParseLSDRemoteABServer(serverValue);

Explorer/Assets/DCL/Infrastructure/Global/Editor/RealmLaunchSettingsDrawer.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,10 @@ private static Rect DrawLocalhost(Rect position, SerializedProperty parent, Init
134134
SerializedProperty remoteWorldContentServerProperty = parent.FindPropertyRelative(nameof(RealmLaunchSettings.remoteHibridWorld));
135135
SerializedProperty remoteSceneContentServerProperty = parent.FindPropertyRelative(nameof(RealmLaunchSettings.remoteHybridSceneContentServer));
136136
SerializedProperty useHibridAssets = parent.FindPropertyRelative(nameof(RealmLaunchSettings.useRemoteAssetsBundles));
137+
SerializedProperty useLocalAssets = parent.FindPropertyRelative(nameof(RealmLaunchSettings.useLocalAssetBundles));
138+
139+
EditorGUI.PropertyField(position, useLocalAssets);
140+
position.y += singleLineHeight;
137141

138142
EditorGUI.PropertyField(position, useHibridAssets);
139143
position.y += singleLineHeight;
@@ -256,7 +260,7 @@ public override float GetPropertyHeight(SerializedProperty property, GUIContent
256260
switch (initialRealmValue)
257261
{
258262
case InitialRealm.Localhost:
259-
var fieldToAdd = 1;
263+
var fieldToAdd = 2;
260264

261265
if (property.FindPropertyRelative(nameof(RealmLaunchSettings.useRemoteAssetsBundles)).boolValue)
262266
{

0 commit comments

Comments
 (0)