Skip to content

EOSPlus+Steam friend-join: mainline generic LAN-emu fixes - #2

Merged
gabrielgad merged 10 commits into
mainfrom
feature/eosplus-steam-friend-join
Jun 16, 2026
Merged

gabrielgad merged 10 commits into
mainfrom
feature/eosplus-steam-friend-join

Conversation

@gabrielgad

Copy link
Copy Markdown
Collaborator

Mainline the generic LAN-emu fixes that enable native friends-list browse→join for EOSPlus+Steam titles (validated end-to-end on StarRupture: friend appears in Join Game → join → Welcomed by server → travel into host world).

Cherry-picked off diag/session-config-logging with all diagnostic-only logging commits stripped:

  • connect: map Steam external id → peer's real Epic handle in EpicAccountId_FromString — the load-bearing fix (UE friend-search is epic-keyed)
  • presence: auto-derive local presence from a presence-enabled lobby (+ release announce-hold, real product identity, MAX_PRESENCE_RECORDS 8→24)
  • social: map Steam external accounts to PUIDs over the LAN beacon
  • auth: EpicAccountId_FromString returns NULL for malformed ids (crash-fix)
  • lobby_search / session_search owner-match; immediate session propagation

Builds clean on CI (verified the bridge + presence-from-lobby symbols are present in the artifact).

…n read

Fixes the splitux Satisfactory co-op join (E007 "unable to join user
backend sessions"). Root cause, proven from logs: the host's EOS session
grows from 17 to 27 attributes when it finishes loading the save (adds the
gameplay attrs the join-migration requires), but the joiner reads the
session ~2s before the complete copy reaches its discovery cache, commits
to the stale 17-attr snapshot, and the migration fails so P2P never starts.
The test bench masks this with a 15s launch stagger (host always fully
loaded before the joiner searches); splitux launches instances together so
the read lands inside the propagation gap every time.

Two changes close the gap:
- EOS_Sessions_UpdateSession now broadcasts the session over LAN discovery
  immediately on every create/update, instead of waiting for the next ~2s
  announce tick. The joiner's cache reflects the host's 27-attr session
  within milliseconds of the host producing it.
- EOS_SessionDetails_CopyInfo re-pulls the latest advertised session from
  the live discovery cache (via a new back-pointer on the details handle +
  discovery_find_cached_session) when it has gained attributes, instead of
  returning the frozen search/Find-time snapshot.

(cherry picked from commit 24f8b10)
EOS_LobbySearch_Find with a target_user_id filter only checked the
lobby's members[] roster. LAN-broadcast lobbies carry just the owner on
the wire (members[] is empty until the lobby is joined), so a
search-by-user for the host — the common 'find friend's game / join by
presence' path, e.g. StarRupture's join menu — always returned 0 results
even though the lobby was discovered. Match the target user against the
owner as well, so an owner-hosted wire-discovered lobby is found.

(cherry picked from commit 9a02476)
Steam+EOS games (e.g. StarRupture) drive their friends/join browser off
the *Steam* friends list: for each Steam friend they call
EOS_Connect_GetExternalAccountMapping(type=STEAM, steam_id) to get the
friend's ProductUserId, then attribute a discovered lobby to that friend.
The LAN emulator never carried Steam IDs, so that lookup always failed and
the browser showed 'No sessions available' even though the lobby search
itself returned the host.

Relay each instance's Steam ID (EOSLAN_STEAM_ID, set by the bench from the
goldberg account_steamid) in the LAN user beacon, store it per-peer, and
resolve it: GetExternalAccountMapping now dispatches type=STEAM to a new
social_bridge_resolve_puid_by_steam (local + discovered peers). Generic to
any Steam+EOS friends-driven join UI, not StarRupture-specific.

Wire format: steam_id[24] added after puid in the user-beacon serialize/
parse; both peers run the same build so the format stays consistent.

(cherry picked from commit 66c06a3)
A well-formed EpicAccountId is 32 hex chars; FromString was instead handing
back the LOCAL user for any shorter/garbage string. Steam+EOS games bridging
external accounts call FromString on a Steam ID string (e.g. the 17-char
'76561198000000001') while building a friend's net id. Returning the local
user there pairs a *remote* host's ProductUserId with the *joiner's*
EpicAccountId, which violates UE's one-puid<->one-epic registry invariant
(FUniqueNetIdEOSRegistry assert InEpicAccountId == FoundEpicAccountId) and
hard-crashes the game the moment the Steam friends list is read.

Return NULL (-> IsValid() false) like real EOS, so the caller treats the id
as absent (PUID-only) rather than a conflicting epic. Surfaced by StarRupture
once Steam->PUID external mapping started resolving.

(cherry picked from commit 397931e)
Same wire-discovered-owner bug as lobby_search (9a02476), in the Sessions
interface. SessionSearch_Find's target_user_id filter only scanned
registered_players[], which is empty on LAN-discovered sessions (members
aren't carried on the wire; owner_id is a process-local pointer, only
owner_id_string survives). StarRupture's friends 'Join Game' browser does a
per-friend Sessions search keyed on the friend's puid, so it returned 0 even
though the friend was hosting -> 'No sessions available'. Match the session
owner by string (owner_id_string vs ToString(target_user_id)). Added
ACCEPT/REJECT logging to confirm whether the per-friend search is reached.

(cherry picked from commit 9b2fb41)
Friends 'Join Game' in EOSPlus+Steam titles (e.g. StarRupture) is driven by
EOS presence, not session/lobby search: the joiner enumerates Steam friends
and calls EOS_Presence_CopyPresence(host) per friend. The host never touches
the Presence interface — it creates a bPresenceEnabled EOS lobby with a
CUSTOMJOININFO attribute and relies on real EOS auto-tying that lobby to its
presence join-info. The emu only populated g_local_presence via
SetJoinInfo/SetData, so the host's user beacon carried empty presence, the
joiner's CopyPresence returned 0 records, and the browser showed 'No sessions
available' despite the lobby being fully discoverable.

Mirror a presence-enabled lobby into the local user's published presence:
lobby id as join-info, lobby attributes as presence data records (join-critical
keys CUSTOMJOININFO/MapName/conn-counts first, capped at MAX_PRESENCE_RECORDS).
Wired on lobby create/update/join, cleared on leave/destroy. Gated by
g_game_set_presence so games that drive presence directly (Satisfactory) are
untouched.

This is the EOS-side analog of the goldberg connect/Joinable auto-derive.

(cherry picked from commit 8aa2337)
… hosts

The first friend/presence notification (which triggers the game's one-shot
FetchFriendInfoAsync + presence re-read) was held until the host had an EOS
session_id. EOSPlus+Steam titles like StarRupture host via a presence-enabled
lobby, not a session, so session_id is always empty: the hold never released,
the friend was never announced as joinable, and the game never re-read
CopyPresence after the lobby's 8 presence records arrived -> 'No sessions
available' forever, even though the records were correctly relayed and cached.

Also release the hold once the host's beacon carries presence records
(record_count > 0), i.e. the presence-enabled lobby is fully populated. The
one-shot fetch then still runs against complete data.

(cherry picked from commit 0a5b50e)
…ords

The host's presence-enabled lobby data now reaches the joiner (CopyPresence
returns 8 records), but the game's CommonSession friend-search still yields 0
sessions. Prime suspect: CopyPresence hardcoded ProductId/ProductName=
'Satisfactory' (the emu's first target game), so StarRupture (ProductId
3e9825c4...) sees the friend as playing a DIFFERENT product and filters it out.

Deep-copy ProductId/ProductName/ProductVersion at EOS_Platform_Create and report
them for a peer's presence (a LAN clone runs the same product). Also dump the
presence records + product/join_info on CopyPresence to see exactly what the
game's friend-search reads.

(cherry picked from commit 58d50e3)
UE copies every EOS presence data record verbatim into
FOnlineUserPresence.Status.Properties (UserManagerEOS.cpp:3047-3051), and the
game's own friend-join code reads those keys to decide joinable. A
presence-enabled lobby host advertises ~14 attributes (incl. ALLOWEDPLATFORM,
BUILDCL, SESSIONTEMPLATENAME, GameMode) but we capped presence at 8 records,
silently dropping 6 — any of which the join check may gate on (the game binary
references bJoinViaPresence + EOS_SessionModification_SetAllowedPlatformIds).
Lift to 24 (beacon buffer is 64KB; prec_count is uint8_t).

(cherry picked from commit e8645a0)
…untId_FromString

ROOT CAUSE of StarRupture friends-browse 'No sessions available' (RE'd against
UE source + live logs): the game's friend-search enqueue reads each friend's
session via UE FUserManagerEOS::GetExternalIdMapping, which treats EVERY external
id as an Epic id: EOS_EpicAccountId_FromString(id) -> IsValid -> registry Find.
The friend ids are STEAM ids, so FromString('76561198...') returned NULL (correct:
not 32-hex) -> IsValid false -> null mapping -> friend never enqueued -> friends
search completes with 0 results, FindFriendSession/RequestLobbyData never reached.
(The EOSPlus base Steam user interface that would handle steam ids is nullptr in
UE -- 'BaseUserInterface not valid'.)

Fix: when FromString gets a non-32-hex id, resolve it as a Steam id via the social
bridge (g_peers: steam->peer) and return that peer's REAL epic handle, so the
epic-keyed registry round-trips (FindOrAdd on the steam query and Find on the read
both land on the host's real epic). Returns the REMOTE peer's epic, never the local
user's, so it does not reintroduce the one-puid<->one-epic crash that returning the
local handle caused (397931e). Unknown steam ids still return NULL.

(cherry picked from commit 302ad7a)
@gabrielgad
gabrielgad merged commit e43a132 into main Jun 16, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant