feat: PointerEvents max_distance is player distance; add max_camera_distance - #9902
feat: PointerEvents max_distance is player distance; add max_camera_distance#9902robtfm wants to merge 2 commits into
Conversation
…stance Follow decentraland/protocol#470: max_distance (and its deprecated alias max_player_distance, larger wins) is the player-distance threshold, and the new max_camera_distance is the camera-distance one. Combination: only player, only camera, both (OR), neither (player <= 10). PrepareDefaultValues no longer writes MaxDistance/MaxPlayerDistance onto the live message: the generated setters flip the Has* presence bits, so every entry looked like it had both fields and only the OR branch ever ran (#9320). The fallback lives in IsQualifiedByDistance instead. Scene-entity raycast results now carry the ray length as the camera distance in every camera mode; the global-entity path is unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
🚦 CI StatusBuild skipped — no changes detected under No C# files changed — lint ratchet skipped.
|
|
Slack notification sent to #explorer-ext-contributions for external review. |
decentraland-bot
left a comment
There was a problem hiding this comment.
PR Review: fix: PointerEvents max_distance is player distance; add max_camera_distance
STEP 1 — Context & Scope
Files in diff:
InteractionInputUtils.cs— core distance-qualification rewrite +PrepareDefaultValuescleanupInteractionInputUtilsShould.cs— new unit tests for the 4-way distance rulesPlayerOriginatedRaycastSystem.cs— scene-entitySetupHitnow passeshitInfo.distance(ray length)PointerEvents.gen.cs— regenerated protobuf withMaxCameraDistance(field 8)
Surrounding context read (not in diff):
PlayerOriginatedProximitySystem.cs— proximity overlap +GetMaxDistanceAndHighestPriority(line 217)ProcessPointerEventsSystem.cs— main consumer callingPrepareDefaultValues+ bothIsQualifiedByDistanceoverloads (lines 306-310)HoverFeedbackUtils.cs— callsIsQualifiedByDistancefor hover-leave events (line 30)PlayerOriginRaycastResultForSceneEntities.cs— struct withfloat? DistanceToPlayerandfloat GetDistance()ProximityResultForSceneEntities.cs— class with non-nullablefloat DistanceToPlayerPlayerInteractionEntity.cs—PlayerPositionnullable whenCharacterControllerabsent
Docs: CLAUDE.md, docs/README.md, review-instructions prompt.
STEP 2 — Root-Cause Check
Problem: The protocol (decentraland/protocol#470) redefines max_distance as player distance (matching what the explorer has effectively measured since 2024), deprecates max_player_distance as an alias, and adds a new max_camera_distance for the camera-origin check. The old code also had a bug (#9320): PrepareDefaultValues wrote MaxDistance = 10 / MaxPlayerDistance = 0, flipping Has* presence bits, so every entry appeared to have both fields and only the OR branch of IsQualifiedByDistance ever ran.
Does the diff fix the cause? Yes for the cursor/raycast path — the rewrite correctly resolves player and camera thresholds from field presence and moves the default out of PrepareDefaultValues into the (null, null) fallback. However, the proximity interaction path (PlayerOriginatedProximitySystem + proximity overload of IsQualifiedByDistance) still reads only MaxPlayerDistance, which is now the deprecated field. This is an incomplete application of the semantic change — see P1 below.
STEP 2 verdict: PARTIAL — cursor path fixed, proximity path not updated.
STEP 3 — Design & Integration
No new long-lived units are introduced. The change is a semantic rewrite within existing static helpers and an existing system. Design is sound for the cursor path.
Teardown/consumption trace: No new subscriptions, events, or resources are opened. The protobuf field MaxCameraDistance is purely a data read. ✅
STEP 4 — Member Audit
DEFAULT_MAX_DISTANCE(new public const, line 13): Used by the(null, null)fallback inIsQualifiedByDistance(1 consumer). Appropriate — documents the protocol default and avoids a magic number.
STEP 5 — Line-Level Findings
P1 — Proximity system not updated for new max_distance semantics
Locations (not in diff):
PlayerOriginatedProximitySystem.csline 233 —GetMaxDistanceAndHighestPriority()InteractionInputUtils.cslines 62-67 — proximity overload ofIsQualifiedByDistance
Problem: This PR redefines max_distance as the canonical player-distance field and deprecates max_player_distance as an alias. The cursor overload correctly resolves both fields (taking the max when both present). The proximity code path has two gaps:
-
GetMaxDistanceAndHighestPriority(line 233) readsinfo.MaxPlayerDistancedirectly (deprecated field only, noHascheck). When a scene uses onlymax_distance: 5for proximity events,MaxPlayerDistancereturns 0 (proto default),sqrMaxPlayerDistance = 0, and every entity is filtered out → proximity silently broken. -
Proximity
IsQualifiedByDistanceoverload (line 66) only checksHasMaxPlayerDistance. Three broken cases:- Scene sets only
max_distance:HasMaxPlayerDistanceis false → returnstrueunconditionally (no distance limit applied) - Scene sets both: only deprecated value used, "larger wins" reconciliation skipped
- Scene sets neither: returns
trueunconditionally — contradicts the cursor overload'sDEFAULT_MAX_DISTANCE = 10ffallback (unlimited range for proximity while cursor has 10-unit cap)
- Scene sets only
Once the SDK migrates scenes from deprecated max_player_distance to canonical max_distance, proximity interactions will silently break.
Fix for GetMaxDistanceAndHighestPriority (line 233):
// Replace:
float maxDistance = info.MaxPlayerDistance;
// With:
float maxDistance = (info.HasMaxDistance, info.HasMaxPlayerDistance) switch
{
(true, true) => Mathf.Max(info.MaxDistance, info.MaxPlayerDistance),
(true, false) => info.MaxDistance,
(false, true) => info.MaxPlayerDistance,
(false, false) => PROXIMITY_DEFAULT_MAX_DISTANCE,
};Fix for proximity IsQualifiedByDistance (lines 62-67):
public static bool IsQualifiedByDistance(
in ProximityResultForSceneEntities proximityResultForSceneEntities,
PBPointerEvents.Types.Info info
)
{
float? maxPlayerDistance = (info.HasMaxDistance, info.HasMaxPlayerDistance) switch
{
(true, true) => Mathf.Max(info.MaxDistance, info.MaxPlayerDistance),
(true, false) => info.MaxDistance,
(false, true) => info.MaxPlayerDistance,
_ => null,
};
float effectiveMax = maxPlayerDistance ?? DEFAULT_MAX_DISTANCE;
return proximityResultForSceneEntities.DistanceToPlayer <= effectiveMax;
}Consider extracting the alias-resolution switch into a shared static float? ResolveMaxPlayerDistance(PBPointerEvents.Types.Info info) helper to avoid duplicating it across three call sites.
P2 — Missing reverse-alias test case
(See inline suggestion on test file)
P2 — Branch/title convention mismatch (ADR-6)
Branch: feat/max-camera-distance, PR title: fix:. The branch prefix should match the commit type. Minor — not blocking.
Security Review
No security issues found.
- NaN distance values:
<=comparisons returnfalseper IEEE 754 (rejects interaction — safe) - Infinity: bounded by
MAX_RAYCAST_DISTANCE = 100fraycast cap - Negatives: distances are non-negative, so
<= negativeisfalse - No hardcoded secrets, no sensitive data in logs
STEP 6 — Complexity
COMPLEX — modifies pointer event distance semantics, interaction qualification logic, and raycast system input handling.
STEP 7 — QA Assessment
QA required — changes affect runtime interaction behavior (distance checks for pointer events governing hover, click, and proximity).
STEP 8 — Non-blocking warnings
None. Main.unity not modified.
REVIEW_RESULT: FAIL ❌
COMPLEXITY: COMPLEX
COMPLEXITY_REASON: Modifies pointer-event distance semantics and interaction qualification across raycast and proximity systems
QA_REQUIRED: YES
Reviewed by Jarvis 🤖 · Requested by unknown (<@unknown>) via Slack
| Assert.IsTrue(InteractionInputUtils.IsQualifiedByDistance(result, new PBPointerEvents.Types.Info { MaxPlayerDistance = 6 })); | ||
| Assert.IsFalse(InteractionInputUtils.IsQualifiedByDistance(result, new PBPointerEvents.Types.Info { MaxPlayerDistance = 4 })); | ||
| Assert.IsTrue(InteractionInputUtils.IsQualifiedByDistance(result, new PBPointerEvents.Types.Info { MaxDistance = 4, MaxPlayerDistance = 6 })); | ||
|
|
There was a problem hiding this comment.
[P2] Missing reverse-alias test case. The test verifies MaxDistance=4, MaxPlayerDistance=6 (alias wins), but not the reverse direction where the canonical field wins. Add a case to confirm Mathf.Max works both ways.
| Assert.IsTrue(InteractionInputUtils.IsQualifiedByDistance(result, new PBPointerEvents.Types.Info { MaxDistance = 4, MaxPlayerDistance = 6 })); | |
| Assert.IsTrue(InteractionInputUtils.IsQualifiedByDistance(result, new PBPointerEvents.Types.Info { MaxDistance = 6, MaxPlayerDistance = 4 })); |
Review follow-up: the proximity broad-phase and its IsQualifiedByDistance overload still read only the deprecated max_player_distance. Resolve the threshold through a shared ResolveMaxPlayerDistance (max_distance, alias, larger wins; null when neither) so cursor and proximity agree; proximity falls back to its 3 m default in the broad-phase and to the 10 m default in the qualifier. Adds the reverse-alias and proximity test cases. Also clears four RedundantArgumentDefaultValue warnings elsewhere to satisfy the lint ratchet (no warnings in the files this PR touches). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Addressed the review:
|
|
PR #9902, run #33197110408 Overall: ✅ no significant changes Builds: Windows change, Windows baseline, macOS change, macOS baseline How to read this table
Intel Core i5
Apple M1
|
Pull Request Description
What does this PR change?
Implements decentraland/protocol#470 for
PBPointerEvents.Info:max_distanceis the player-distance threshold (this is what the explorer has effectively measured since 2024 — avatar head in every mode except first-person — and what deployed scenes rely on; the protocol comment now says so instead of "camera distance").max_player_distanceis a deprecated alias formax_distance; when both are set the larger wins.max_camera_distance: the camera-origin check (ray length), for scenes driving a virtual camera away from the avatar.Fixes #9320:
PrepareDefaultValueswroteMaxDistance = 10/MaxPlayerDistance = 0onto the live message, and the generated setters flip theHas*presence bits, so every entry looked like it had both fields and only the OR branch ofIsQualifiedByDistanceever ran (the "onlymax_player_distance" branch was dead code). The fallback now lives inIsQualifiedByDistance.Changes:
InteractionInputUtils.IsQualifiedByDistance(cursor overload): resolves the player and camera thresholds from field presence, then the 4-way rule. Proximity overload untouched.InteractionInputUtils.PrepareDefaultValues: no longer touches the distance fields.PlayerOriginatedRaycastSystem: scene-entitySetupHitgetshitInfo.distanceas the camera distance in every camera mode (previously mode-dependent). Global-entity path unchanged.PointerEvents.gen.csregenerated from the Feat: worlds compatibility chat commands #470 protocol (scripts/npm run build-protocol);scripts/package.jsonstill pins the older npm release — bump it once Feat: worlds compatibility chat commands #470 is published.InteractionInputUtilsShould.QualifyByDistancecovers the four rules and the alias.Behaviour deltas to be aware of: first-person
maxDistancenow measures from the avatar root to the hit point rather than along the ray from the near plane; scenes setting onlymaxPlayerDistanceno longer get an implicit|| rayDistance <= 10.Test scene:
pointer-camera-distance-scene(cubes withmaxDistance: 2,maxPlayerDistance: 2,maxCameraDistance: 5, keyed virtual cameras) — same scene used to verify the bevy side in decentraland/bevy-explorer#1158. SDK side: decentraland/js-sdk-toolchain#1560.🤖 Generated with Claude Code