Skip to content

release: 2026-07-20#9441

Merged
anicalbano merged 16 commits into
mainfrom
release/2026-07-20
Jul 21, 2026
Merged

release: 2026-07-20#9441
anicalbano merged 16 commits into
mainfrom
release/2026-07-20

Conversation

@github-actions

Copy link
Copy Markdown
Contributor

🚀 Automated release PR

popuz and others added 16 commits July 14, 2026 12:40
# Pull Request Description

## What does this PR change?

Fixes SDK **continuous external forces** (`PBPhysicsCombinedForce` — wind tunnels, wind zones) dying the moment any **Portable Experience** is loaded (a global PX or a worn smart wearable), plus a second bug of the same class: unloading any PX/scene killed an active force owned by another scene.

**Root cause.** `SDKExternalPhysicsSystems` runs in every scene world. Each instance whose scene is "current" zeroed the shared global `CharacterRigidTransform.ExternalForce` every frame and re-asserted it only from its own world's components. A Portable Experience world is *permanently* current (`PortableExperienceSceneFacade` sets `IsCurrent = true` forever), so the PX instance erased the parcel scene's continuous force every frame and never re-asserted anything (a PX world has no force components of its own). Impulses (jump pads) survived because they accumulate via `+=`.

**Fix.** The shared-field write is replaced with **per-scene-world contribution slots** — `CharacterRigidTransform.ExternalForceContributions` (`Dictionary<World, Vector3>`):
- each scene world writes/removes **only its own slot**; no world can erase another world's force anymore;
- the effective `ExternalForce` is derived every tick in `ApplyExternalForce.Execute` as the **vector sum** of all slots — forces from several current worlds stack (scene wind + PX jetpack is intended physics);
- a world unloading (`FinalizeComponents`) or becoming non-current removes only its own contribution;
- `/reload` clears the whole map;
- the **impulse pipeline is untouched**.

Covered by new red-first EditMode regression tests (`SDKExternalPhysicsSystemsShould`, 7 tests — the main repro test was verified failing on the old code) + adapted `ApplyExternalForceShould`.

## Test Instructions

### ⚠️ Read first: reproduce the bug BEFORE validating the fix

The bug only manifests while a Portable Experience world is running — with no PX loaded every case below trivially "passes" and validates nothing. So the QA pass has two phases:

1. **Phase 1 — repro on a build WITHOUT this fix** (any other recent PR / dev build): confirm a PX is actually running, then confirm the force dies.
2. **Phase 2 — the same steps on THIS PR build**: confirm the force keeps working.

**How PX loading works in builds:** The only lever is the environment: the PX list comes from the `alfa-global-portable-experiences` feature flag of the current environment, so launch with `--dclenv org` (production) to get the global PX list (feature flags are environment-specific).

### Prerequisites
- [ ] A Portable Experience running: the startup-loaded global PX (launch with `--dclenv org`), a **worn smart wearable** (ideal — a PX world runs on you the whole session), or `/loadpx <name>` / `/killpx <name>` chat commands.
- [ ] Test scene with a **continuous external force** (wind tunnel) and a **jump pad** (impulse): launch with `--realm flutterecho.dcl.eth`, or `/goto flutterecho.dcl.eth` from chat.

**Steps (standard run)** — verified launch line:
```bash
metaforge explorer run 9343
# or launch the downloaded build directly:
Decentraland.exe --realm flutterecho.dcl.eth --dclenv org
```
- `--realm flutterecho.dcl.eth` — loads the test world directly (no `/goto` needed)
- `--dclenv org` — production environment, whose `alfa-global-portable-experiences` flag carries the global PX list

### Test Steps

**Case 0 — repro on an OLD build (phase 1, mandatory):**
1. On any build without this fix (launched with the same args), enter the wind tunnel — the character is pushed.
2. Load any PX (`/loadpx <name>`) or equip a smart wearable.
3. Confirm the push **dies** the moment the PX starts. This is the bug. Now switch to this PR build for all cases below.

**Case 1 — continuous force survives a loaded PX (the main fix):**
1. Enter the wind tunnel, confirm the push works.
2. While inside, load a PX (`/loadpx`) — and separately, arrive already wearing a smart wearable.
3. Expected: the push keeps working, no interruption or stutter in either variant.

**Case 2 — unloading a PX must not kill a foreign force:**
1. Stand in the wind tunnel with a PX loaded (force working).
2. `/killpx <name>` (or unequip the smart wearable) while standing in the airflow.
3. Expected: the force continues uninterrupted.

**Case 3 — jump pads (impulses) unchanged:**
1. Use a jump pad with no PX loaded, then again with a PX loaded and while wearing a smart wearable.
2. Expected: behavior identical to production — same launch strength, no double or missing impulse.

**Case 4 — forces from several worlds stack:**
1. Stand in the wind tunnel and activate a PX that applies its own force (e.g. a jetpack smart wearable), if one is available.
2. Expected: the forces combine (vector sum), one does not replace the other; killing the PX removes only its own share, the tunnel keeps pushing.

**Case 5 — leaving the scene stops its force:**
1. With a PX still loaded, walk out of the force-emitting parcel to a neighboring parcel.
2. Expected: the scene's push stops on leaving; re-entering restores it.

**Case 6 — `/reload`:**
1. Stand in the wind tunnel (force active, PX loaded).
2. Type `/reload`.
3. Expected: the force stops instantly with the reload; after the scene reloads, re-entering the tunnel applies the force again.

### Additional Testing Notes
- Run the cases with the PX present in **both** forms: a global PX loaded via `/loadpx` and a **worn smart wearable** — the bug is the same but the world lifecycle differs.
- Regression watch: glider response to wind (#9317) must behave as on dev — open the glider inside the tunnel airflow.
- When reproducing on the old build the force dies the *same frame* the PX world starts — it is very visible, no need to wait.
* fix: stop Enter from focusing chat behind stacked popups

Pressing Enter while typing in a view opened from a context menu (e.g. the
community card's announcement field, opened via the chat "View Community"
menu) focused the chat behind it instead of typing.

The chat toggled its UI.Submit handler on every MVC view show/close. When a
popup opens another popup (the context menu closes as the card opens), the
menu's deferred OnViewClosed fired after the card's OnViewShowed and
re-subscribed the handler while the card was still on top.

Replace the fragile show/close subscription toggling with a single query at
Enter time: skip focusing the chat when any non-persistent view is showing.
This is order-independent and also covers the other context-menu-opened
inputs (Passport, Friend Request, Gift Selection).

Adds IMVCManager.IsAnyNonPersistentViewShowing().

* chore: reduce warnings and simplify comments.

* fix: focus chat on enter was lost
* chore: add check-team-dev composite action

Shared by the Jarvis review request/status workflows to resolve whether
a PR author is a team dev from auto_assign_config_dev.yml.

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

* chore: add Jarvis review request workflow

Auto-requests a decentraland-bot review on fix/chore PRs from team devs
(agent-server webhook picks it up), keeps the ext-contribution Slack
notify flow, and posts a deprecation notice on @claude comments.

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

* chore: add Jarvis review status workflow

Translates decentraland-bot's submitted review (REVIEW_RESULT /
COMPLEXITY / QA_REQUIRED markers) into the Claude Review commit status,
auto-approval, and claude-approved / 'no QA needed' labels — same gating
rules as the old post-review step, now event-driven. Includes a stale-
commit guard and a failure status when agent-server reports an error.

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

* chore: point require-review gate at Jarvis

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

* chore: adapt review instructions to Jarvis toolset

The reviewer now clones the repo and posts a single COMMENT review via
gh api; the verdict markers are pinned to the end of the review body so
the status workflow can parse them.

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

* chore: fix stale Grep tool reference in review instructions

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

* docs: update PR standards for the Jarvis review flow

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

* chore: remove claude-code-action review workflow

Superseded by the Jarvis review request/status workflows. The cost
report script's only consumer was this workflow; Jarvis posts its own
cost audit to Slack.

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

* chore: harden Jarvis verdict gating after review findings

Re-run step now waits for in-flight enforce-approvals runs and retries
failures of any event type (the bot's review co-triggers a run that reads
labels before they land); drops the minimizeComment call that can never
succeed on PullRequestReview nodes; catches approval/label/comment API
errors so the final status write always executes.

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

* chore: guard enforce-approvals poll against transient API failure

An empty gh run list result now counts as zero in-flight runs so the
re-run step proceeds instead of erroring under bash -e.

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

* fix: inline team-dev check, drop check-team-dev composite action

A local 'uses: ./.github/actions/check-team-dev' is resolved from the
checked-out tree, which both workflows check out at the PR base SHA (so
the team list is read from trusted base, not the PR head). The action
therefore had to already exist on the base branch and could not bootstrap
on the PR that introduces it — check-author failed with 'Can't find
action.yml'. Inlining the ~12-line check into both workflows removes the
bootstrap dependency entirely; the config file it reads already exists on
dev. A comment in each workflow records why it is not a shared action.

* fix: address Jarvis review findings on the migration PR

- Stale-verdict recovery: when a verdict arrives for an older commit than
  the PR head, re-request decentraland-bot for the current HEAD instead of
  leaving the Claude Review check unresolved (pushes that land during an
  active review are debounced by the request workflow, so nothing else
  re-requests).
- Fetch reviews/comments only on the auto-review path; feature and
  external verdicts no longer spend the paginated API calls they never
  read.
- Docs: a re-request triggers a fresh full review; the previous wording
  implied re-review semantics the webhook does not carry.
- Mark the @claude deprecation-notice job with a removal TODO.

* feat: auto-request Jarvis review for all team-dev PRs regardless of type

The fix/chore title check no longer gates the review request — every
non-draft PR from a team dev is auto-requested on open and on push. The
title type still bounds what the verdict may do in jarvis-review-status:
auto-approve and the QA waiver remain fix/chore-only, so feature PRs get
a required status check but always need human DEV review. Docs updated,
including the external-contributor section (externals cannot request
reviewers themselves; a maintainer does it as part of routing).

* fix: set pending status on debounced pushes too

A push arriving while the bot's review is in flight is debounced (no
re-request), but the new SHA previously got no Claude Review status and
showed 'Expected — waiting for status'. The pending status is now set
whenever a review genuinely covers the push (fresh request or debounce);
only a failed request leaves the check unset, since a 'requested' status
there would mask the failure.

* fix: only strip 'no QA needed' when the verdict requires QA

COMPLEXITY answers whether a DEV must review; QA_REQUIRED answers whether
QA must test runtime behavior. The old reconciliation removed a manually
applied 'no QA needed' whenever the PR wasn't SIMPLE, so a COMPLEX
CI-only change with QA_REQUIRED: NO had its waiver stripped on every
verdict. Adding the waiver stays SIMPLE-only (conservative); removal now
happens only when the verdict reports QA_REQUIRED: YES.

* feat: auto-request only the initial review; re-reviews are on demand

The initial Jarvis review is still requested automatically when a
team-dev PR opens or is marked ready. A push after a verdict no longer
re-requests: the Claude Review check resets to a descriptive pending
('Awaiting Jarvis review — request a review from decentraland-bot') and
the dev re-requests via the Reviewers panel when the PR is ready for a
fresh pass. Pushes during an in-flight review remain covered by it, and
the stale-verdict recovery in the status workflow keeps auto-completing
explicitly requested reviews.

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: davidejensen <davidejensen@live.it>
Co-authored-by: Ani <139157886+anicalbano@users.noreply.github.com>
* feat: unify asset delivery and make it configurable

Behind the optimized-assets feature flag, asset delivery resolves through
a single origin instead of the separate ab-cdn, lod-generator, and
asset-bundle-registry hosts. The assets-base-url flag variant repoints
that origin, and the --optimized-assets-url launch argument forces the
feature on for per-machine local development. With the flag off, URL
resolution is unchanged.

* chore: clear actionable InspectCode warnings in the DCL.Network module

Separate from the feature change above. Clears the fixable ReSharper warnings
in and around the files this PR touches, with zero behavior change:

- 16 redundant using directives
- 4 redundant single-statement braces
- 1 redundant null-forgiving operator (string.IsNullOrEmpty already guards)
- 4 object-initializer conversions
- 2 readonly fields, 1 readonly struct, 2 var-for-builtins
- 1 for->foreach, 1 expression-bodied method
- 1 unused private const
- CheckNamespace suppressed per-file on the 16 files this PR modifies, via
  `// ReSharper disable once CheckNamespace`. These namespaces intentionally
  don't mirror the Unity Assets/ folder layout (the repo-wide convention that
  accounts for ~4,461 of the same hit); the directive scopes the opt-out to
  exactly our files rather than changing shared inspection policy.

The remaining warnings in this module are structural and intentionally left
untouched: JSON-DTO field names (which must match the wire format and can't be
renamed without breaking deserialization) and CS8618 on reflection-populated
DTO fields. CommsHandshakeMetadata's [Serializable] fields are also left alone —
they're consumed by JsonUtility, so their "unused" flag is a false positive.

* chore: clear actionable InspectCode warnings in the DCL.Network module

Separate from the feature change above. Clears the fixable ReSharper warnings
in and around the files this PR touches, with zero behavior change:

- 16 redundant using directives
- 4 redundant single-statement braces
- 1 redundant null-forgiving operator (string.IsNullOrEmpty already guards)
- 4 object-initializer conversions
- 2 readonly fields, 1 readonly struct, 2 var-for-builtins
- 1 for->foreach, 1 expression-bodied method
- 1 unused private const
- CheckNamespace suppressed per-file on the 16 files this PR modifies, via
  `// ReSharper disable once CheckNamespace`. These namespaces intentionally
  don't mirror the Unity Assets/ folder layout (the repo-wide convention that
  accounts for ~4,461 of the same hit); the directive scopes the opt-out to
  exactly our files rather than changing shared inspection policy.

The remaining warnings in this module are structural and intentionally left
untouched: JSON-DTO field names (which must match the wire format and can't be
renamed without breaking deserialization) and CS8618 on reflection-populated
DTO fields. CommsHandshakeMetadata's [Serializable] fields are also left alone —
they're consumed by JsonUtility, so their "unused" flag is a false positive.

* chore: document serialized JSON DTO wire-format exceptions in wiki docs

Mirror the CLAUDE.md / code-standards skill rules in docs/code-style-guidelines.md:
keep wire-format field casing (per-file ReSharper suppression), null!/default! on
schema-required fields with a schema-link comment, T? on optional ones, and guards
following the declaration. Cross-reference from docs/standards.md serialization section.

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

* chore: clear EntityDefinition InspectCode warnings and codify DTO wire-format rules

Clears all 18 remaining InspectCode findings in EntityDefinitionBase.cs and
EntityDefinitionGeneric.cs, and codifies the previously unwritten rules that
determined the fixes.

Code standards (CLAUDE.md + code-standards skill):
- New rule: serialized JSON DTO fields keep wire-format casing; suppress
  InconsistentNaming per file instead of renaming. [JsonProperty] only rescues
  the Newtonsoft path - DTOs also parsed via Unity's JsonUtility
  (SceneEntityDefinition through WRJsonParser.Unity) have no rename mechanism.
- DTO nullability rule extended: `= default!` for generic-parameter fields;
  schema-optional fields kept non-nullable only when every consumed entity
  type guarantees them in practice (stated in the schema-link comment); fields
  absent from the linked schema default to optional; required fields must not
  be null-guarded at all (?., ??, == null).
- Fixed the skill contradicting CLAUDE.md ("never use ! outside tests" vs the
  DTO null! exception).

EntityDefinition DTOs (schema: decentraland/common-schemas entity.ts#/Entity):
- Required fields (type, version, pointers, content) initialized `= null!`
  with schema-link comment; dead ?./??/as-cast guards on them removed.
- thumbnail declared `string?` (absent from Entity schema; GiftItemLoaderService
  already treats it as maybe-absent).
- metadata initialized `= default!` (schema-optional, guaranteed in practice).
- Equals takes a nullable parameter (IEquatable contract) and uses null-safe
  string.Equals, fixing a latent NRE when id is null.
- Per-file InconsistentNaming suppression; no serialized field renamed.

Consumer fallout from the honest `string?` on thumbnail - two latent NRE paths
exposed and fixed: ITrimmedAvatarAttachment.GetThumbnail falls back to
string.Empty, LoadTrimmedElementsByIntentionSystem skips SanitizeEntityHash
for null thumbnails.

Also ignores the local ReSharper CLI layout (rsharp/, rsharp.zip) used by
scripts/lint.

Verified with the CI lint scripts (scoped InspectCode + filter-warnings.sh):
the two DTO files report zero findings; the touched consumer files match
their pre-existing baseline exactly.

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

* chore: deduplicate serialized JSON DTO rules to single source

Full wire-format DTO rules now live only in docs/code-style-guidelines.md;
CLAUDE.md and the code-standards skill keep condensed summaries linking to it.

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

* chore: document caching semantics flagged in review

---------

Co-authored-by: Mikhail Agapov <mikhail.agapov@decentraland.org>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Juan Molteni <juanignaciomolteni@gmail.com>
The performance_report hiccup metrics recomputed over a rolling 1024-frame
ProfilerRecorder buffer that was never reset, so hiccups were double-counted
across reports and survived teleports/unfocus. The 50ms bar also ignored the
target frame rate, and the Capacity-based guard left dead code.

- Reset the hiccup recorders after each report and on collection stop, so each
  report covers only its own interval (recorders are recreated since
  ProfilerRecorder has no flush).
- Make the threshold target-relative: max(50ms, 2x target frame time), so
  capped configs are not judged by the same bar as uncapped ones.
- Add hiccups_excess_time, hiccups_samples_amount and hiccups_threshold fields
  (plus gpu_ twins) for normalization and cross-cohort comparison.
- Fix the Capacity vs valid-sample-count guard and replace the return tuple
  with a HiccupStats struct.
- Document the performance_report event in docs/performance-analytics.md.
* feat: allow buy from client with new credits shop

* Update Explorer/Assets/DCL/MarketplaceCredits/Purchase/Relay/CreditsManagerMetaTxRelayer.cs

Co-authored-by: Muna <44584806+decentraland-bot@users.noreply.github.com>
Signed-off-by: davidejensen <davidejensen@live.it>

* Update Explorer/Assets/DCL/MarketplaceCredits/Purchase/Relay/PolygonSettlementPoller.cs

Co-authored-by: Muna <44584806+decentraland-bot@users.noreply.github.com>
Signed-off-by: davidejensen <davidejensen@live.it>

* minor improvement

* test fixes

* Update Explorer/Assets/DCL/MarketplaceCredits/Purchase/CreditsPurchaseService.cs

Co-authored-by: Muna <44584806+decentraland-bot@users.noreply.github.com>
Signed-off-by: davidejensen <davidejensen@live.it>

* Update Explorer/Assets/DCL/MarketplaceCredits/Purchase/CreditsPurchaseService.cs

Co-authored-by: Muna <44584806+decentraland-bot@users.noreply.github.com>
Signed-off-by: davidejensen <davidejensen@live.it>

* Update Explorer/Assets/DCL/MarketplaceCredits/Purchase/Api/ShopListingDto.cs

Co-authored-by: Muna <44584806+decentraland-bot@users.noreply.github.com>
Signed-off-by: davidejensen <davidejensen@live.it>

* Update Explorer/Assets/DCL/MarketplaceCredits/Purchase/Api/TradeDto.cs

Co-authored-by: Muna <44584806+decentraland-bot@users.noreply.github.com>
Signed-off-by: davidejensen <davidejensen@live.it>

* Update Explorer/Assets/DCL/MarketplaceCredits/Purchase/CreditsPurchaseService.cs

Co-authored-by: Muna <44584806+decentraland-bot@users.noreply.github.com>
Signed-off-by: davidejensen <davidejensen@live.it>

* Update Explorer/Assets/DCL/MarketplaceCredits/Purchase/UI/CreditPurchaseModalController.cs

Co-authored-by: Muna <44584806+decentraland-bot@users.noreply.github.com>
Signed-off-by: davidejensen <davidejensen@live.it>

---------

Signed-off-by: davidejensen <davidejensen@live.it>
Co-authored-by: Muna <44584806+decentraland-bot@users.noreply.github.com>
* fix: null-safe budget release in LoadAssetBundleSystem

The pooled StreamableLoadingState can be recycled (budget disposed and
nulled via the pool actionOnRelease) if the promise is forgotten or
cancelled while the detached flow is mid-await. Releasing the budget with
!. would NRE in that window, so guard with ?. to match how the budget is
treated everywhere else in the flow.

* fix: honor cancellation token in ArtificialDelayWebRequestController

Pass envelope.Ct to UniTask.Delay so the artificial delay is cancelled
along with the request instead of running to completion after the request
is already gone.
* feat: add deep link param to force open backpack on client open

* fixed credits modal overlap and added apparg to docs

* code improvement
@github-actions

Copy link
Copy Markdown
Contributor Author

@DafGreco DafGreco left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✔️ PR reviewed and approved by QA on both platforms following instructions playing both happy and un-happy path

Regressions for this ticket had been performed in order to verify that the normal flow is working as expected:

Checks done on both plaforms

@anicalbano
anicalbano self-requested a review July 21, 2026 16:46
@anicalbano
anicalbano added this pull request to the merge queue Jul 21, 2026
Merged via the queue into main with commit 668704d Jul 21, 2026
596 of 613 checks passed
@anicalbano
anicalbano deleted the release/2026-07-20 branch July 21, 2026 17:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.