Restore full Remote Play Together on current Steam builds (invite and control) - #289
Restore full Remote Play Together on current Steam builds (invite and control)#289okabeeer wants to merge 3 commits into
Conversation
…rifted builds) IClientEngine and IClientRemoteClientManager are undocumented interfaces whose vtable layout changes between Steam client builds. Because a virtual call is compiled to a fixed vtable index taken from the declaration order in the OpenSteamworks headers, once Valve inserts/removes/reorders a method, every call after it lands on the wrong function -> wrong arguments -> crash. This is the recurring "worked before, crashes after a Steam update" report. Resolve the method indices by name at runtime instead. Each Steam client stub references its own bare name as an ASCII string (Valve telemetry/profiling scope); we read the `lea reg,[rip+disp32]` that loads it and build a name -> vtable-index map, then call through the resolved index. Calls become self-adapting across builds. The scan is pure x86-64 machine-code matching and the string exists in both steamclient64.dll (PE) and steamclient.so (ELF), so the same path works on Windows and Linux; module bounds are found via PE headers (VirtualQuery) on Windows and dladdr + dl_iterate_phdr on Linux (already linked via CMAKE_DL_LIBS). Only the two drifting interfaces are routed through the resolver, via a thin proxy (CRemoteClientManagerByName) exposing exactly the methods this app uses; existing call sites are unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Creates a real RPT group before inviting (BCreateRemotePlayGroup + settle), then grants per-user input on connect. A control window shows each connected guest (avatar + name) with keyboard/mouse/controller toggles to grant or take control per person. All Steam methods resolved by name (cross-platform, in-process; no separate helper needed). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The initial control-roster implementation created the Remote Play Together group inside SendInvite() with a blocking RunCallbacks + sleep loop, which froze the UI for ~5s on the first invite. Replace it with a small state machine driven from the app timer (GroupTick): SendInvite() queues the invitee and kicks off group creation, and the invite fires once the group has settled. Also drive GroupTick from the CLI oneshot loop so -i still works. Confirmed live with two simultaneous guests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
I compiled and tested it on linux and it crashed my steam every time. I think it might be bugged there, but I'd be willing to help debug it with you |
could you give me your discord so we can stay in touch? @Raiden-Pax |
|
@Raiden-Pax Could you build that and try this: RPW_VTDEBUG=1 ./remoteplaywhatever 2> rpw-vt.log and paste rpw-vt.log? If that crashes as well, a backtrace would show us exactly where: |
|
Any updates on this? 👀 |
I tested the linux-vtable-fix branch on Arch Linux and managed to fix the initialization crash! The issue was that the I simply increased the scan limit in With 300, it successfully resolves all methods. The UI and controls are working correctly! The only issue I encountered was the lack of audio on the remote player's end. rpw-vt.log output |
Restore full Remote Play Together on current Steam builds (invite and control)
On today's Steam client, stock RemotePlayWhatever has two problems: it crashes on some builds, and even when it doesn't, invited friends can only watch — they never get control and never show up in the overlay's player list. This PR fixes both, so a guest can actually join and play, and adds a small window to hand each guest keyboard / mouse / controller individually (and take it back).
It's three self-contained commits so you can take them separately if you'd rather:
1. Crash fix — resolve internal interfaces by method name
IClientRemoteClientManageris an undocumented interface, and Valve keeps adding Remote Play methods to it between Steam client builds. A C++ virtual call compiles to a fixed vtable index taken from the declaration order in the OpenSteamworks header. So the moment a real Steam build no longer matches that order, every call after the inserted method lands on the wrong function, gets the wrong arguments, and crashes the client. This is the recurring "it worked before, now it crashes after a Steam update" report.The fix: resolve the method indices by name at runtime instead of trusting the header's order. Each Steam client method stub references its own bare name as an ASCII string near the top of the function (Valve's telemetry / profiling scope). The resolver reads the
lea reg, [rip+disp32]that loads that string, builds aname -> vtable-indexmap, and calls through the resolved index. Calls then self-adapt to whatever build is installed.steamclient64.dll(PE, Windows) andsteamclient.so(ELF, Linux — incl. Steam Deck), so the same path works on every target this project builds for.VirtualQueryon Windows;dladdr+dl_iterate_phdron Linux (already linked throughCMAKE_DL_LIBS).ClientVTableByName.h(clientvt::NamedVTable), and onlyIClientRemoteClientManageris routed through it via a thinCRemoteClientManagerByNameproxy.IClientEngineis left exactly as before (stable interface, and its stubs don't embed method-name strings). Existing call sites (RemoteClientManager()->…) compile and read identically.Init()now fails cleanly if a required method can't be resolved instead of calling a wrong slot.CancelRemotePlayInviteAndSession). That method is resolved by anchoring between its two named neighbours (…Create, Cancel, Join…); if the anchors bracket exactly one slot that's Cancel, otherwise it's skipped rather than mis-called.ABI note: on x86-64 both conventions (Windows x64 and System V) pass
thisas the first integer argument, so invoking the resolved function pointer with the interface pointer prepended is ABI-identical to the original virtual call.RemotePlayPlayer_targs are forwarded by value with their declared types.2. Guests can actually play — control roster
The root cause of "friends can only watch": stock RPW hardcodes
groupID = 1but never actually creates a Remote Play Together group. On current builds the guest joins the stream but has no real group membership — no input control, and they don't appear in the Shift-Tab player list.The fix runs the real sequence before inviting:
ShowRemotePlayTogetherUI(appId)→ pollGetLocalRemotePlayTogetherGroupIDwhile it settles →BCreateRemotePlayGroup()→ poll again → then invite with the real group id and a guest id fromGetAvailableRemotePlayTogetherGuestID. Control is granted per guest viaSetPerUser{Keyboard,Mouse,Controller}InputEnabled, using the exactRemotePlayPlayer_tSteam hands back on theRemoteClientStartStreamSession_tcallback, and re-asserted every ~2s so Steam can't silently reset it.New proxy methods for the above are added to
CRemoteClientManagerByName(same name-resolution path as the crash fix, so this stays drift-proof too).UI: a small
ControlFramewindow pops up when the first guest connects — one row per guest with their avatar and name and three green/red toggle buttons (Keyboard / Mouse / Controller). Clicking a toggle grants or revokes that peripheral for that guest live. It's plain wxWidgets (core base adv), no new dependencies, hidden until there's someone to show.3. Async invites — no UI freeze
The first cut created the group with a blocking
RunCallbacks+ sleep loop insideSendInvite, freezing the window for ~5s. This commit replaces it with a small state machine (GroupTick) driven from the existing 200ms app timer:SendInvitejust queues the invitee and starts group creation, and the invite fires once the group has settled.GroupTickis also called from the CLI oneshot loop so-istill works headless.Testing
BCreateRemotePlayInviteAndSessionresolves to 113, not the header's 126 — this build has already drifted, which is exactly what the crash fix handles.steamclient.so, but I don't have a Deck to verify end-to-end. A Linux test from you or the community would be appreciated before merge.Happy to split the commits into separate PRs, or adjust naming / style / UI to match your preferences.