aboutsummaryrefslogtreecommitdiff
path: root/libs/steam_appinfo_ffi/src
diff options
context:
space:
mode:
authorSulfurNitride <SulfurNitride@users.noreply.github.com>2026-04-17 00:53:20 -0500
committerSulfurNitride <SulfurNitride@users.noreply.github.com>2026-04-17 00:53:20 -0500
commit06c396fd27f55929b40161e587cb9b596b549f3f (patch)
tree40fdb465d2fe6e7d4487bbd237582868c8332459 /libs/steam_appinfo_ffi/src
parente583d6dda3cde9e8bccee313fe1ad69634593708 (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.rs89
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
+}