diff options
| author | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-04-17 00:53:20 -0500 |
|---|---|---|
| committer | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-04-17 00:53:20 -0500 |
| commit | 06c396fd27f55929b40161e587cb9b596b549f3f (patch) | |
| tree | 40fdb465d2fe6e7d4487bbd237582868c8332459 /libs/steam_appinfo_ffi/src | |
| parent | e583d6dda3cde9e8bccee313fe1ad69634593708 (diff) | |
Symlink saves, rank prefixes by Steam app type, ntsync safeguards
- Replace copy-in/copy-out save deployment with a direct symlink from the
prefix's __MO_Saves to the profile's saves/. Writes land in the profile
immediately; afterRun reverts the symlink + prefix INI so a vanilla
launch outside MO2 uses the default Saves dir.
- Parse Steam's binary appinfo.vdf (v41) via a new Rust FFI crate backed
by steam-vdf-parser. Rank detected games by common.type so real Games
beat Tools/Editors when multiple prefixes share a My Games folder name
(fixes Creation Kit shadowing Skyrim SE). Top-ranked candidate
overwrites stale symlinks from earlier runs.
- Switch bundled Steam Linux Runtime downloader from sniper (steamrt3)
to steamrt4 so Proton 11+ works out of the box. steamrt4 omits xrandr
so we inject it from the Debian x11-xserver-utils .deb into a
dedicated dir and prepend it to PATH via `env PATH=...` inside the
pressure-vessel container (PATH env doesn't propagate).
- Auto-kill stale wineboot/wineserver/pv-adverb processes still bound
to the prefix before starting a new wineboot -u; match by
/proc/<pid>/environ WINEPREFIX / STEAM_COMPAT_DATA_PATH since Wine
cmdlines are Windows-style. Same sweep runs in destroyPrefix so a
delete actually frees file handles.
- Blacklist Proton 11 (Steam-bundled) — its current Wine tree deadlocks
during wineboot -u on modern kernels via ntsync / futex_wait_multiple
races. Use "waitforexitandrun" (not "run") for prefix init so
use_sessions=1 Protons don't fork into a persistent session manager.
- Drop WINEDEBUG=-all during prefix init so wineboot progress is
visible. Suppress focus-stealing by setting WA_ShowWithoutActivating
on the PrefixSetupDialog and SLR download progress dialogs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'libs/steam_appinfo_ffi/src')
| -rw-r--r-- | libs/steam_appinfo_ffi/src/lib.rs | 89 |
1 files changed, 89 insertions, 0 deletions
diff --git a/libs/steam_appinfo_ffi/src/lib.rs b/libs/steam_appinfo_ffi/src/lib.rs new file mode 100644 index 0000000..c134b97 --- /dev/null +++ b/libs/steam_appinfo_ffi/src/lib.rs @@ -0,0 +1,89 @@ +//! C FFI bridge to steam-vdf-parser for reading Steam's appinfo.vdf (binary, +//! including v41 / magic 0x07564429 with string table). +//! +//! Exposes a single function that streams `(appid, type, name)` tuples to a +//! C callback so the C++ side can build its own data structures without +//! needing to know the parser's internals. + +use std::ffi::{CStr, CString, c_char, c_void}; +use std::fs; + +use steam_vdf_parser::parse_appinfo; + +/// Callback invoked once per app. `user` is passed through unchanged. +/// Strings are valid for the duration of the call only — the C++ side must +/// copy whatever it wants to keep. +pub type SteamAppInfoCallback = + extern "C" fn(user: *mut c_void, appid: u32, type_: *const c_char, name: *const c_char); + +/// Parse the appinfo.vdf at `path_c` and call `cb(user, appid, type, name)` +/// for every app whose `common` section is readable. +/// +/// Returns 0 on success, negative on error: +/// -1 = path is null / not valid UTF-8 +/// -2 = file read error +/// -3 = parse error (unsupported magic, truncated, etc.) +/// +/// # Safety +/// `path_c` must be a valid null-terminated C string. `cb` must be non-null. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn steam_appinfo_parse( + path_c: *const c_char, + user: *mut c_void, + cb: SteamAppInfoCallback, +) -> i32 { + if path_c.is_null() { + return -1; + } + let path = match unsafe { CStr::from_ptr(path_c) }.to_str() { + Ok(s) => s, + Err(_) => return -1, + }; + + let data = match fs::read(path) { + Ok(d) => d, + Err(_) => return -2, + }; + + let vdf = match parse_appinfo(&data) { + Ok(v) => v.into_owned(), + Err(_) => return -3, + }; + + let root = match vdf.as_obj() { + Some(o) => o, + None => return -3, + }; + + for (app_id_str, app_value) in root.iter() { + let appid = match app_id_str.parse::<u32>() { + Ok(v) => v, + Err(_) => continue, + }; + + let app_obj = match app_value.as_obj() { + Some(o) => o, + None => continue, + }; + + let common = app_obj + .get("appinfo") + .and_then(|v| v.as_obj()) + .and_then(|appinfo| appinfo.get("common")) + .and_then(|v| v.as_obj()); + + let common = match common { + Some(c) => c, + None => continue, + }; + + let type_ = common.get("type").and_then(|v| v.as_str()).unwrap_or(""); + let name = common.get("name").and_then(|v| v.as_str()).unwrap_or(""); + + let type_c = CString::new(type_).unwrap_or_default(); + let name_c = CString::new(name).unwrap_or_default(); + cb(user, appid, type_c.as_ptr(), name_c.as_ptr()); + } + + 0 +} |
