feat: presence from Pulse — heartbeat flag, all-realms friends lookup, drop ONLY_SDK7 - #10022
Draft
mikhail-dcl wants to merge 11 commits into
Draft
mikhail-dcl wants to merge 11 commits into
mikhail-dcl wants to merge 11 commits into
Conversation
ws-connector now pings idle sockets itself, so the client heartbeat can be turned off remotely. The archipelago-heartbeats flag is a kill switch: it reads with a default of on, so a deploy with no flag change keeps sending heartbeats exactly as today. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
/comms/peers?id= now searches every realm and returns each peer's realm, so OnlinePlayersJsonDtoConverter maps a .dcl.eth realm into the world name and the per-friend /wallet/:wallet/connected-world lookups (N+1) are gone with WorldInfoOnlineUsersProviderDecorator and DecentralandUrl.RemotePeersWorld. The unfiltered GetAsync(ct) keeps main-realm semantics through the 308. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The platform is SDK7-only and the places API ignores only_sdk7, so the parameter and its plumbing are dead weight. The places UI keeps its SDK filter for analytics; it no longer reaches the query string. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Constructing a sign flow (SignFlowShould does, with no registry) must not require an initialized FeaturesRegistry, and the surrounding loop already reads feature state per cycle. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Contributor
🚦 CI StatusWaiting for the build to start… Lint in progress, come back later! Waiting for tests to start… Bare-metal benchmarks run automatically after each successful build; results land in this section. Add the On demand — comment |
…d guard
The sign flow's own `if (!enabled) return Result.SuccessResult()` was unreachable
from production (the room's cycle step returns first) and reported success for a
packet it never sent, so a future caller could read that success as "the position
reached archipelago". The sign flow goes back to being a dumb transport and the
flag is read in exactly one place.
That place is now a named `internal` method, `ArchipelagoIslandRoom
.SendHeartbeatIfEnabledAsync`, instead of an inline guard inside the `protected`
cycle step: the guarantee ("no Heartbeat on the wire with the flag off") is what
needs a test, and the cycle step cannot be driven from the test assembly.
Behaviour is unchanged.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…eartbeats
The comment on ForceFreshIslandAssignmentAsync claimed the server pushes a fresh
IslandChangedMessage "after the next heartbeat" - a precondition the
archipelago-heartbeats kill switch removes, which would have left a player whose
cached connection string was rejected three times with no island room at all.
The replacement mechanism is server-side: ws-connector publishes
`peer.{address}.connect` after every handshake and comms-gatekeeper re-emits the
peer's current island assignment on it. No client change is needed; the client
release must ship after both server changes.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…s off FeaturesRegistryArchipelagoHeartbeatsShould only asserted the registry boolean, so nothing observed whether the boolean is acted on: deleting the guard, or hoisting its flag read into a constructor, kept every test green while the fleet resumed 1 Hz position reporting to the service being retired. ArchipelagoHeartbeatKillSwitchShould drives ArchipelagoIslandRoom over the real LiveConnectionArchipelagoSignFlow and asserts at IArchipelagoLiveConnection - the socket abstraction - that with the flag served false nothing is sent at all, and that with the flag served true (and with no flag resolved, the rollback default) exactly one ClientPacket carrying a Heartbeat at the player's position arrives. The fixture lands in DCL.EditMode.Tests via Archipelago.Tests.asmref, which already references DCL.Multiplayer, Character, the protocol assembly, NSubstitute and UniTask, and DCL.Multiplayer grants it InternalsVisibleTo. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
IsEnabled(id, defaultWhenAbsent) differed from the off-by-default IsEnabled(id) by one argument and from IsEnabled(id, variantId) only by that argument's type, so a second kill switch added by copying the ArchipelagoHeartbeats line and dropping the named argument would silently default off - the exact inversion of the rollback rule the overload existed to satisfy. It also made IsEnabled(id, null) ambiguous between the bool and string overloads. IsEnabledUnlessKilled(id) has no such twin. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
ALL_REALMS_RESPONSE was hand-rolled with address, position and realm only, so the one fixture that claims to model the all-realms response end-to-end omitted id, lastPing and parcel and could not catch a DTO that chokes on a field the real server sends. It now deserializes the same bytes the converter fixture does, straight out of the copied golden. The fixture doc also stops implying a request-volume win on the friends panel: every production call site passes a single-element buffer, and C2 caps ?id= at 200 ids. AskForEveryFriendInOneRequest is renamed to AskForEveryRequestedIdInOneRequest to say what it actually pins. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
[JsonProperty("world")] on worldName and [JsonProperty("wallet")] on avatarId
existed so the deleted WorldInfoOnlineUsersProviderDecorator could deserialize a
worlds-content-server connected-world body straight into the struct. Nothing
reads those names now - the converter populates the struct field by field from
its own DTOs, and the peers response uses realm and address - so anyone who
later serialized an OnlineUserData for a cache or a debug dump would have got
keys matching no live contract.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
ResolveNoPerFriendConnectedWorldLookup copied LeaveNoDecentralandHostBehindOnACustomBaseDomain's scaffolding to assert a literal substring the compiler already makes impossible: DecentralandUrl .RemotePeersWorld is gone, so nothing can resolve to that template, and a reintroduced lookup under any other path segment - /current-world, say - would pass it. What actually catches a returning per-friend request is ArchipelagoHttpOnlineUsersProviderShould's request-count assertion. The genuinely useful half of that commit, the RemotePeers case on MoveEveryHostOntoTheCustomBaseDomain, stays. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Not verified in an editor — required before review
This branch was written in a sparse checkout without a Unity editor: nothing here has been
compiled and no test has been run. Before review, please
Explorer/and run the edit-mode suite,.cs.metafiles import cleanly (they were hand-written with fresh GUIDsbecause
*.metais outside this checkout's sparse set — the editor may want to re-serialize them),archipelago-heartbeatsserved asfalse, the islandroom stays connected, keeps receiving
island_changed, and reconnects after a socket drop (ws-connectorpings idle sockets itself since the server-side WP3a change),
reaches): make the cached connection string fail three times in a row so
ArchipelagoIslandRoom.ForceFreshIslandAssignmentAsyncruns, and confirm anisland_changedstillarrives. This works because ws-connector publishes
peer.{address}.connectafter every handshake andcomms-gatekeeper re-emits the peer's current island assignment on it — see the rollout note below.
Rollout ordering — this client release must ship last
The kill switch removes the client
Heartbeat, and until recently a fresh peer session got its islandassignment only after one. Two server changes replace that: ws-connector publishes
peer.{address}.connectafter every handshake, and comms-gatekeeper re-emits the currentisland_changedon that subject. Both must be deployed before a client build carrying this flagreaches production (rollout step 7 after steps 5–6), and
archipelago-heartbeatsmust stay unset (ortrue) until they are. No client-side change is needed for it.What this does
Server side (already shipped):
GET …/comms/peers?id=…searches all realms and every peer carries"realm"("cozyfarm.dcl.eth"for a world,"main"for Genesis City); ws-connector pings idle sockets.1. Heartbeats behind a server-controlled flag. New feature flag
archipelago-heartbeats(
FeatureFlagsStrings.ARCHIPELAGO_HEARTBEATS/FeatureId.ArchipelagoHeartbeats), read inFeaturesRegistrythroughFeatureFlagsConfiguration.IsEnabledUnlessKilled— a default of on, so adeploy that changes no flag, and a client whose flag fetch failed, behave exactly as today.
The flag is read in exactly one place:
ArchipelagoIslandRoom.SendHeartbeatIfEnabledAsync, which thecycle step calls after its reconnect check. With the flag off nothing else runs — no position read, no
thread-pool hop, no call into the sign flow — so no
Heartbeatreaches the socket.LiveConnectionArchipelagoSignFlowstays a dumb transport that always sends what it is asked for, so asuccess result from it always means the position really reached archipelago. The socket, the sign flow
and
island_changedhandling are untouched.Rollback: serve
archipelago-heartbeats: true(or nothing at all) — today's behaviour.2. One all-realms friends lookup.
OnlinePlayersJsonDtoConvertermaps a peer'srealmintoOnlineUserData.worldNamewhen it ends with.dcl.eth(case-insensitive suffix, name kept verbatim),and leaves it unset otherwise (
main, another realm, absent field).WorldInfoOnlineUsersProviderDecorator,DecentralandUrl.RemotePeersWorld, itsDecentralandUrlsSourcecase and itsGatewayUrlsSourceentryare deleted, along with the dead
[JsonProperty("world"/"wallet")]mapping onOnlineUserDatathat onlythe decorator's
connected-worldbody ever used.To be accurate about the win: this is not an N-per-friend saving. Every call site
(
FriendListSectionUtilities,FriendSectionController,FriendsSectionDoubleCollectionController,PassportController,CommunityPlayerEntryContextMenu,GenericUserProfileContextMenuController)passes a one-element buffer with a single address, so the retired decorator cost one extra
/wallet/:wallet/connected-worldrequest per jump-in click (1+1, not 1+N). What is gone is that secondrequest and its dependency on worlds-content-server. The contract caps
?id=at 200 ids per request(201+ answers
400 {"ok":false,"error":"too many ids (max 200)"}), so a future caller that does batch areal friend list has to chunk. The unfiltered
GetAsync(ct)(map hot-user markers) is unchanged andkeeps main-realm semantics through the 308.
3.
ONLY_SDK7housekeeping. The platform is SDK7-only and the places API ignoresonly_sdk7, so theURLParameterand everyonlySdk7parameter/argument are gone — the query parameter is no longer sentat all. The Places UI toggle is deliberately left as it is:
PlacesFilters.SDKVersionstill exists, isstill settable, and is still emitted to analytics as an applied filter (
PlacesAnalytics), while nolonger reaching the request. Removing the control needs a prefab edit, which is outside this sparse
checkout — please open a follow-up ticket for the UI + analytics removal.
Not touched:
MainSceneLoader.IsLIvekitDeadAsync(ArchipelagoStatus+GatekeeperStatushealthgate) and
GoToChatCommand/goto crowd(ArchipelagoHotScenes).Tests (written, not run)
Explorer/Assets/DCL/Multiplayer/Connections/Archipelago/Tests/ArchipelagoHeartbeatKillSwitchShould.cs— the kill switch's only guarantee. Drives
ArchipelagoIslandRoomover the realLiveConnectionArchipelagoSignFlowand asserts atIArchipelagoLiveConnection, the socketabstraction: flag served
false→ nothing sent at all; flag servedtrue, and no flag resolved(the rollback default) → exactly one
ClientPacketwhoseMessageCaseisHeartbeat, carrying theplayer's position. Deleting the guard or hoisting its flag read into a constructor fails here.
Explorer/Assets/DCL/FeatureFlags/Tests/FeaturesRegistryArchipelagoHeartbeatsShould.cs— flag absent →enabled; served
true→ enabled; servedfalse→ disabled; plus the kill-switch read itself.Explorer/Assets/DCL/Tests/Editor/OnlinePlayersJsonDtoConverterShould.cs— over the contract golden(
Explorer/TestResources/iteration-2/http/peers-by-id.json, byte-identical to the fixture pack, pinnedeol=lfin.gitattributesbecause the pack digests it):cozyfarm.dcl.eth→worldName,main→none, plus the realm→world-name cases (absent /
null/ empty / non-world ENS / mixed-case suffix).Explorer/Assets/DCL/Tests/Editor/ArchipelagoHttpOnlineUsersProviderShould.cs— edit-mode with asubstituted
IWebRequestController(play-mode would need a live archipelago). The stubbed response isthe golden
bodyitself (id,lastPing,parcel,realmincluded), decoded by the realconverter: three ids produce one
?id=&id=&id=request, no request URL containsconnected-world,and the peer in a world comes back with the world name from that single call.
DecentralandUrlsSourceShould—RemotePeersresolves on a custom base domain.Run them in the editor: Window > General > Test Runner > EditMode (or the CI edit-mode job), which runs
all four fixtures — none of them carries a
[Category], so the workflow's-testCategory "!Performance"filter includes them.
Review round 1 — what changed
ArchipelagoHeartbeatKillSwitchShould(above). The flag read moved out of theprotectedcycle step intointernal ArchipelagoIslandRoom.SendHeartbeatIfEnabledAsyncso it can be driven fromDCL.EditMode.Tests, whichDCL.Multiplayeralready grantsInternalsVisibleTo.peer.{address}.connect+ comms-gatekeeper re-emit). The stale doc comment onForceFreshIslandAssignmentAsyncis corrected; rollout ordering and the extra play-mode check are stated above. No client logic added.IsEnabled(id, defaultWhenAbsent)was one keyword from its oppositeIsEnabledUnlessKilled(id). No twin to mis-copy, andIsEnabled(id, null)is no longer ambiguous.body(shared with the converter fixture) instead of a hand-rolled two-field response.AskForEveryFriendInOneRequest→AskForEveryRequestedIdInOneRequest.[JsonProperty]onOnlineUserDataResolveNoPerFriendConnectedWorldLookupwas vacuous