Skip to content

Commit 1d4ffe4

Browse files
authored
Merge pull request #9048 from decentraland/release/2026-06-22
release: 2026-06-22
2 parents b5d1f4d + 8ace21f commit 1d4ffe4

215 files changed

Lines changed: 5002 additions & 3144 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/agents/dcl-sdk-feature-implementation/dcl-explorer-specialist.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ After installing a new protocol package, always re-run `npm run build-protocol`
4646

4747
## Protocol Generation
4848

49-
> **Prerequisite:** `build-protocol` runs a Python `protoc` plugin (`protoc-gen-bitwise`, for the quantized/bit-packed Pulse network state) in addition to the Node toolchain. **Python 3** must be on `PATH` with the **`protobuf`** package installed (`python3 -m pip install protobuf`), otherwise generation fails with `ModuleNotFoundError: No module named 'google'`.
49+
> **Prerequisite:** Node toolchain only. `build-protocol` runs the `protoc-gen-bitwise` plugin (for the quantized/bit-packed Pulse network state), a dependency-free Node script bundled in `@dcl/protocol` — no Python or extra packages required.
5050
5151
To generate C# code from protocol definitions:
5252
```bash

.claude/skills/plugin-architecture/SKILL.md

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,19 @@ Composition root (MainSceneLoader / Bootstrap)
6969
- **Plugins read from containers. Plugins never construct, initialize, or mutate a container.** If you find yourself writing `container.SomeField = new Thing()` inside `InitializeAsync`, the graph is inverted — stop and restructure.
7070
- Containers are constructed from a single place (the composition root or a parent container). The constructor takes pre-built dependencies; the container exposes them.
7171
- If a plugin needs a dependency that doesn't exist yet, **create a scoped container** for the feature and construct it from the composition root. You can have as many small scoped containers as you want — they are cheap, and they keep the dependency graph honest.
72-
- Do not reach for `ObjectProxy` to paper over a missing dependency. `ObjectProxy` is an anti-pattern documented in CLAUDE.md; it exists only to unbreak legacy circular deps. For new code, the right answer is a scoped container.
72+
- **Never introduce a new `ObjectProxy`.** The codebase was swept of it; the only legitimate remaining instances model true runtime lifecycles (`MainPlayerAvatarBaseProxy`, `ExposedCameraData.CameraEntityProxy`). For everything else use a decoupling recipe below.
73+
74+
### Decoupling without ObjectProxy
75+
76+
Match the situation to the recipe (full rationale in `docs/architecture-overview.md` § "Deferred dependencies — decoupling without ObjectProxy"):
77+
78+
| Situation | Fix | Existing example |
79+
|---|---|---|
80+
| Service trapped in a late, UI-owning container | Split services into their own container created before any consumer | `FriendsServicesContainer` (services) vs `FriendsContainer` (UI) |
81+
| Dependency exists only when a feature flag is on | Pass `T?` (null = disabled) and null-check where `.Configured` used to be, or use a null-object | `IFriendsService?`; `NullUserBlockingCache`, `NullRoomHub` |
82+
| Scene-world plugin needs comms/multiplayer services | Construct the plugin in `DynamicWorldContainer.WorldPlugins`, not in `StaticContainer` with an empty slot | `AvatarAttachPlugin`, `SceneMaskedEmotePlugin`, `RealmInfoPlugin` |
83+
| Dependency is per-scene data | Add it to `ECSWorldInstanceSharedDependencies`, threaded from `SceneFactory` | `IRoomHub` for the media streaming room |
84+
| Object created in a plugin's async `InitializeAsync` but consumed by earlier objects | Create it eagerly in a container; the plugin only *attaches* the UI-bound parts | `NavmapCommandBus` + `NavmapCommandFactory.AttachUiControllers` |
7385

7486
**Symptoms of inverted flow:**
7587

@@ -82,15 +94,17 @@ Composition root (MainSceneLoader / Bootstrap)
8294

8395
Created first. Produces common dependencies and world plugins.
8496
- Creates `IComponentPoolsRegistry`, `CacheCleaner`, `IAssetsProvisioner`
85-
- Instantiates world plugins (`IDCLWorldPlugin`) with their dependencies
97+
- Instantiates most world plugins (`IDCLWorldPlugin`) as `ECSWorldPlugins`
8698
- Provides `StaticSettings` (all plugin settings)
8799

88100
### DynamicWorldContainer
89101

90102
Created after StaticContainer. Holds global plugins and runtime state.
91103
- Instantiates global plugins (`IDCLGlobalPlugin`)
104+
- Instantiates the world plugins whose dependencies (comms, multiplayer) only exist here, exposed as `WorldPlugins`; the bootstrap concatenates them with `StaticContainer.ECSWorldPlugins` for initialization and scene-world creation
92105
- Creates `RealmController`, `GlobalWorldFactory`
93106
- Manages scene lifecycle
107+
- Never writes into `StaticContainer` — if a value created here is needed by something in `StaticContainer`, that something is constructed in the wrong container
94108

95109
### ComponentsContainer
96110

.claude/skills/sdk-component-implementation/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ In `js-sdk-toolchain`, generate serialization code and optional helper functions
3030

3131
In `unity-explorer`:
3232
1. Run protocol update: `npm install @dcl/protocol@experimental && npm run build-protocol`
33-
- Requires **Python 3** on `PATH` with the **`protobuf`** package (`python3 -m pip install protobuf`) `build-protocol` runs a Python `protoc` plugin (`protoc-gen-bitwise`); without it the build fails with `ModuleNotFoundError: No module named 'google'`.
33+
- Node/npm only `build-protocol` runs the `protoc-gen-bitwise` plugin, a dependency-free Node script bundled in `@dcl/protocol` (no Python or extra packages required).
3434
2. Add partial class to `IDirtyMarker.cs`
3535
3. Register in `ComponentsContainer.cs` using `SDKComponentBuilder<T>`
3636
4. Create feature folder under `Explorer/Assets/DCL/SDKComponents/<Feature>/`

.github/workflows/pr-comment-delete-artifact-url.yml

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,35 +16,44 @@ permissions:
1616
pull-requests: write
1717

1818
jobs:
19-
delete-comment:
19+
pre-validation:
2020
runs-on: ubuntu-latest
21+
outputs:
22+
pr-number: ${{ steps.check-pr.outputs.pr-number }}
2123
steps:
22-
- name: Get PR info
24+
- name: Check if PR number exists
25+
id: check-pr
2326
env:
2427
WORKFLOW_RUN_EVENT_OBJ: ${{ toJSON(github.event.workflow_run) }}
25-
OWNER: ${{ github.repository_owner }}
26-
REPO: ${{ github.event.repository.name }}
2728
run: |
28-
PR_NUMBER=$(jq -r '.pull_requests[0].number' \
29-
<<< "$WORKFLOW_RUN_EVENT_OBJ")
30-
29+
PR_NUMBER=$(jq -r '.pull_requests[0].number' <<< "$WORKFLOW_RUN_EVENT_OBJ")
3130
echo "Pull request Number: $PR_NUMBER"
32-
echo "PR_NUMBER=$PR_NUMBER" >> "$GITHUB_ENV"
31+
if [[ -z "$PR_NUMBER" || "$PR_NUMBER" == "null" ]]; then
32+
echo "PR_NUMBER is not set, skipping comment update."
33+
exit 0
34+
fi
35+
echo "pr-number=$PR_NUMBER" >> $GITHUB_OUTPUT
36+
37+
delete-comment:
38+
needs: pre-validation
39+
if: needs.pre-validation.outputs.pr-number != ''
40+
runs-on: ubuntu-latest
41+
steps:
3342
- name: Find Comment
3443
uses: peter-evans/find-comment@v2
3544
id: find-comment
3645
with:
37-
issue-number: ${{ env.PR_NUMBER }}
46+
issue-number: ${{ needs.pre-validation.outputs.pr-number }}
3847
comment-author: 'github-actions[bot]'
3948
- name: Update Comment
4049
uses: peter-evans/create-or-update-comment@v3
4150
with:
42-
issue-number: ${{ env.PR_NUMBER }}
51+
issue-number: ${{ needs.pre-validation.outputs.pr-number }}
4352
comment-id: ${{ steps.find-comment.outputs.comment-id }}
4453
edit-mode: replace
4554
body: |-
46-
![badge] <img src="https://ui.decentraland.org/decentraland_256x256.png" width="30">
47-
55+
![badge] <img src="https://ui.decentraland.org/decentraland_256x256.png" width="30">
56+
4857
New build in progress, come back later!
4958
5059
[badge]: https://img.shields.io/badge/Build-Pending!-ffff00?logo=github&style=for-the-badge

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ Reviewers have repeatedly identified AI-generated code by these smells. Check yo
121121
* **Defensive null-checks against non-null declarations.** If the declared type is `T` (not `T?`), don't null-check it. Trust the annotations. Every redundant check is a lie to the reader about what can happen.
122122
* **Debug/mock code in production hot paths.** Runtime bools like `DebugRandomizeX` execute on every call in retail builds. Guard debug branches with `#if UNITY_EDITOR` or move them to an editor-only companion system — never rely on a runtime flag alone.
123123
* **Plugins initializing or mutating containers.** Containers are constructed top-down from the composition root. Plugins **read** from containers. A plugin that writes into a container is a signal the dependency graph is inverted — create a scoped container instead.
124-
* **`ObjectProxy` is an anti-pattern**, not a solution. It exists to paper over circular dependencies. If you reach for it, the right fix is almost always restructuring the dependency flow.
124+
* **`ObjectProxy` is an anti-pattern** — never introduce a new instance. The codebase has been swept of it; the only legitimate remaining uses model true runtime lifecycles (`StaticContainer.MainPlayerAvatarBaseProxy` — avatar set/released as the player loads, and `ExposedCameraData.CameraEntityProxy` — entity created during world build). Every other use was a wiring-order mistake and was eliminated by restructuring. To decouple without it, pick the matching recipe from `docs/architecture-overview.md` § "Deferred dependencies — decoupling without ObjectProxy": create the service before its consumers (hoist it out of a UI container into its own container), model an optional feature as a nullable dependency or null-object, let the container that owns a late-created service also construct the plugins that need it (`DynamicWorldContainer.WorldPlugins`), or pass per-scene data through `ECSWorldInstanceSharedDependencies`.
125125
* **Retry/resolve loops without a termination condition.** A loop that re-adds the same unresolved item to the queue will spin forever when the server returns stable but empty results. Always have a "give up" predicate.
126126
* **Wiring pooled/virtualized list items per rebind.** For item pools, wire callbacks once when the item is created, not every time `SetItemData` runs. Prefer an `Action` field (single subscriber, direct assignment) over C# `event` (`+=`/`-=` churn) when there is exactly one subscriber.
127127
* **Reimplementing primitives that already exist.** Before writing manual atlas UV math, check `TMP_Sprite Asset`. Before hand-batching profile lookups, check the batched `GetProfilesAsync(IReadOnlyList<string>, ct)` overload. Before adding a bespoke event pathway, check `ViewEventBus` / `ChatEvents`.

Explorer/Assets/AddressableAssetsData/AssetGroups/Test Resources.asset

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,18 +35,13 @@ MonoBehaviour:
3535
m_ReadOnly: 0
3636
m_SerializedLabels: []
3737
FlaggedDuringContentUpdateRestriction: 0
38-
- m_GUID: 75051ef81d8168741991242a9b201866
39-
m_Address: Integration Tests World Container
40-
m_ReadOnly: 0
41-
m_SerializedLabels: []
42-
FlaggedDuringContentUpdateRestriction: 0
4338
- m_GUID: c9893189605d84349b03f89a01d6abca
4439
m_Address: Test Report Settings
4540
m_ReadOnly: 0
4641
m_SerializedLabels: []
4742
FlaggedDuringContentUpdateRestriction: 0
4843
- m_GUID: f13b19cfc3075014aaddb1fd403e2081
49-
m_Address: Integration Tests Global Container
44+
m_Address: Integration Tests Container
5045
m_ReadOnly: 0
5146
m_SerializedLabels: []
5247
FlaggedDuringContentUpdateRestriction: 0

Explorer/Assets/DCL/AuthenticationScreenFlow/AuthenticationScreenController.cs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,8 @@ protected override void OnViewInstantiated()
166166
new LoginSelectionAuthState(fsm, viewInstance, this, CurrentState, splashScreen, web3Authenticator, webBrowser, enableEmailOTP),
167167
new ProfileFetchingAuthState(fsm, viewInstance, this, CurrentState, selfProfile, storedIdentityProvider),
168168
new IdentityVerificationDappAuthState(fsm, viewInstance, this, CurrentState, web3Authenticator),
169-
new LobbyForExistingAccountAuthState(fsm, viewInstance, this, splashScreen, CurrentState, characterPreviewController)
169+
new LobbyForExistingAccountAuthState(fsm, viewInstance, this, splashScreen, CurrentState, characterPreviewController),
170+
new LobbyForNewAccountAuthState(fsm, viewInstance, this, CurrentState, characterPreviewController, selfProfile, wearablesProvider, webBrowser, webRequestController, decentralandUrlsSource, profileChangesBus)
170171
);
171172

172173
if (enableEmailOTP)
@@ -175,10 +176,7 @@ protected override void OnViewInstantiated()
175176
otpVerificationState.OTPVerified += (email, success) => OTPVerified?.Invoke(email, success);
176177
otpVerificationState.OTPResend += () => OTPResend?.Invoke();
177178

178-
fsm.AddStates(
179-
otpVerificationState,
180-
new LobbyForNewAccountAuthState(fsm, viewInstance, this, CurrentState, characterPreviewController, selfProfile, wearablesProvider, webBrowser, webRequestController, decentralandUrlsSource, profileChangesBus)
181-
);
179+
fsm.AddStates(otpVerificationState);
182180
}
183181

184182
fsm.Enter<InitAuthState>();

Explorer/Assets/DCL/AvatarRendering/AvatarShape/Components/RemoteAvatarPipeline.cs

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@ namespace DCL.AvatarRendering.AvatarShape.Components
1616
/// <summary>
1717
/// Batched pipeline for all remote avatars. Supports dynamic resizing and index recycling.
1818
/// Completion is deferred to a later system for maximum parallelism.
19-
/// Flat backing arrays are pre-filled with dummyTransform so TAA slots can be updated
20-
/// in-place via indexed access, avoiding full rebuilds on every Register/Release.
19+
/// Flat backing arrays are pre-filled with dummyTransform. Register updates the avatar's TAA
20+
/// slots in-place; Release does NOT touch the TAA (released avatars are pooled, so their
21+
/// transforms stay valid and the calculation job skips the slot) — see Release for details.
2122
/// </summary>
2223
internal class RemoteAvatarPipeline : IDisposable
2324
{
@@ -129,22 +130,23 @@ public void Release(ref AvatarTransformMatrixComponent avatarTransformMatrixComp
129130

130131
updateAvatar[validIndex] = false;
131132

132-
// Reset flat backing arrays to dummyTransform
133+
// Reset the flat backing arrays to dummyTransform (cheap managed writes) so a later
134+
// RebuildTransformAccessArrays (on resize) produces a clean TAA with no released slots.
133135
int offset = validIndex * bonesArrayLength;
134136

135137
for (int b = 0; b < bonesArrayLength; b++)
136138
flatBones[offset + b] = dummyTransform;
137139

138140
flatRoots[validIndex] = dummyTransform;
139141

140-
// Update TAA in-place — no rebuild needed
141-
if (bonesTransformAccessArray.isCreated && validIndex < taaSlotCount)
142-
{
143-
for (int b = 0; b < bonesArrayLength; b++)
144-
bonesTransformAccessArray[offset + b] = dummyTransform;
145-
146-
rootsTransformAccessArray[validIndex] = dummyTransform;
147-
}
142+
// The TAA is intentionally NOT reset here — that per-bone managed<->native loop was the
143+
// bulk-unload spike. Released avatars are returned to a GameObject pool (deactivated, NOT
144+
// destroyed), so the slot keeps referencing valid transforms: the gather jobs read them
145+
// harmlessly and BoneMatrixCalculationJob skips the slot (updateAvatar = false, set above).
146+
// Reuse overwrites the slot in Register; a resize rebuilds the TAA from the flat arrays
147+
// (already dummied above). Invariant this relies on: a released avatar's transforms are not
148+
// destroyed while its slot is still live in the TAA (guaranteed by pooling; the TAA is
149+
// disposed at teardown).
148150

149151
releasedIndexes.Push(avatarTransformMatrixComponent.IndexInGlobalJobArray);
150152
avatarTransformMatrixComponent.IndexInGlobalJobArray = GlobalJobArrayIndex.Unassign();
@@ -245,7 +247,7 @@ public void Dispose()
245247
Job.Dispose();
246248
stopwatch.LogStep("job.Dispose");
247249

248-
if (bonesTransformAccessArray.isCreated)
250+
if (bonesTransformAccessArray.isCreated)
249251
{
250252
bonesTransformAccessArray.Dispose();
251253
stopwatch.LogStep("bonesTransformAccessArray.Dispose");

Explorer/Assets/DCL/AvatarRendering/Emotes/Systems/Play/CharacterEmoteSystem.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ private void UpdateRemoteMaskedEmoteTags(ref CharacterMaskedEmoteComponent maske
265265

266266
// This query takes care of consuming the CharacterEmoteIntent to trigger an emote
267267
[Query]
268-
[None(typeof(DeleteEntityIntention))]
268+
[None(typeof(DeleteEntityIntention), typeof(PlayerTeleportIntent.JustTeleported))]
269269
private void ConsumeEmoteIntent([Data] float dt, Entity entity,
270270
ref CharacterEmoteComponent emoteComponent,
271271
ref CharacterEmoteIntent emoteIntent,

0 commit comments

Comments
 (0)