feat: Pulse transport (ENet avatar-state relay) + Multiplayer Debug panel - #2541
Conversation
- Vendor decentraland.pulse protos + common/options.proto in the git-tracked lib/proto_overlay/ (a second protoc include root) — the npm-managed proto tree is gitignored and wiped by 'cargo run -- install', and the pinned @dcl/protocol (commit-a598406) doesn't ship them yet (protocol PR #429). Source: protocol@45edead4 (feat/pulse-prd), pinned in file headers. - Patch rfc4 PlayerEmote.is_stopping at build time into an OUT_DIR copy (upstream field the npm pin predates; Unity peers already send it) and compile that copy; no-op once the pin catches up. - Port bevy-explorer build_quant.rs: generate {field}_dequantized/_quantized/ _step accessors from the quantized/quantized_power field options via the FileDescriptorSet (prost-reflect 0.11, same pairing bevy uses). - Fix pre-existing --no-default-features build breakage: scene-access/ gatekeeper code used livekit-gated imports (Arc, TokioRuntime, wallet, gatekeeper_url*) — un-gate them. - Unit tests: linear + power-law grids (sign/exact-zero), optional delta accessors, envelope prost round-trip.
Port of bevy-explorer's pulse transport (PR #919 @ 3f65c164), ENet only: - transport.rs: byte-boundary seam (frames, status, server disconnect codes) - native.rs: 'pulse-enet' driver thread — rusty_enet (robtfm fork, ENet-CSharp modified protocol; stock/Godot ENet is wire-incompatible), pinned by rev - decoder.rs: quantized state -> rfc4::Movement reconstruction, parcel grid, seq-gap resync; full bevy test-vector port - pulse_room.rs: connection state machine (Down->Connecting->Idle->Signing-> AwaitingResponse->Established/Dead, 2s cooldown, 5s handshake timeout, pipe-close-authoritative), MessageProcessor bridging with room_id 'pulse', synthetic PeerLeft flood on teardown, outbound API (movement/emote/teleport/ profile-version) with grid-bounds guard - sign_pulse_connect: signed-fetch-shaped connect payload, x-identity dict as protobuf bytes (verbatim, no lowercasing) - CommunicationManager: pulse_room parallel to scene_room, lifecycle via change_adapter/clean, 5-strike session fallback (Unity parity), deferred teleport until valid realm name + cached position - Activation (opt-in): --pulse / --pulse-server / PULSE_SERVER env; urls::pulse_server() org/zone endpoint; use_pulse feature (default on, runtime off) - --no-livekit-movement CLI + dual-channel flag plumbing (wired in M3) 24 unit tests (quant grids, decoder, state machine).
- broadcast_movement: build the uncompressed rfc4 Movement unconditionally (it feeds the Pulse PlayerStateInput), tee to Pulse when established (10Hz/1Hz cadence inherited from broadcast_position.gd, under the 20Hz cap) - livekit_movement_dual_channel (default ON): OFF skips only the island movement sends (main room + archipelago) and only while Pulse is established — LiveKit auto-resumes on Pulse drop, scene-room movement and the archipelago heartbeat are never gated - send_emote/set_emoting -> Pulse EmoteStart/EmoteStop (reliable) - broadcast_profile_version -> Pulse announce (on-change only, deduped in PulseRoom; the 10s LiveKit rebroadcast loop is not teed) - notify_player_teleported #[func] latched via pulse_teleport_pending (coalesces to <=1 teleport/frame vs the server's 20/s discrete-event budget) + explorer.gd move_to hook (scene movePlayerTo funnels through it per handle_restricted_actions.rs:210)
…op + teleport snap (M2/M4) - MessageProcessor: per-peer pulse_live flag; while set, LiveKit Position/Movement/MovementCompressed/PlayerEmote for that peer are discarded (LiveKit sender-clock vs Pulse server-tick timestamps are incomparable — merging starves one source permanently). Both dedup layers reset on every flip; pulse PeerLeft (incl. PulseRoom's teardown flood) hands the peer back to LiveKit within a frame - 'pulse' room exempt from the 5s inactivity sweep: the server stops sending deltas for static distant peers by design; membership (reliable Joined/Left) is the liveness signal - rfc4 PlayerEmote.is_stopping handled before the incremental-id dedup -> AvatarScene::stop_emote -> avatar.gd stop_emote_from_network (also fixes the latent gap for Unity LiveKit peers, which already send it) - Movement.is_instant -> DclAvatar::snap_to_position (teleports snap instead of lerping across the jump) - get_peer_room_info: Pulse/…+Pulse room labels, '*' = pulse-driving - unit test pins the gate's message classification
- Deeplink params (router + desktop fake-deeplink path): decentraland://open?pulse-server=<host:port> joins a specific Pulse server — shareable, implies enabling, clears the 5-strike session fallback, rebuilds the room immediately; pulse=true|false toggles with the configured endpoint. - CommunicationManager::set_pulse_server/set_pulse_enabled runtime #[func]s; room creation defers until an identity exists (a deeplink can land before login — was a 'wallet not initialized' panic). - ServiceGroup::Pulse: dclenv=pulse::zone,org points ONLY Pulse at the zone deployment (dclenv=zone flips it along with everything else); urls::pulse_server() resolves through it, Today -> .zone. Live-verified: fake-deeplink run with no --pulse flag connected, authenticated and teleported on the local server (peers_connected +1, teleport +1 in server metrics).
Rename the LiveKit debug panel to "Multiplayer Debug" everywhere and refactor it into a full comms-state overlay: - Deeplink param is now multiplayer_debug=true (livekit_debug kept as a working legacy alias); settings toggle renamed to "Multiplayer Debug" and now syncs with the actual panel state on menu open. - LiveKit rooms track a real connection state (connecting/connected/ reconnecting/disconnected) via a shared atomic written by the room event loop; Reconnecting/Reconnected events were previously swallowed. - New state getters: WebSocket room handshake state, Archipelago state + island id + island room state, Pulse state machine name + endpoint; Adapter trait gains a connection_state_str() default. - get_debug_room_info() extended (key-stable across feature combos): connection_state, main_room_type/state, archipelago_state, island_id, island_room_state, scene_room_state, pulse_available/enabled/state/ endpoint/failures/disabled_for_session, dual_channel. - Panel shows realm, adapter, main room, archipelago/island, scene room, Pulse line and a per-peer room list (get_debug_peer_rooms, capped at 10 rows); panel widened for the longer rows. - validate_all_scripts now skips embedded Godot projects (the Android build template's instrumented assets broke the sweep after a class cache regeneration). Verified live on Genesis Plaza with the local Pulse server: panel shows island id + connected states and Pulse established @ 127.0.0.1:7777.
- New shareable deeplink param dual-channel=true/false (default true): false makes movement Pulse-only while the Pulse link is established; LiveKit movement auto-resumes if Pulse drops. Wired in both the deeplink router and the desktop fake-deeplink path. - Peer room labels (avatar nametag debug + panel peer rows) now list every room explicitly, e.g. "PULSE + SCENE + ARCHIPELAGO" instead of the ambiguous "Both"/"+Pulse" scheme. The trailing '*' on PULSE still marks the source driving the avatar.
Remove the separate ServiceGroup::Pulse — Pulse is part of the comms stack, so dclenv=comms::zone,org (or plain dclenv=zone) now points the Pulse endpoint at the zone deployment together with the rest of comms. Today still resolves to zone (no .today Pulse deployment).
📦 Build Report🤖 Android
Build Status: ✅ Success 🍏 iOS
🔗 Workflow Run: View logs 🔄 Updated: 2026-08-04 13:30:29 UTC |
- rusty_enet now comes from dcl-regenesislabs/rusty_enet @ 8023f9b — the DCL-org mirror of robtfm/rusty_enet branch decentraland-pulse @ 6fa651d (verified tree-identical). - @dcl/protocol pin moved from commit-a598406 to the protocol PR #429 branch build (sdk-team-cdn, commit-45edead), which ships the decentraland/pulse protos, options.proto and rfc4 is_stopping upstream. Repoint at the released npm tarball once the PR merges. - Deleted the interim lib/proto_overlay/ vendoring and the build.rs rfc4 is_stopping patch: all protos now compile from the npm-extracted tree with a single include root. - The newer proto vintage adds Chat.forwarded_from and seven PlayerEmote fields (is_repeating, interaction_id, social emote fields, mask) — initializers updated with ..Default::default() / struct-update spreads so future additive fields don't break the build.
…eplink) New dev/testing switch that disables LiveKit entirely and leaves avatar sync to Pulse alone: - CLI --no-livekit, shareable deeplink livekit=false (livekit=true re-enables live), and Global.comms.set_livekit_enabled(bool). - While disabled, change_adapter skips the main livekit room and the archipelago manager (still creating the shared MessageProcessor for Pulse/avatars), scene rooms are skipped, and a racing async gatekeeper connect result is dropped. Chat, voice and scene messages go with the rooms; ws-room realms are unaffected (not LiveKit). - Runtime disable tears the rooms down; re-enable rebuilds the current adapter and reconnects the scene room. Disabling while Pulse is not established logs a warning (no avatar sync source). - Debug panel shows an orange "LIVEKIT DISABLED (pulse-only mode)" banner (new livekit_enabled key in get_debug_room_info). Live-verified on the local Pulse server: archipelago/scene rooms skipped, zero LiveKit rooms created, Pulse handshake accepted and teleport sent, server counted the peer.
…nim latch) Three cross-client bugs seen testing Godot peers from the Unity client: - Rotation: rfc4 rotation_y was signed degrees, but the Pulse quantizer clamps to [0, 360] (Unity Quantize.Encode parity) — every negative yaw collapsed to wire 0, so half the compass faced one fixed direction on Unity (masked while moving: Unity derives facing from velocity). Wrap with rem_euclid(360) at the rfc4 source and in from_movement, mirroring the existing head-angle handling. - Emotes: Pulse EmoteStart fired at wheel-press time while EmoteStop is derived from the per-frame animation poll — any gap between press and playback (first-use async emote load, idle gate, cooldown) flapped is_emoting true->false and the stop cancelled the start server-side before it played. EmoteStart is now deferred (pending urn) and flushed by set_emoting when playback actually starts, which also attaches an idle PlayerState (Unity parks remote emote intents while the networked movement blend is > 0.1). The explicit EmoteStop is load-bearing for one-shots too: we never send duration_ms, so the server ledger only clears on an explicit stop. - "Running in place": remote locomotion flags (walk/jog/run) derive from the inter-update target distance and latch until the next packet. LiveKit streams ~10 Hz and self-corrects; Pulse goes silent for stationary peers, so a PlayerJoined bridged as a normal movement made the avatar sprint from its previous/default target and keep the run flag forever. Joins now bridge as teleport (snap), and DclAvatar decays the flags to idle after 300 ms without a movement update.
📱 Mobile Distribution Triggered🍏 iOS → TestFlight | 🤖 Android → APK ready (R2) + Slack notification 🔗 Workflow: View run 📍 Branch: 🔄 Triggered: 2026-07-31 18:45:05 UTC |
🤖 Android Build Ready✅ Status: APK available for download 📱 Download APK: direct link 🔗 Workflow: View run 📍 Branch: 🔄 Completed: 2026-07-31 19:14:53 UTC |
- Panel header gains a collapse/expand button (the only clickable element; the rest stays click-through). Body rebuilds pause while collapsed. - Adapter line elides access_token values to 16 chars + '...'. - Peer rows show the profile display name next to the short address: get_peer_room_info now returns (address, rooms, name) and get_debug_peer_rooms exposes it as a 'name' key. Names are bbcode-escaped.
Unity publishes lambdasEndpoint as ".../lambdas" (no trailing slash) in its LiveKit metadata while Godot publishes ".../lambdas/". The profile fetch URL was built by bare concatenation, producing ".../lambdasprofiles/0x..." (404) for every attempt against a Unity peer's endpoint — the full retry chain (6 retries, backoff capped at 60s) burned ~5 minutes before the asset-bundle-registry fallback rescued the profile, leaving remote avatars on the loading ghost the whole time. - build the profiles URL with the base trimmed of trailing slashes (request_lambda_profile and the default-profile path) - compare peer endpoint vs realm endpoint modulo trailing slash so the realm's own catalyst isn't mistaken for a different fetch target
The peer-advertised lambdasEndpoint is untrusted LiveKit metadata; when it hard-fails (bad URL, unreachable catalyst) the retry chain used to burn all 6 backoff attempts (~2 min) on it before any fallback ran. - validate the metadata at intake: anything that isn't a plausible http(s) URL is discarded, so the fetch sticks to the realm endpoint - on the first hard error from a peer endpoint, switch the remaining retry chain to the realm endpoint and retry immediately; Ok-but-stale responses still retry the same endpoint (catalyst propagation lag)
Pulse replays a peer's last emote announcement at join, and LiveKit emotes can race the profile fetch — either way the emote reached async_play_emote while the avatar was still the loading ghost, resolved against the default body shape and got wiped by the profile rebuild, so a peer already mid-emote appeared idle until they re-triggered it. Latch the urn when avatar_ready is false and replay it right after the avatar finishes loading (same pattern as the AvatarShape pending_expression_trigger); a network stop clears the latch so a stopped emote isn't ghost-played later.
5c3eb73 to
d4effc0
Compare
Conflict resolution notes: - @dcl/protocol pin: neither side's pin worked alone (ours had pulse protos but predated protocol PR #426 that main's UiInputBinding/TouchscreenInput components need; main's had #426 but no pulse). Repointed at the protocol PR #449 branch build (commit fdfb857) which ships both. That build tracks protocol `main`, so the `experimental`-only rfc4 fields our comms code uses (PlayerEmote.is_stopping & co., Chat.forwarded_from) are re-added by a build-time patch in lib/build.rs (patched_rfc4_comms_proto) that no-ops once upstream ships them. - billboard.rs: comment-only conflict, kept main's wording. - explorer.gd hit gdlint's 1900-line cap post-merge: panel load() folded into a const preload and an else/if fused to elif.
Flips the CLI default (deeplinks inherit it): Pulse now activates without pulse=true / --pulse. Disable per run with the new --no-pulse flag or at runtime with a pulse=false deeplink. --pulse stays accepted as a no-op for older scripts; --pulse-server keeps overriding the endpoint (and the deeplink form still force-enables + clears the session park). Dual-channel movement and LiveKit were already on by default — unchanged. The 5-strike session fallback still parks Pulse when the server never answers, so blocked-UDP environments degrade silently to LiveKit-only.
|
QA update. Mobile testing (iOS + Android) ✅ Validated:
🔴 Blocking issues found:
⏳ Not yet tested (need backend coordination or a stable build first): pulse-server unreachable fallback confirmation, dual-channel, kill-the-server, Unity cross-client rotation/emote cases. Screen_Recording_20260730_163924_Decentraland.mp4 |
…ecoder edges Consolidated fixes from two deep reviews of the Pulse transport: - Pulse resurrection: create_fallback_connection() and release_comms() now call ensure_pulse_room() — previously the archipelago kill switch (no fixedAdapter) or a pre-comms hold left Pulse permanently uncreated. - Remote kill switch: new `pulse` feature flag (disable-only, so a server flag can never override a local --no-pulse run). - FeatureFlags fetch failure downgraded printerr→push_warning: fail-open is the design, offline cold starts must not burn Sentry error quota. - Idle decay reshaped to Unity parity (RemotePlayerAnimationSystem): only ground locomotion decays on stream silence; rise/fall latch, so a mid-air peer no longer pops to a grounded pose on a >300ms packet gap. Also fixes the dead lerp clamp so the final step lands exactly on target. - Decoder: subject-slot rebind to a different wallet now emits a synthetic Left for the replaced wallet (its pulse_live gate leaked forever); ResyncRequests are deduped per subject while one is in flight. - Pulse movements skip the f32 timestamp dedup: server_tick/1000 loses ms precision past ~12 days of server uptime, silently dropping 10Hz updates (the decoder already sequence-orders them). - Driver prefers IPv4 resolution and binds a matching-family socket (NAT64/DNS64 carriers resolve AAAA first → every send failed → Pulse parked after 5 strikes). - Sentry quota: decode-failure and send-failure warns dedupe to once per connection; emote-before-movement warn → debug. - pending_pulse_emote_urn cleared on emote end and room teardown — a stale urn from a while-moving press could be announced by the next emote. - Emote stop-during-load: generation counter so an EmoteStop arriving while the GLB downloads cancels the deferred playback (looping emotes played forever). - notify_app_resumed gated to real mobile: desktop alt-tab must not arm the 30s Duplicate*-eviction grace window. - Nits: shared _apply_comms_deeplink_params helper (was duplicated in global.gd/deep_link_router.gd), BBCode-escape server-derived panel strings, drop stale plan_pulse/ doc references.
…ump-in for config-only links A target-less deeplink (e.g. ?multiplayer_debug=true with no realm/position) broke two ways: - route() matched "/open" with no target and emitted deep_link_jump, popping the jump-in side panel over Discover with no data set — an empty "scene information" card. Now the panel only opens for links with no params at all (pure navigation); config-only params were already applied in process_deep_link and route to nothing. - The panel never appeared after entering a scene from Discover: the flag's only carrier was deep_link_obj, read once in explorer._ready — fragile across the lobby → menu → explorer scene changes. The flag is now latched on the comms manager at parse time (_apply_comms_deeplink_params, enable-only) and explorer re-reads it on boot, plus re-checks on deep_link_received for links arriving while already in-world. The working flow (realm + multiplayer_debug) is unchanged.
… UAF panics on teleport/re-entry QA's Android "crash on scene teleport" (~3/4 repro on a Galaxy A35): killing the Explorer (sign-out / return-to-discover / realm change) frees base_ui, the current scene's UI root and the Player while the SceneManager autoload still holds Gd handles; cache-hot scene spawns racing the next explorer._ready() then clone the freed nodes. The panics are caught on fast devices but escalate to native crash fallout on slow/low-RAM ones. Deterministic repro: return_to_discover() -> jump back in within 1-3s. 3 UAF panic events per cycle before, 0 after (verified on device, 5 cycles). - start_scene(): recreate base_ui eagerly when the Explorer freed it - scene_fetcher: skip+requeue spawns while there is no Explorer - parcel-change handler: validity-guard UI re-parenting, only re-attach Alive scenes' UI roots - update_scene_ui(): skip scenes whose UI root is already freed - sign_out()/return_to_discover(): detach base_ui before change_scene_to_file so freeing the Explorer can't free Rust-held UI - player_avatar_node/player_body_node: Option<Gd> with validity-filtered property getters — the plain #[var] getter panicked before any GDScript is_instance_valid() could run; freed now reads as null - player_collider_filter: null-parent guard on tree_changed during teardown Not pulse-specific: the same crash family ships in prod 1.11.1 (Sentry GODOT-EXPLORER-1DY, 3k+ events / 186 users, regressed). Fixes GODOT-EXPLORER-1DY Fixes GODOT-EXPLORER-1E0
…shrinks to header - The deep_link_jump handlers (menu + explorer) opened the Discover jump-in side panel unconditionally; with no place loaded it rendered the default "Scene Title" card at 0,0. Guard on item_data being set — a deeplink with no navigation target now lands on Discover itself. Defense in depth on top of 9d03422's router guard (mobile.dclexplorer.com links convert to decentraland://open?<params>, so config-only links hit this path). - Multiplayer debug panel: a Control never shrinks below its grown size, so collapsing kept the full-height background. reset_size after the collapse toggle and after each content refresh.
reset_size() collapsed the authored 440px width to the collapse button's width because both RichTextLabels autowrap (minimum width ~0). Snap only the height to the content minimum instead.
36x22 @ 12pt was too small a touch target on phones — now 72x44 @ 24pt.
…6038) Protocol PR #449 merged — replace the branch-build CDN tarball with the npm-published release built from protocol main (what the next dist-tag resolves to today), kept as an explicit pin so upstream publishes can't change builds without a repo change. Main still lacks the experimental-only rfc4 fields, so the patched_rfc4_comms_proto build-time patch stays. Upstream delta is additive except pulse_client.proto dropping the unused SceneListenerHandshakeRequest/ParcelRect.
Ludmilafantaniella
left a comment
There was a problem hiding this comment.
✅ Approve
Tested in a group session (6 people — QA + mobile team), cross-client against Unity on .zone.
Debug panel
- Panel shows correct Pulse state, fails count, dual-ch status
- Peer rows show PULSE*/SCENE/ARCHIPELAGO,
*correctly flags who's driven by Pulse - Nametag labels match panel data
Cross-client (Godot vs Unity)
- Smooth movement (~10Hz + interpolation), no jitter, no double-apply
- Clean teleports, no lerp streaks
- Rotation/yaw correct across headings, no collapse-to-0° bug
- Emotes: first-time, re-trigger after cancel, and join-mid-emote all work
- No running-in-place; idle settles within ~300ms
Fallback / resilience
- Unreachable pulse-server → 5-strike fallback to LiveKit-only works, no interruption
- Killing Pulse mid-session → peers flip back to LiveKit within a frame
Alternate modes
-
dual-channel=falsebehaves correctly (player never invisible) -
livekit=false(Pulse-only) works as expected - Regression: streaming/voice/chat unaffected in normal mode
No blockers. Solid fallback UX even under forced failures.
Pulse now stays OFF until feature_flags.gd confirms the mobile-bff 'pulse' flag is explicitly true — a fetch failure or an absent flag keeps the transport disabled and LiveKit avatar sync covers everything. Every other feature flag keeps its fail-open enabled default. The flag verdict lands in a dedicated slot (set_pulse_flag_enabled), separate from the deeplink override, and a single pulse_enabled() helper resolves precedence: deeplink pulse=/pulse-server= > CLI > server flag. Explicit local opt-ins force Pulse on regardless of the flag: --pulse (previously an accepted no-op), --pulse-server / PULSE_SERVER (naming an endpoint is explicit intent, matching the pulse-server= deeplink which already self-enables), and pulse=true. --no-pulse / pulse=false always win — a server flag can never force-enable Pulse over a local opt-out, and a flag verdict can no longer clobber a local test run.
Pulse transport (ENet avatar-state relay) + Multiplayer Debug panel
Adds the Pulse transport coexisting with LiveKit. Pulse replaces only the high-frequency avatar-sync slice — movement (quantized/delta-compressed, server-side AoI LOD), emotes, teleports and profile-version announcements — over ENet/UDP (port 7777). Chat, voice, scene messages, scene rooms and Archipelago discovery stay on LiveKit.
Interop contract (matches Unity in prod): outbound is dual-sent to both Pulse and LiveKit; inbound is gated per peer — while a peer is live on Pulse, their LiveKit avatar-sync is discarded (the two clock domains are incomparable, merging starves one source). Pulse is ON by default (as are dual-channel movement and LiveKit itself); disable it per run with
--no-pulseor at runtime with apulse=falsedeeplink. The 5-strike session fallback still parks Pulse automatically when the server never answers, so a blocked UDP path degrades silently to LiveKit-only.Dependency provenance:
rusty_enetcomes from the DCL-org mirrordcl-regenesislabs/rusty_enet(tree-identical to robtfm'sdecentraland-pulsebranch — the ENet-CSharp "modified protocol" the server speaks; stock ENet is wire-incompatible). The@dcl/protocolpin points at the protocol PR #449 branch tarball (commit-fdfb857), which tracks protocolmain(incl. PR #426 TouchscreenInputControls/UiInputBinding that godot main needs) and ships the pulse protos — repoint at the released npm tarball once that PR merges. Theexperimental-only rfc4 fields (PlayerEmote.is_stopping& co.,Chat.forwarded_from) are not in that build;lib/build.rsre-adds them via a build-time patch that no-ops once upstream ships them.Also included:
PULSE + SCENE + ARCHIPELAGO(*on PULSE = Pulse is driving that avatar).lambdasEndpointwithout a trailing slash; bare concatenation produced…/lambdasprofiles/0x…(404) for the whole profile retry chain until the registry fallback rescued it. URL building and endpoint comparison now tolerate both slash styles. The peer-advertised endpoint (untrusted LiveKit metadata) is also validated at intake, and a hard failure on it now falls back to the realm endpoint immediately instead of burning the ~2-min retry chain first.How to test
Everything is driven by deeplinks — whatever comes after the
?inhttps://mobile.dclexplorer.com/is the deeplink (e.g.https://mobile.dclexplorer.com/?position=0,0&scene-stats=true). On desktop the same query string can be passed with--fake-deeplink "decentraland://open?<params>".1. See it working (debug panel)
Pulse is on by default — you only need the panel:
(
pulse=falseturns Pulse off at runtime;--no-pulseon desktop.) The panel opens automatically (top-center overlay). Expect within a few minutes of entering the world:Pulse: established @ pulse-server.decentraland.org:7777 | fails 0 | dual-ch ONPeers: Nwith rows likeSomePlayer 0x1e93..452b PULSE* + SCENE + ARCHIPELAGO— the*means that avatar is being driven by Pulse (its LiveKit movement is being discarded). The name appears once the peer's profile resolves.The panel can also be toggled in Settings → Advanced → Multiplayer Debug (the old
livekit_debug=truedeeplink still works as an alias).2. Test against the .zone deployment
Everything on
.zone(content, comms and Pulse):Only the comms stack (gatekeeper + Pulse at
pulse-server.decentraland.zone) on.zone, everything else on prod — useful to interop-test against Unity clients pointed at the zone Pulse:Note: Pulse follows the
commsenv group — there is no separatepulse::group. Pulse has.org/.zonedeployments only (todayresolves to.zone).3. Join a specific Pulse server (shareable)
pulse-server=implies enabling Pulse and overrides the env endpoint — everyone opening the same link lands on the same instance. Visibility is partitioned by realm string (exact match), so testers should also be on the same realm.4. Dual-channel movement flag
Default is ON (movement dual-sends to LiveKit and Pulse). To make movement Pulse-only while the link is established:
The panel shows
dual-ch OFF. A LiveKit-only observer stops seeing your movement while your Pulse link is up, and starts seeing it again automatically if Pulse drops — the flag can never make a player invisible. Scene-room movement and the Archipelago heartbeat are never gated.5. Pulse-only mode (LiveKit fully disabled)
Dev/testing switch (
--no-livekiton desktop): no LiveKit rooms are created at all — chat, voice and scene messages are gone with them; avatar sync runs purely over Pulse. The panel shows an orangeLIVEKIT DISABLED (pulse-only mode)banner,livekit=truere-enables live (rooms rebuild, scene room reconnects). Useful to prove a behavior comes from Pulse and not from the LiveKit dual-send.6. Failure/fallback behavior
Point
pulse-server=at an unreachable host: the panel cyclesdown → connecting → …,failsclimbs, and after 5 never-established attempts showsdisabled for session(red) — avatars keep working via LiveKit throughout. A working setup that later loses the server retries every ~2s indefinitely (the path proved itself once).What to look for cross-client (vs Unity on .zone)
*in the peer rows.EmoteStartis now sent when playback actually starts (not at wheel-press), so it can't be chased by a spuriousEmoteStopduring the async emote load, and it piggybacks an idlePlayerState(Unity parks remote emote intents while the networked movement blend is > 0.1).Follow-ups (not in this PR)
position_precision, warm socket,initial_state.