Fix Steam Launcher for Multiplayer/Networking/Cloud Saves - #720
Open
maxjivi05 wants to merge 29 commits into
Open
Fix Steam Launcher for Multiplayer/Networking/Cloud Saves#720maxjivi05 wants to merge 29 commits into
maxjivi05 wants to merge 29 commits into
Conversation
Adopted from the SteamLite VAC handoff by The412Banner (Bannerlator), https://github.com/The412Banner/winlator-contents. Their agent is itself a derivative of WinNative's wn-steam-launcher and they handed the work back so WinNative could reach VAC-secured servers. Their handoff, notice and reference scripts are kept under wn-steam-launcher/steamlite-handoff, and CREDITS.md there records exactly what was taken. IClientAppManager::LaunchApp only returns NoError when the app is registered under steamapps\common\<InstallDir> against a matching appmanifest, where InstallDir is Steam's canonical install-folder name. The manifest, the symlink and the launch path were all built from the on-disk folder name instead, which diverges for custom install locations and for apps that fall back to the store name. When it diverged LaunchApp failed with EAppUpdateError=18 and the launcher fell back to CreateProcess, which starts the game -insecure, and VAC-secured servers refuse that connection. The canonical name now drives the manifest installdir, the common symlink and the launch path together. ColdClient keeps the on-disk name its INI is written against, and the shared call site links both names when they differ so neither mode regresses. The launcher also accepts a spec file, by argv or WN_STEAM_GAMEEXE_FILE, carrying the exe path and an appId override, so game paths no longer have to survive Wine command-line quoting. A bare argv path still works.
Verified the SteamLite handoff against the built agent under Wine 9.0. The handoff's argv[1] handling fopen()s the argument and treats any file that opens as a spec, so a bare game-exe path - the documented back-compat form, and the fallback taken when the spec cannot be written - had the first line of its PE header parsed as the game path. The first line must now look like an absolute Windows path before the argument is accepted as a spec. Creating the canonical steamapps/common link as a second call to ensureSteamappsCommonSymlink also re-copied the game's _CommonRedist into Steamworks Shared on every launch. Both link names are now created in one pass with the redists staged once, which also restores the on-disk-named link at the setupSteamEnvironment call site. The reference scripts vendored from the handoff were being dropped by the repo-wide *.py ignore; force-added so the attribution is complete. Credit: The412Banner (Bannerlator), https://github.com/The412Banner/winlator-contents - see wn-steam-launcher/steamlite-handoff/CREDITS.md
Resolve SteamUtils.createAppManifest: keep the branch's canonical installdir name for the steamapps/common symlink and take upstream's cached installSizeOnDisk() in place of the per-call directory walk.
…f, insecure fallback
The Steam Launcher reached VAC-secured servers only intermittently and some
live-service titles refused to go online. Four separate defects, all verified
on device against a real account.
1. App ownership ticket was never fetched.
IClientUser::GetAppOwnershipTicketLength returned 0 for the app on every
launch, which is the state steamclient itself names in
"GetAuthSessionTicket called but no app ownership ticket available" - the
game's GetAuthSessionTicket then yields nothing for a game server or a
publisher backend to validate. The agent now calls
BUpdateAppOwnershipTicket after the licence sync and waits for the ticket,
logging its length before and after (measured 0 -> 178 bytes on device).
The vtable slots are hardcoded per client build and were derived for
steamclient64 md5 8a1a789229c222017b741fad46d73fee by locating the
IClientUser vtable in .rdata from a known slot address, then mapping each
method-name literal's RIP-relative lea site back to the enclosing slot.
Validated against BIsSubscribedApp(181), SetLoginToken(56),
BHasCachedCredentials(49) and BLoggedOn(4), then confirmed at runtime:
BUpdateAppOwnershipTicket slot 69 0x228
GetAppOwnershipTicketLength slot 103 0x338
WN_STEAM_OWNERSHIP_SLOT still overrides the default.
2. Launcher hand-off looked like a failed launch.
LaunchApp starts the app's configured entry, which for most engines re-execs
an arch-specific sibling and exits (cstrike.exe -> cstrike_win64.exe,
Foo.exe -> Foo-Win64-Shipping.exe). Matching the image name literally saw
zero processes and fell through to the insecure CreateProcess fallback,
which both loses VAC eligibility and trips "Only one instance of the game
can be running". Processes are now matched by a stem that folds arch and
hand-off suffixes, plus any process whose image path sits under
steamapps\common\<installdir> for renames the stem cannot catch. The same
matcher drives the exit watch and teardown, so the real child is reaped.
3. The insecure fallback raced the secure launch.
The appear window is now 120s with a 5s heartbeat, error and null-handle
paths get a 30s grace instead of falling through immediately, and there is
a final re-check plus a guard inside create_process_game. The tailer
re-arms its 35s watchdog on the heartbeat, so a slow cold start no longer
reads as a failure while a dead agent still times out.
4. Ownership/licence and appinfo were not waited on.
BIsSubscribedApp is polled before appinfo and LaunchApp (3-5s on device),
because PICS appinfo for a token-gated app needs the licence list first.
The appinfo wait is 8s, dropping to 1.5s once an app is recorded as never
delivering AppInfoUpdateComplete_t; LaunchApp's MissingConfig retry is the
real safety net. WN_STEAM_SKIP_APPINFO and WN_STEAM_APPINFO_WAIT_MS tune it.
App side:
- isUserOverriddenSteamExe compared bare base names, so auto-detect picking the
64-bit sibling looked like a user override and skipped LaunchApp entirely -
a guaranteed insecure launch. It now compares exe families.
- WN_STEAM_BUILD_ID and WN_STEAM_DEPOTS fed the agent's ACF writer with the
newest published build id and manifest gids rather than what is installed,
overwriting the correct values the Kotlin path computes. Both now go through
SteamBranchSelection.installedBuildId/.installedManifest.
- checkForAppUpdate reported "no updates" whenever every depot's manifest
failed to resolve from PICS; it now cross-checks the recorded install build.
- steamservice and its version VDFs are also staged to
Common Files\Steam, the location steamclient looks in.
Diagnostics the agent now writes to wn-launcher.log: game identity and install
root, per-DLL steam_api genuineness (restoring from .orig when an emulator swap
is found), the ownership ticket length either side of the update, the
ActiveProcess registry read back after launch, and the game process's loaded
Steam modules.
Adds JVM tests for the exe-family matcher.
VAC's process monitor lives in steamservice.dll, not steamclient: string
analysis shows VacProcessMonitor (src\common\vacprocessmonitor.cpp) with two
references in steamservice.dll and zero in steamclient64.dll. steamservice.dll
is 32-bit, so a 64-bit agent can never load it in-process - the client logged
"Failed to load Steam Service (GLE 193 = ERROR_BAD_EXE_FORMAT)" on every launch
and Dota 2 reported "VAC was unable to verify that your machine is secure".
Real Steam's own topology is a 32-bit client host, so the agent now builds for
i386 as well and is selected by default. 64-bit games are unaffected: they load
steamclient64.dll in their own process and IPC to the 32-bit host, exactly as on
Windows.
Device result: Dota 2 reaches the main menu, logged in, with no VAC dialog, and
"Failed to load Steam Service" is gone from the client's service_log.
The port's real difficulty was calling-convention ARITY, not vtable slots. x86
__thiscall is callee-cleanup, so every wrong argument count drifts the stack 4
bytes per call; on x86-64 there is no callee cleanup, so the same mistakes are
invisible, which is why they survived. True arity is readable from each
function's "ret 0xN" epilogue (N/4 = stack args), disassembled rather than
byte-scanned. Three were wrong:
IClientEngine::GetIClientUser declared 3 (hUser, hPipe, version string)
actual 2 - there is no version arg; the
body reads only 0x8(%ebp) and 0xc(%ebp)
IClientUser::BUpdateAppOwnershipTicket declared 3 (app, bool, bool)
actual 2 (app, bool)
IClientAppManager::LaunchApp declared 4 (pGameId, opt, src, args)
actual 5 - CGameID goes by VALUE on x86
GetIClientUser was the one that mattered: it corrupted appId before the first
ownership call, so BIsSubscribedApp and the ticket fetch ran against a garbage
app id and execution eventually jumped to address 1.
Vtable constants are now pointer-size-agnostic indices rather than byte offsets,
since the class layouts are identical between builds - one set of constants is
correct for both. Verified against the 32-bit tables: engine rva 0x10278e0,
IClientUser 0xffe8c8, IClientApps 0x1004190, IClientAppManager 0x10044e4.
Also in this change:
- preferLauncherExe, from Bannerlator, whose comments record it as device-proven
on CS:S (cstrike_win64.exe launches insecure, cstrike.exe is VAC-secure).
- log_line gains a gnu_printf format attribute; a %s/argument mismatch added
while making the client DLL name arch-conditional had crashed the agent at
startup, and the attribute makes that class of bug a compile error.
- Per-shortcut Steam launch options now reach LaunchApp via WN_STEAM_USER_ARGS;
they were appended to the agent's command line, which the agent never reads.
- Interface validation plus an engine-vtable dump, so a bad interface pointer
logs a diagnosis instead of calling into garbage.
- The 32-bit agent is selected by PrefManager.wnSteamAgent32 or the marker file
filesDir/.wn_steam_agent_32; prefs are EncryptedSharedPreferences and cannot
be edited over adb, so the marker exists to toggle builds without a rebuild.
Known gaps on the 32-bit path: cloud sync is skipped because the RemoteStorage
indices were derived from the 64-bit table and are unverified, and gamemodules
cannot snapshot a 64-bit game from a 32-bit agent (GLE 299), which is diagnostic
only.
Team Fortress 2 was launching insecure. Steam's configured entry for app 440 is
tf_win64.exe; preferLauncherExe correctly rewrites that to the tf.exe launcher
for process watching, because Steam spawns the launcher and the arch exe is its
child. But the override check ran after the rewrite, so it compared our own
rewrite against Steam's entry, concluded the user had picked a different exe,
and set WN_STEAM_DIRECT_EXE=1 - which tells the agent to skip LaunchApp and
CreateProcess the exe itself, the insecure path with no VAC session.
Device log before:
watch exe 'tf_win64.exe' -> launcher 'tf.exe'
resolved exe 'tf.exe' is not in the family of Steam's configured entry
'tf_win64.exe' - treating as a user override
WN_STEAM_DIRECT_EXE=1 - user-overridden launch exe
after:
LaunchApp(appId=440 launchOption=0) -> EAppUpdateError=0
"tf.exe" is running (secure launch)
The override check and the launch-option lookup now run on the resolved exe,
before preferLauncherExe; only the watched exe is the launcher. A rewrite we
performed ourselves is by definition not a user override.
The family match was also wrong for this pair independently. exeStem refuses to
strip a suffix that would leave fewer than EXE_STEM_MIN_LENGTH characters, which
was 3, so "tf_win64" could not shed "_win64" (remainder "tf" is two) and instead
shed the shorter "64" to give "tf_win", which does not equal "tf". Lowering the
floor to 2 makes tf_win64 -> tf while leaving every other case unchanged,
including the ones the floor exists to protect: x64, 32, game and launcher are
all still returned intact, since a suffix equal in length to the whole name is
skipped before the floor is consulted. Covered by two new tests.
TF2 now reaches its main menu logged in, with the store and friends panels
populated, and no VAC error. Its reported "freeze while loading" is a slow first
load off FUSE-backed external storage - roughly six minutes, during which the
process is demonstrably progressing (rchar climbing ~25 MB/s early on), not
deadlocked.
Brawlhalla shows "INCORRECT VERSION" on WinNative while the identical bytes work under real Steam on desktop, so the question was whether our hosted client fails to serve some interface the game needs. Nothing static could answer that: the game's Steam layer is an AIR native extension that writes no diagnostics, and release Adobe AIR discards trace(). wn-iface-probe.exe replicates exactly what that extension does. It loads the game's own steam_api64.dll, calls SteamInternal_SteamAPI_Init, then SteamInternal_FindOrCreateUserInterface for each interface version string the extension imports, and reports which resolve. The agent runs it after ownership sync and folds its output into wn-launcher.log. It is gated on the marker file C:\wn-iface-probe.on so normal launches are untouched. Device result for Brawlhalla (291550): init returns 0 (OK), HSteamUser=1, and all eleven interfaces the game asks for resolve - Apps008, UserStats012, RemoteStorage016, UGC020, HTTP003, HTMLSurface_005, Screenshots003, RemotePlay002, Music001, MusicRemote001, ParentalSettings001. HTMLSurface mattering was the leading theory, since we ship no steamwebhelper; it is served, and Banner's package ships no steamwebhelper either. So the client-side interface surface is complete and is not the fault. Recorded because this closes a whole class of theories that kept looking plausible. The probe deliberately builds a 64-bit PE even though the agent is now 32-bit: the game is 64-bit and binds steamclient64.dll in its own process, so probing from a 32-bit host would measure the wrong side of the IPC. Note when reading its output: interfaces are named by two different conventions. The extension uses STEAMAPPS_INTERFACE_VERSION008 style, but User/Friends/Utils/ MatchMaking/Networking use SteamUser023 / SteamFriends017 style. Querying the former spelling for the latter group returns NULL and means nothing.
Extends wn-iface-probe to measure what the client actually reports to the game, through the same calls the game makes. Device result for Brawlhalla (291550): ISteamApps::GetAppBuildId() = 24843973 (matches the live public build) ISteamApps::BIsSubscribed() = 1 GetHSteamUser=1 BLoggedOn=1 GetSteamID=76561198448983170 GetAuthSessionTicket -> handle=2 length=234 So the client reports the correct version, the session is logged on as the right account, and a signed auth session ticket is issued. Both were live theories for the "INCORRECT VERSION" dialog and both are now disproven by measurement. Uses the flat C API (SteamAPI_ISteamUser_*) rather than walking vtables. The first attempt indexed the ISteamUser vtable directly and got GetHSteamUser and BLoggedOn right but garbage from GetSteamID; "fixing" that by treating CSteamID as an sret return crashed the probe (0xC0000354). CSteamID is trivially copyable so it comes back in RAX - the original call was ABI-correct and index 2 simply is not GetSteamID on this build. The flat exports take the interface pointer as an ordinary first argument and let the DLL resolve the layout, which removes the guesswork entirely; GetSteamID then returns the real id. Anchor checks earned their keep: they caught the bad layout before the probe called a wrong index with a 2 KB output buffer. GetEncryptedAppTicket reports rc=0 length=0 here, which is expected rather than a finding - that API needs a prior RequestEncryptedAppTicket and a callback round-trip, which this pass does not do. Worth testing properly, since the encrypted app ticket is how a Steam game authenticates to a publisher backend, and app access tokens for this appid are denied by Steam.
Adds the RequestEncryptedAppTicket -> RunCallbacks -> GetEncryptedAppTicket
round-trip to the probe. The encrypted app ticket is how a Steam game
authenticates to a publisher's own backend, which is what Brawlhalla does with
Ubisoft's servers, and it was the last untested Steam-side mechanism behind the
"INCORRECT VERSION" dialog.
Device result: RequestEncryptedAppTicket returns a call handle and
GetEncryptedAppTicket then yields rc=1 length=143 within 500ms. So it works, and
that theory is disproven too. The earlier rc=0 length=0 reading was an artifact
of calling the getter without the request first, not a defect.
The suspicion was that Steam denies app access tokens for this appid
("Requested 17 app access tokens, 0 received, 17 denied" in appinfo_log), and
that the encrypted app ticket would fail for the same reason. It does not - the
denial does not block ticket issuance.
Everything the game asks Steam for is now measured correct: build id 24843973,
BIsSubscribed, logged-on session with the right SteamID, a 234-byte auth session
ticket, a 143-byte encrypted app ticket, and all eleven interfaces served.
Banner's SteamLite v3 handoff pack (agent source, received 2026-08-30) states the cause of Brawlhalla's "INCORRECT VERSION" as: the host fetches app-info app-side and seeds the prefix appcache, then the in-Wine RequestAppInfoUpdate fails because the CM denies PICS access tokens to a steam.exe-replacement agent, and that failed refresh poisons the seeded build id so GetAppBuildId returns 0. His fix is WN_STEAM_SKIP_APPINFO=1. Tested here and that mechanism does not occur on WinNative. The probe now runs immediately before the LaunchApp dispatch rather than right after ownership sync, so it measures what the game will actually see, and it reports GetAppBuildId = 24843973 - the correct live build - both with and without the skip. Our build id is never 0, and setting the flag changed nothing. The flag is therefore not adopted: Banner's own RealSteamLauncher leaves it off by default "so the proven VAC launch path (which relies on the refresh) is unchanged", and enabling it would be an unverified behaviour change for Dota 2 and TF2, which currently work. Probe position matters and is the reason this was worth measuring: the earlier placement ran before the appinfo phase, so a poisoned build id would have been invisible to it. Keep it adjacent to the dispatch. Also adds an optional debug CA import to the probe (gated on C:\wn-mitm-ca.der) for intercepting a game's HTTPS. It does not help for Adobe AIR titles: AIR bundles its own OpenSSL (opensslsocket.cpp / OpenSSL_CryptImpl.cpp strings in Adobe AIR.dll) and compiles in its roots, so it never consults Wine's schannel store. Kept because it is correct for games that do use schannel.
Both come from Banner's SteamLite v3 handoff pack (agent source + host
reference, received 2026-08-30). Diffing it against ours showed only five real
differences; these are the two that could plausibly affect behaviour.
Controller passthrough (RealSteamLauncher.applyControllerPassthrough parity),
applied when staging the prefix:
- delete GameOverlayRenderer.dll / GameOverlayRenderer64.dll from the staged
Steam dir - their FEX HID hook breaks DirectInput
- set SteamController_{XBox,GenericGamepad,PS,Switch}Support to "0" in
userdata/<accountid>/config/localconfig.vdf so Steam Input stops reserving
XInput slots
Measured on device, and the second lever does NOT work here: the in-guest Steam
client rewrites localconfig.vdf during launch and drops the keys. Verified twice
- once written at stage time, once written directly with the app stopped - and
both times the key count went 4 to 0, with the file's mtime moving to ~1 minute
after the write. The first lever is a correct no-op for us since our package
ships no overlay DLLs. Kept because it is right for prefixes that do have them,
but it is not a working passthrough yet; making it stick needs a hook after the
client has loaded its config, not before.
Minimal ACF (SteamLite's ACF shape: no buildid, empty InstalledDepots, just
appid/universe/LauncherPath/name/StateFlags/installdir/LastOwner + language),
gated on the marker C:\wn-minimal-acf.on so it can be A/B'd without a rebuild.
Default is unchanged.
Result is genuinely interesting but not a fix. On the first run Brawlhalla got
PAST the version check for the first time ever - its loading sequence showed
"Launched Client / Loading Game Files 9% / Connecting to Brawlhalla Servers" and
it opened a connection to AWS 34.202.88.182, having previously never gone beyond
api.brawlhalla.com. It then froze. The next two runs gave a hard freeze
(rchar static across 12s) and then INCORRECT VERSION again. So the ACF shape
does influence the version verdict, but the minimal form is unstable and also
lets the client write back buildid=0 and SizeOnDisk=0.
Left disabled by default for that reason. Note this inverts Banner's stated
mechanism: he says buildid=0 causes the error, but here the run that got
furthest was the one with buildid=0.
The preceding commit said the minimal-ACF run "got PAST the version check for
the first time ever" and reached AWS 34.202.88.182 "having previously never gone
beyond api.brawlhalla.com". Both statements are wrong.
The loading checklist that run reached ("Launched Client / Loading Game Files 9%
/ Connecting to Brawlhalla Servers") appears EARLY in startup. The INCORRECT
VERSION dialog appears later - in every baseline run it is drawn over the Code of
Conduct screen, after loading completes. So that run froze during loading and
never reached the version check at all, which is worse than baseline rather than
progress.
34.202.88.182 was likewise not new: it is present in the baseline /proc/net
endpoint capture taken before any of these changes. The real backend
(166.117.x.x, AWS Global Accelerator) was absent both before and after.
So the minimal ACF is a pure regression - two runs froze during loading, the
third reproduced INCORRECT VERSION - and the claim that ACF shape influences the
version verdict is not supported by this evidence. It stays disabled by default.
LaunchApp was returning EAppUpdateError=16 and the launcher silently fell back to CreateProcess, giving an insecure, VAC-ineligible session with no indication to the user. Reverse-engineering steamclient64.dll shows 16 is "Application running": the client still has a game registered, usually left by a session that died without Steam_LogOff, and it refuses to start a second one. 35 is the separate "Other session playing" case. Detect both before dispatching LaunchApp and again on those two errors, surface the blocking game by name, and let the user clear it: ShutdownApp for a stale local registration, BKickOtherPlayingSession for another device. If the user declines or the state cannot be cleared, abort rather than downgrade the launch. Slot indices and calling conventions were taken from the shipped binaries and verified identical for x86 and x64: GetRunningGameID returns CGameID through an sret pointer, and BIsOtherSessionPlaying ends "ret 4", so both are called through WN_THISCALL. Also drop kVtAppMgr_RefreshAppInfo, which pointed at slot 83 RefreshLibraryFolders; no interface in this build has a RefreshAppInfo, so every launch was asking Steam to rescan its library folders right before LaunchApp. The MissingConfig retry now calls IClientApps RequestAppInfoUpdate, which is what the readiness path already used. Replace the hand-written error legend with the 58-entry EAppUpdateError table read out of the client, so launch failures name themselves. docs/steam holds the reverse-engineering reference: 54 interfaces, 2029 slot-indexed methods, and the extraction method.
IClientAppManager slot 21, bool(AppId_t), signature read off the shipped stubs and identical for x86 and x64. It does not discriminate: on device it returns 0 for Brawlhalla, which fails its own version check, and also 0 for Team Fortress 2, which launches and plays. Our installs are a symlink plus a hand-written appmanifest rather than a Steam-managed install, so the client has no depot bookkeeping to compare against and reports everything as out of date. Keep it as a diagnostic line, do not gate a launch on it.
Adds a marker-gated pass to the interface probe that walks Brawlhalla's eight AIR native extensions, loads each one, and reports the result along with the shared libraries they depend on. Load the AIR runtime first and use LOAD_WITH_ALTERED_SEARCH_PATH: every ANE imports from "Adobe AIR.dll", so a plain LoadLibraryA on one fails with ERROR_MOD_NOT_FOUND and all eight look broken when nothing is wrong. On device seven of the eight load, DnaManager among them with its DnaInitializer export present, and every dependency resolves. EpicAir is the exception; it wants EOSSDK-Win64-Shipping.dll, which the Steam build does not ship.
The probe sets SteamAppId and calls SteamAPI_Init, which registers the probe process itself with the Steam client as a running instance of the game. It ran immediately before LaunchApp, so the client correctly refused with EAppUpdateError=16 "Application running" and the launcher fell back to an insecure CreateProcess. Across the captured logs the correlation is exact: every run where the probe reached SteamAPI_Init returned 16 and fell back; every run without it returned 0 and launched secure. The blocking pid reported by the new stale-session prompt was the probe's own. Probe mode is now exclusive - when the marker is present the probe runs and the launch is skipped, because the two cannot share a client session. Also stop an insecure launch presenting as a successful one. The tailer counted "game process started pid=" as terminal, so a CreateProcess fallback fired onLaunchComplete and looked identical to a secure launch. It now raises a warning naming the game, unless the user chose direct-exe mode, where CreateProcess is expected.
The agent bitness was pinned to 32-bit for every container by wnSteamAgent32 defaulting true, with no regard for the prefix. Both prefixes we ship are 64-bit Windows, so the 64-bit agent is the natural match: it loads the same steamclient64.dll the games themselves load rather than driving the client through the 32-bit build over IPC. Device-verified on the arm64ec prefix (ntdll machine 0xaa64): the agent now stages as steam.exe, loads steamclient64.dll at a 64-bit base, takes an app ownership ticket and gets EAppUpdateError=0 with a secure launch for Team Fortress 2. The 32-bit agent remains verified on both the arm64ec and x86-64 prefixes. The pref lives in EncryptedSharedPreferences and can only be reached from the settings UI, which made the 64-bit path untestable from a shell. A .wn_steam_agent_64 marker in filesDir now forces 64-bit, mirroring the existing .wn_steam_agent_32, and takes precedence over both the marker and the pref.
The Steam path forced Wine module/loaddll logging on even when the user had debug logging off, and left steam.exe and steamservice.exe outside the container's CPU affinity policy while every other Wine service was pinned. Recovering the IClientEngine vtable (RVA 0x13151B0) shows all 80 slots: 55 are interface getters that differ only by a cached-pointer field offset, filled by the container Init at 0xA0A650 (per-user) and 0x95779B (per-pipe). Resolving each factory's vtable through RTTI names 49 of the 54 documented interfaces and reproduces all 5 previously known slots unchanged. The other 5 live in steamservice.dll behind its own pipe, which is why their ipc ids collide with client-pipe ones. Adds IClientUser::BGameConnectTokensAvailable to the readiness log: without game-connect tokens a game cannot authenticate to a secure server even when the launch itself is secure.
Valve ships steamservice.dll as i386 only — every copy across every client version available here is machine 0x14c, as are Steam.dll and Steam2.dll, while steamclient/tier0_s/vstdlib_s each ship both. That is the shape of the real product: the client host process is 32-bit, and steamclient64.dll exists to be loaded into 64-bit game processes, which the device logs confirm the game does on its own. So a 64-bit host cannot load the service DLL (GLE 193 ERROR_BAD_EXE_FORMAT), and since the channel to the elevated host is a CCrossProcessPipe the client creates during that same sequence, the pipe cannot be provisioned any other way. The five service-side interfaces are simply unreachable. Measured, agent bitness is the only variable: 32-bit preloads the DLL, 64-bit fails. Reverts the default flipped in 0b96398. The .wn_steam_agent_64 marker still forces the 64-bit agent, now with a documented cost.
"Failed to create Service pipe (GLE 2)" was ours. The client resolves its Steam Service connection from the SteamClientService environment variable, and the bionic-steam block publishes SteamClientService=127.0.0.1:57344 for every Steam launch — an endpoint served by wn-libsteamclient's tcp_services, which Plan W does not run. So the real client dialled a dead port instead of using its own service connection, then failed the seven-slot check with "Failed to connect to Steam Service". Unset it under Plan W. The 32-bit host fix in fd6b34d was necessary but not sufficient: it cleared the GLE 193 load failure and the service now starts and writes its own log, but the client still could not reach it.
…vice port" This reverts commit b0dfa07.
…alls The agent now drives Steam Cloud through Valve's own IClientRemoteStorage instead of the Android-side reimplementation, and several launch-path stalls are gone. Cloud sync - Call EvaluateRemoteStorageSyncState before reading sync state. Without it GetLastKnownSyncState returns kSyncUnknown forever, which is why launch sync never converged. Signature (AppId_t, bool) was derived from the 32-bit steamclient.dll: the vtable stub ends in ret 0x8 and marshals a 4-byte appid plus a 1-byte bool. The previous one-argument call corrupted the stack under __thiscall and faulted. - Give every RemoteStorage vtable typedef WN_THISCALL. On x86 __thiscall passes this in ECX; the plain pointers passed garbage and crashed the launcher before LaunchApp ever ran. - Break out of the settle loop as soon as the state reads synchronized. IsAppSyncInProgress stays true long after the transfer ends and was burning the full 60s budget on exit. - Log the RemoteStorage vtable RVA and IsCloudEnabledForAccount/App so a wrong interface or a disabled app is visible immediately. Launch speed - Run the steam_api genuineness scan on a background thread and join it right before LaunchApp. It walks the whole install tree (56k entries for a 10GB game) and was blocking the critical path for 15.8s. - Drop the post-miss appinfo wait to 400ms for apps that have never delivered AppInfoUpdateComplete_t. - Claim an install script's directories when its hasrunkey says the entry is already installed. Only the run path claimed them, so the redist scan re-ran installers every launch; DXSETUP.exe was failing on each one. Stuck launches - Track whether Steam registers the app as running. If it never registers, the dispatch did not take effect; if it registers and then de-registers without the process appearing, the game started and crashed. Either way stop waiting instead of burning the full 120s. Exit - Raise the launcher shutdown handshake ceiling to 90s so the container is not torn down mid-upload, and give the game 10s to flush its save before the hard kill. - Drive the closing screen from the launcher log: waiting for the game to save, uploading, uploaded. Diagnostics - Timestamp every launcher log line and stamp the build date and arch on the first line, so a stale staged binary is obvious.
nsiproxy was set to demand-start (Start=3) by the service-trimming pass, and at startupSelection 2 it was disabled outright. It is the driver that creates \Device\Nsi, and nothing demand-starts a driver, so nsi.dll's CreateFileW on \\.\Nsi failed and every NSI-backed API returned ERROR_FILE_NOT_FOUND with no results: GetAdaptersInfo, GetAdaptersAddresses, GetIfTable, GetIpAddrTable, GetNetworkParams. Containers looked like machines with no network cards. Set nsiproxy to auto-start and add it to the forced-auto-start list so the aggressive presets cannot disable it, and bump LAUNCH_REGISTRY_POLICY_VERSION so existing containers re-apply instead of skipping on an unchanged stamp. Add libnetshim.so, preloaded ahead of libfakeinput.so so its ioctl hook wins the symbol lookup, forwarding everything else down via RTLD_NEXT. Android denies SIOCGIFHWADDR to app uids (rc=-1), which left every interface at MIB_IF_TYPE_OTHER with a zero-length MAC, so the shim substitutes a stable per-interface address derived from ANDROID_ID and reports lo as ARPHRD_LOOPBACK so GetAdaptersInfo excludes it. getifaddrs and if_nameindex fall back to synthetic entries only when the real ones come back empty. Fix the probe's adapter dump to use IP_ADAPTER_INFO instead of hardcoded 32-bit field offsets, which read garbage in a 64-bit PE and always printed addrlen=0. Measured on device: GetAdaptersInfo goes from rc=2 with 0 adapters to rc=0 with 5, type 6 (ETHERNET), addrlen 6, wlan0 carrying the real LAN address. This does not resolve Brawlhalla's INCORRECT VERSION, which persists with adapters and MACs fully restored; the MAC-derived hardware-id theory is refuted. Filed separately: changeServicesStatus's registry writes do not land, so this policy change is not yet effective without a manual system.reg edit, and winehid and HTTP are affected the same way.
Capturing Brawlhalla's login previously needed a rooted tcpdump, which meant only the phone could be used. netshim already loads into every Wine process, so hooking send/sendto/sendmsg/recv/recvmsg and filtering on peer port via getpeername gives the same visibility with no root and works on the tablet. Gated behind WN_NET_TAP=1 and capped at 400 events. Only the socket calls are hooked, deliberately not read/write, so libfakeinput's evdev path is untouched. First capture, with 5 adapters and real MACs present: OUT 80 14 | 00 00 00 00 | 02 5f | 00 ... 0e "MaxsTechReview" IN 00 10 | 00 23 | 00 20 | "Incorrect Version, Please Update" 0x025f is 607, identical to the pre-MAC-fix body, and the 0x40 presence flag in body byte 1 is still clear. The 16-byte optional field is therefore not MAC-derived - that theory is now refuted by measurement rather than inference.
Brawlhalla is 32-bit and reaches Steam through steam_api.dll, while the existing probe was 64-bit on steam_api64.dll - an untested-different path that could have explained the empty app ticket the DNA gate rejects. Build the probe for both arches, select the DLL by target arch, and have the launcher run each into its own log. Also report ISteamUtils::GetAppID (the pipe's app context) and call GetEncryptedAppTicket once before any RequestEncryptedAppTicket, which is what SteamAir sees if it never issues a request of its own. Result: both arches report app context 291550 and 143 bytes, before and after requesting. The ticket is globally visible once the launcher pre-warm has run, so neither arch nor pipe context nor a missing request explains the gate.
…Cs, add Networking settings Brawlhalla's Steam extension (SteamAir.dll) builds a hardware id from the first network adapter whose MAC passes a validity check and embeds it in the encrypted app ticket it requests from Steam; the game server rejects a login whose ticket lacks it with the generic "Incorrect Version, Please Update". The check drops virtualisation OUIs and any address whose first byte has the multicast or locally-administered bit set. Every MAC libnetshim synthesized began with 0x02, so all adapters were discarded and the login frame was 16 bytes shorter than a desktop client's. Proven by proxying the extension on the working desktop copy and the tablet: the call sequences are identical up to the login, differing only by those 16 bytes, and the validator was read straight out of the disassembly (OUI blacklist 00:50:56 00:05:69 00:0c:29 00:1c:14 00:03:ff 00:0f:4b 00:16:3e, then testb $3 on the first byte). netshim - Derive synthesized MACs from a table of real vendor OUIs plus three hash bytes instead of 02:xx; treat kernel MACs with the multicast or locally-administered bits as unusable. - Honour WN_NET_MAC for the uplink interfaces (wlan*, eth*, rmnet*, ccmni*, usb*) and WN_NET_SHIM=0 to disable every substitution; reject malformed or locally-administered values. - Prefix log lines with milliseconds since load; gate connect/getaddrinfo logging on WN_NET_TAP. Settings - New Networking section in both the container and the shortcut settings: network driver (WinNative Network Driver, on by default for every container, or Wine default) and an optional MAC address with validation and the automatic address shown. Stored as the netDriver/netMac extras with the usual shortcut override semantics and applied at launch through NetworkingSettings.applyEnv. Launcher agent - The launch-time cloud sync drained the callback queue with an unbounded loop; when the client keeps re-posting callbacks (seen after an unclean exit) it never returned and the preloader sat on "Syncing Steam Cloud saves" indefinitely. Cap the drain per tick, log a heartbeat every 5s, and break out as soon as Steam reports the sync disabled, not only synchronized. docs/networking-driver.md describes the driver, the settings and the environment contract.
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
The Steam launcher re-ran VC_redist.x86/x64.exe before LaunchApp (~136s each under translation), freezing the preloader on "Verifying install" even though the VC++ runtime was already seeded. Detect the installed runtime and mark the installscript and redist-scan entries done without running the installer. Map the install/redist phases to preloader text so a real install shows "Installing Microsoft Visual C++ ..." not a stale phase.
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.
No description provided.