EOS SDK Bridge Integration Test

Load via SOOverrideURL in GameUserSettings.ini. Bridge status: Checking...

Bridge Discovery

Access Token Override (Review 52399292)

Client Access Token (Review 52399308)

Notifications (Review 52403607)

Social (14 functions)

get-idle-state returns the current EUserIdleState (Active/Idle/Away). subscribe-idle-state-changes registers for outbound idle-state-changed pushes - observe via Long Poll. mic-permission-changed-subscribe registers for outbound mic-permission-changed pushes with payload {isAllowed:bool} (true iff OS mic permission is authorized) - polled at 1s, fires only on transition.


Voice-party tray state - new in v10 (DISTRO-13486)

Inbound push (overlay -> launcher, fire-and-forget) of the voice/party state that drives the Windows system-tray icon when the Launcher is backgrounded. Routes FEOSSocialNotificationHandler::HandleVoicePartyStateChanged -> ISocialSettingsProvider::SetVoicePartyState -> IVoicePartyStateService -> IWindowsSystemTray::SetIconState. Unknown states (e.g. "Bogus") are rejected with a Warning and the icon is left unchanged. Background the Launcher and watch the tray icon (and the launcher log under -eosverbose).


Sends a desktop toast. When user clicks it, notification-actioned is pushed back via Long Poll.


Both buttons share a fixed conversationId="conv-pending". Fire "Send Notify" several times - when toasts are suppressed (fullscreen game / DND / Focus Assist) they pile up in FNotificationService::NotificationQueue all under the same id. Then click "Conversation Read" to drop the whole batch via ClearNotificationsByConversationId. Live (already-showing) toasts are intentionally not faded.


The producer shape that has no key. This reproduces what production actually sends for a DM: a notify with conversationType+senderId but no conversationId. Without a key the toast would be permanently unclearable, so ConversationKey::Resolve synthesizes synth:dm:<senderId> at the Domain choke point - to see it, run with -LogCmds="LogSocialNotify Verbose" and watch for synthesized launcher-local key (category LogSocialNotify; the line is Verbose and the category defaults to Log, so it is invisible without that switch). Only a DM gets a surrogate, because it is the one producer path that omits the id; a group message always carries a real conversationId and needs no invention.
A synth: key is launcher-local, so an inbound conversation-read carrying the real id will not clear this toast. This is permanent, not a pending fix. The producer is the Social Overlay app, which is frozen and will not start sending conversationId on direct-message-received, so for a DM the toast click is by design the only thing that ever clears the toast. Group chat is unaffected and works end to end.

Common (5 functions)

Opens the URL in the system default browser via ICommonProvider::LaunchExternalUrl. get-window-state returns the current ELauncherWindowState (Foreground/Background/Minimized). subscribe-window-state-changes registers for outbound window-state-changed pushes - observe via Long Poll.


Connection state - new in v13 (DISTRO-14430 / SO-9508) unknown

get-connection-state -> get-connection-state-response {state:"Online"|"Offline"}; subscribe-connection-state-changes is idempotent and registers for outbound connection-state-changed. Both are EOS-bridge only - there is deliberately no window.ue.* counterpart, so the legacy-bridge buttons elsewhere on this page have no equivalent here.
What it actually reports: IDisconnectNotificationService::IsServiceDisconnected() - "Offline" iff any of the three tracked services (FAuthStatusService = MCP identity, FEpicConnectStatusService = EpicConnect websocket/STOMP, FEOSAuthStatusService) reports NotConnected. It is not internet reachability, not presence (that is social/get-online-status above), and not the offline info strip's visibility - the strip layers a show delay, a debug force and a post-reconnect hold on top, and is pinned visible for any user who is not signed in online, so the strip and this value legitimately disagree in both directions.
Two results that surprise people (both verified, both the opposite of the obvious guess): early reads are pessimistic - FEpicConnectStatusService is seeded from IsConnectedAndReceiving(), so right after sign-in this answers "Offline" until the websocket reaches StompReceiving. And there is no signed-out answer at all - the common handler only exists once IsUserSignedIn() is true, which is strictly SignedInOnline, so signed out (or cold-started straight into offline mode) the intent is simply unhandled and you will see nothing come back.

How to drive a real transition locally. Nothing on this page can move the value - it is backed by real service state, so you have to break the connection for real:
1. Click SubscribeConnectionStateChanges (it auto-starts the Long Poll). You get one immediate connection-state-changed snapshot - that is by design, the channel is a level, not an edge, and every subscribe re-echoes.
2. Disable the network adapter / turn off Wi-Fi. The EpicConnect websocket drops, FEpicConnectStatusService goes NotConnected, and you should get exactly one connection-state-changed with {state:"Offline"}.
3. Re-enable it. After the socket reconnects and STOMP resumes you should get exactly one {state:"Online"}.
The negative test is the interesting one. Open the debug tools (-DebugTools, or Ctrl+Shift+~) and hit Toggle Disconnection Info Strip. The strip appears, the launcher broadcasts OnStateChanged - and you should see no connection-state-changed at all, because the services are still Connected and FCommonProviderAdapter dedupes. If you do see a push there, the adapter-side dedupe has regressed. Same expectation for a second service flapping while another is already down: the OR-aggregate does not move, so nothing is pushed.

Game (3 functions) - new in v7 (DISTRO-13251)

Routes FEOSGameNotificationHandler -> IGameFeatureProvider -> IGameInviteManager::AcceptGameInvite/RejectGameInvite. join-game and accept-game-invite are intentionally aliased to the same provider call (matches legacy USocialJSBridge::JoinGame in SocialJSBridge.cpp:791). Restores the FN party-invite Join Game flow in -launcherlabel=EOSSDK builds.

ProductInfo (1 function)

TTS (9 functions)


STT (12 functions)

Outbound Events (Launcher -> Overlay, appear in Long Poll)

These are pushed automatically by the Launcher. Start Long Poll to observe them.
- social/notification-actioned - fires when user clicks a desktop toast (trigger via Send Notify above)
- social/screen-reader-state-changed - fires every 1s when screen reader state changes
- social/idle-state-changed - fires when user idle state transitions (subscribe via SubscribeIdleStateChanges above)
- social/mic-permission-changed - fires when OS microphone permission flips, payload {isAllowed:bool} (subscribe via SubscribeMicPermissionChanges above)
- common/window-state-changed - fires when launcher window state transitions (subscribe via SubscribeWindowStateChanges above)
- common/connection-state-changed - payload {state:"Online"|"Offline"} (subscribe via SubscribeConnectionStateChanges above). Fires once immediately on every subscribe, then only on a genuine change of the service aggregate - deduped at FCommonProviderAdapter, so info-strip show/hide/debug-toggle broadcasts produce nothing
- tts/speech-start|complete|error|progress - fires during TTS playback (trigger via StartSpeak above)
- stt/dictation-result|error|started|ended - fires during STT dictation (trigger via StartDictation above)

Badge Count (Review 52399314)

The overlay sends badge count via update-badge-count intent. The SDK fires EOS_UI_OnBadgeCountUpdatedCallback to the Launcher, which routes through OnBadgeCountUpdated event to SocialJSBridge.





These call setbadgecounter on the legacy CEF bridge directly, bypassing the EOS SDK - the only way to exercise the read-state path that runs with -UseEOSSDKSocial=false and on Mac (v12, DISTRO-14213). The clear fires on the >0 -> 0 edge, so push 3 first, then 0; a bare 0 from a standing 0 does nothing by design. Queue some toasts first (Notifications section below, with the Launcher window minimized or a fullscreen game focused so they stay pending) and confirm they are gone after the 0. With UseEOSSDKSocial=true the relay is gated off and only the cache/callback effects fire - use the EOS buttons above instead.


Legacy CEF bridge equivalents of the EOS-SDK voice-party push (v11, DISTRO-13486), for older store pages on window.ue.social.*. getvoicepartystate() returns the current state string; voicepartystatechanged(cb) fires cb(state) on each change. Windows + UseEOSSDKSocial only - returns "not-in-party" and never pushes otherwise. Drive transitions from the v10 voice-party buttons above (or the overlay) after subscribing.

Overlay Control

EOS Native Popup Window (DISTRO-13514)

Triggered by a plain JS window.open(url, target, features) where target starts with EOSNativeWindow. CEF's OnBeforePopup inside the EOS overlay process intercepts it (see EOSOverlayWebview.cpp::FOverlayWebview::OnBeforePopup), creates a native HWND, and fires EOS_UI_OnPopupWindowCreatedCallback. That callback flows to the launcher via EOSManagerEGL -> FriendsAndChatManager::OnPopupWindowCreated -> FExternalChatPresentationImpl::HandleEOSPopupWindowCreated_EOS, which wraps the HWND in a Slate SWindow. The suffix after EOSNativeWindow becomes the Id field (EOSNativeWindowChat -> Id="Chat" reuses the existing ChatWindow member; EOSNativeWindow with no suffix -> empty Id; any other suffix lands in EOSExtraPopupWindows_EOS). Watch the launcher log for LogExternalChatPresentation and LogFriendsAndChatManager: OnEOSSDKPopupWindowCreated.

CEF target-name reuse: per the HTML window.open spec, calling window.open(url, target, ...) with a target name that ALREADY belongs to an open window will navigate that existing window rather than open a new one - and OnBeforePopup never fires on the second call, so the launcher gets no event. The buttons below side-step this by either (a) using distinct target names per variant or (b) closing the previous popup via WindowRef.close() before reopening.


"Open unnamed popup" has empty Id from the SDK. The launcher normalizes empty Id to "Chat" at the point of entry (see HandleEOSPopupWindowCreated_EOS) so both EOSNativeWindow and EOSNativeWindowChat share one map key and one close-binding Id - avoids alias bugs where a stale empty-key entry could route an alive-ticker cleanup to the wrong wrapper. Opening this while Chat is already open replaces Chat (and vice versa). Expected.


These use distinct target names (EOSNativeWindowChat900x600, EOSNativeWindowChat500x350) so CEF can't reuse a previously-opened browser. The Id is the suffix after EOSNativeWindow, so these land in EOSExtraPopupWindows_EOS (not the ChatWindow slot). Exercises popupFeatures.widthSet/heightSet/xSet/ySet -> FEOSPopupWindowCreated::Width/Height/X/Y.


JS-side close is not possible in this setup. CEF returns null from window.open whenever OnBeforePopup swaps in a custom client (which it does for every EOSNativeWindow* target - see FOverlayWebview::OnBeforePopup: client = new FNativePopupWebview(...)). The calling JS never receives a Window handle, so WindowRef.close() can't be called. Close popups manually via the wrapper's title-bar X button - this destroys the Slate wrapper's native HWND, and Windows then cascades DestroyWindow to the cross-process EOS child HWND (the child was reparented under the wrapper at popup-create time and re-styled WS_CHILD, so it dies with its parent). CEF observes the HWND going away and releases the target name; the next window.open with the same target works as a fresh popup. (The narrow case where the wrapper is destroyed before the deferred reparent fires is handled separately by HandleEOSPopupWindowCreated_EOS's realize ticker, which PostMessages WM_CLOSE to the orphan EOS HWND.) This button just lists what's been opened from this page.


For comparison: opens with target="_blank". Does NOT trigger the native-popup path; CEF spawns a regular off-screen tab inside the overlay process. Use this if you suspect the EOS-prefixed buttons aren't reaching OnBeforePopup at all.

Long Poll State Changes

Event Log