diff --git a/app/src-c/include/metalsharp_backend/steam_actions.h b/app/src-c/include/metalsharp_backend/steam_actions.h index 2423991da..16ffe9b50 100644 --- a/app/src-c/include/metalsharp_backend/steam_actions.h +++ b/app/src-c/include/metalsharp_backend/steam_actions.h @@ -21,6 +21,8 @@ void ms_steam_apply_launch_preferences(const char* home); void ms_steam_deploy_controller_input_shims(const char* home, const char* game_dir); /* Return the app-managed Wine shim that opens requested Wine virtual desktops. */ char* ms_steam_wine_launch_wrapper_path(const char* home); +/* Resolve the actual Witcher 3 game executable, not a storefront launcher. */ +char* ms_witcher3_game_executable(const char* game_dir); char* ms_steam_launch_json(const char*, int*); char* ms_steam_stop_json(const char*, int*); /* Migration-time guarantee: verify/restore steamwebhelper wrapper, bridge shim and Goldberg payloads. */ diff --git a/app/src-c/runtime/epic.c b/app/src-c/runtime/epic.c index 7ec123f86..6006a3d7b 100644 --- a/app/src-c/runtime/epic.c +++ b/app/src-c/runtime/epic.c @@ -1238,6 +1238,11 @@ static char* epic_games_response(const char* home, const ms_json* games, const m *artwork = epic_artwork_url(home, app_name, title, game, &artwork_from_thegamesdb); char* install_path = json_string_field(install, "install_path"); char* executable = json_string_field(install, "executable"); + char* witcher3_executable = ms_witcher3_game_executable(install_path); + if (witcher3_executable) { + free(executable); + executable = witcher3_executable; + } char* pipeline = epic_bottle_field(home, app_name, "preferred_pipeline"); char* mouse_mode = epic_bottle_field(home, app_name, "mouse_mode"); double install_size = 0; @@ -1790,7 +1795,7 @@ char* ms_epic_launch_json(const char* home, const unsigned char* body, size_t bo char *tool = NULL, *wine = NULL, *real_wine = NULL, *wineserver = NULL, *prefix = NULL, *logs = NULL, *log_path = NULL; char* launch_pid_path = NULL; - char *pipeline = NULL, *mouse_mode = NULL, *install_path = NULL; + char *pipeline = NULL, *mouse_mode = NULL, *install_path = NULL, *override_exe = NULL; char* result = NULL; if (!app_name) return epic_failure("invalid Epic app name"); @@ -1821,20 +1826,24 @@ char* ms_epic_launch_json(const char* home, const unsigned char* body, size_t bo goto done; } install_path = epic_installed_game_path(home, app_name); - if (install_path) + if (install_path) { ms_steam_deploy_controller_input_shims(home, install_path); + override_exe = ms_witcher3_game_executable(install_path); + } if (configure_epic_mouse(home, prefix, mouse_mode) != 0) { result = epic_failure("could not apply Epic game mouse settings"); goto done; } static const char* supervisor_script = - "pid_marker=$6; supervisor_pid=$$; " + "pid_marker=$7; supervisor_pid=$$; " "cleanup_pid_marker() { if [ -r \"$pid_marker\" ]; then IFS= read -r stored_pid < \"$pid_marker\"; " "if [ \"$stored_pid\" = \"$supervisor_pid\" ]; then IFS= read -r confirmed_pid < \"$pid_marker\"; " "if [ \"$confirmed_pid\" = \"$supervisor_pid\" ]; then /bin/rm -f \"$pid_marker\"; fi; fi; fi; }; " "trap cleanup_pid_marker EXIT; " - "\"$1\" launch \"$2\" --skip-version-check --wine \"$3\" --wine-prefix \"$4\"; " - "launch_status=$?; WINEPREFIX=\"$4\" \"$5\" -w; exit $launch_status"; + "if [ -n \"$5\" ]; then \"$1\" launch \"$2\" --skip-version-check --wine \"$3\" --wine-prefix \"$4\" " + "--override-exe \"$5\"; else \"$1\" launch \"$2\" --skip-version-check --wine \"$3\" " + "--wine-prefix \"$4\"; fi; " + "launch_status=$?; WINEPREFIX=\"$4\" \"$6\" -w; exit $launch_status"; char* const argv[] = {"/bin/sh", "-c", (char*)supervisor_script, @@ -1843,6 +1852,7 @@ char* ms_epic_launch_json(const char* home, const unsigned char* body, size_t bo app_name, wine, prefix, + override_exe ? override_exe : (char*)"", wineserver, launch_pid_path, NULL}; @@ -1890,6 +1900,7 @@ char* ms_epic_launch_json(const char* home, const unsigned char* body, size_t bo free(pipeline); free(mouse_mode); free(install_path); + free(override_exe); return result; } diff --git a/app/src-c/runtime/gog.c b/app/src-c/runtime/gog.c index b40a6b373..229b6859f 100644 --- a/app/src-c/runtime/gog.c +++ b/app/src-c/runtime/gog.c @@ -651,7 +651,7 @@ static bool spawn_gogdl_download(const char* home, const char* product_id, const } static bool spawn_gogdl_launch(const char* home, const char* product_id, const char* platform, const char* folder, - const char* engine, pid_t* pid_out, char** log_out) { + const char* engine, const char* override_exe, pid_t* pid_out, char** log_out) { char *binary = gogdl_path(home), *log_dir = join(home, "logs/gog"), *wine = ms_steam_wine_launch_wrapper_path(home); if (!wine) wine = join(home, "runtime/wine/bin/metalsharp-wine"); @@ -692,11 +692,25 @@ static bool spawn_gogdl_launch(const char* home, const char* product_id, const c if (support) setenv("GOGDL_SUPPORT_PATH", support, 1); ms_steam_apply_graphics_route(home, engine && *engine ? engine : "auto"); - char* args[] = {binary, "--auth-config-path", auth ? auth : (char*)"", - "launch", (char*)folder, (char*)product_id, - "--platform", (char*)platform, "--wine", - wine, "--wine-prefix", prefix, - NULL}; + char* args[18]; + int count = 0; + args[count++] = binary; + args[count++] = "--auth-config-path"; + args[count++] = auth ? auth : (char*)""; + args[count++] = "launch"; + args[count++] = (char*)folder; + args[count++] = (char*)product_id; + args[count++] = "--platform"; + args[count++] = (char*)platform; + args[count++] = "--wine"; + args[count++] = wine; + args[count++] = "--wine-prefix"; + args[count++] = prefix; + if (override_exe) { + args[count++] = "--override-exe"; + args[count++] = (char*)override_exe; + } + args[count] = NULL; execv(binary, args); _exit(127); } @@ -2001,7 +2015,7 @@ char* ms_gog_action_json(const char* home, const char* action, const unsigned ch } if (!strcmp(action, "play")) { char *folder = NULL, *install_root = NULL, *platform = NULL, *stored_title = NULL; - char* engine = field(j, "engine", "auto"); + char *engine = field(j, "engine", "auto"), *override_exe = NULL; pid_t launch_pid; char* log_path = NULL; if (!gog_launch_record(home, s, &stored_title, &platform, &install_root, &folder)) { @@ -2028,10 +2042,13 @@ char* ms_gog_action_json(const char* home, const char* action, const unsigned ch return err("platform must be windows, osx, or linux"); } char* prefix = join(home, "bottles/gog-prefix/prefix"); - if (!strcmp(platform, "windows")) + if (!strcmp(platform, "windows")) { ms_steam_deploy_controller_input_shims(home, folder); + override_exe = ms_witcher3_game_executable(folder); + } bool started = prefix && mkdir_p(prefix) && - spawn_gogdl_launch(home, s, platform, folder, engine, &launch_pid, &log_path); + spawn_gogdl_launch(home, s, platform, folder, engine, override_exe, &launch_pid, &log_path); + free(override_exe); if (!started) { free(prefix); free(folder); diff --git a/app/src-c/runtime/steam_actions.c b/app/src-c/runtime/steam_actions.c index c7c4633b6..d2eda7e16 100644 --- a/app/src-c/runtime/steam_actions.c +++ b/app/src-c/runtime/steam_actions.c @@ -2370,6 +2370,19 @@ static char* find_named_game_executable(const char* directory, const char* name, return NULL; } +char* ms_witcher3_game_executable(const char* game_dir) { + char* executable; + if (!game_dir || !*game_dir) + return NULL; + executable = join(game_dir, "bin/x64/witcher3.exe"); + if (executable && access(executable, R_OK) == 0) + return executable; + free(executable); + /* Some storefront builds vary path casing; locate the canonical basename + * recursively if the standard relative path is not an exact host match. */ + return find_named_game_executable(game_dir, "witcher3.exe", 0); +} + static char* rule_preferred_game_executable(const char* game_dir, unsigned id) { char* raw = ms_mtsp_default_rules_json(); char error[96]; @@ -2431,7 +2444,13 @@ static char* preferred_steam_game_executable(const char* game_dir, unsigned id, preferred[count++] = "Chameleon/Binaries/Win64/PenguinHotel-Win64-Shipping.exe"; else if (id == 4126040) preferred[count++] = "Aniimo.exe"; - else if (id == 8500) + else if (id == 284160) + preferred[count++] = "Bin64/BeamNG.drive.x64.exe"; + else if (id == 292030) { + char* witcher3 = ms_witcher3_game_executable(game_dir); + if (witcher3) + return witcher3; + } else if (id == 8500) preferred[count++] = "Launcher/evelauncher.exe"; else if (id == 1145360 && pipeline && !strcmp(pipeline, "dxmt_32")) preferred[count++] = "x86/Hades.exe"; diff --git a/app/src-c/tests/runtime_routes_test.c b/app/src-c/tests/runtime_routes_test.c index c72727bb1..2fbc5242d 100644 --- a/app/src-c/tests/runtime_routes_test.c +++ b/app/src-c/tests/runtime_routes_test.c @@ -1112,6 +1112,47 @@ int main(int argc, char** argv) { assert(aoe4_exe && strstr(aoe4_exe, "/aoe4/RelicCardinal.exe")); free(aoe4_exe); free(aoe4_dir); + { + const char* game_root = "beamng"; + static const char* const route_files[] = {"d3d10.dll", "d3d11.dll", "d3d12.dll", "dxgi.dll"}; + char* game_dir = join(home, game_root); + char* executable; + assert(game_dir); + pe_fixture(home, "beamng/BeamNG.drive.exe", 0x014c); + pe_fixture(home, "beamng/Bin64/BeamNG.drive.x64.exe", 0x8664); + executable = preferred_steam_game_executable(game_dir, 284160, "d3dmetal"); + assert(executable && strstr(executable, "/beamng/Bin64/BeamNG.drive.x64.exe")); + for (size_t i = 0; i < sizeof(route_files) / sizeof(route_files[0]); i++) { + char relative[PATH_MAX]; + snprintf(relative, sizeof(relative), "runtime/d3dmetal-gptk4-beta2/wine/x86_64-windows/%s", route_files[i]); + fixture(home, relative, route_files[i]); + } + assert(ms_steam_stage_route_for_executable(home, "d3dmetal", game_dir, executable)); + for (size_t i = 0; i < sizeof(route_files) / sizeof(route_files[0]); i++) { + char relative[PATH_MAX]; + snprintf(relative, sizeof(relative), "beamng/Bin64/%s", route_files[i]); + char* staged = join(home, relative); + assert(staged && access(staged, R_OK) == 0); + free(staged); + } + free(executable); + free(game_dir); + } + { + char* game_dir = join(home, "witcher3"); + char* resolved; + char* steam_selected; + assert(game_dir); + fixture(home, "witcher3/REDlauncher.exe", "storefront launcher"); + fixture(home, "witcher3/bin/x64/witcher3.exe", "actual game executable"); + resolved = ms_witcher3_game_executable(game_dir); + assert(resolved && strstr(resolved, "/witcher3/bin/x64/witcher3.exe")); + steam_selected = preferred_steam_game_executable(game_dir, 292030, "d3dmetal"); + assert(steam_selected && strstr(steam_selected, "/witcher3/bin/x64/witcher3.exe")); + free(resolved); + free(steam_selected); + free(game_dir); + } { char parse_error[128]; char* defaults_json = ms_mtsp_default_rules_json(); diff --git a/app/src-c/tests/smoke.sh b/app/src-c/tests/smoke.sh index a1f5ad569..3f0281d79 100755 --- a/app/src-c/tests/smoke.sh +++ b/app/src-c/tests/smoke.sh @@ -275,6 +275,43 @@ gog_stop=$(curl --silent --fail --request POST --header 'Content-Type: applicati printf '%s' "$gog_stop" | python3 -c 'import json, sys; v=json.load(sys.stdin); g=v["game"]; assert v["ok"] and g["installed"] and not g["running"] and g["status"] == "installed" and g["installRoot"].endswith("/gog-play") and g["gameFolder"].endswith("/gog-play/Game")' gog_after_stop=$(curl --silent --fail "http://127.0.0.1:$port/sharp-library/gog/games") printf '%s' "$gog_after_stop" | python3 -c 'import json, sys; g=json.load(sys.stdin)["games"][0]; assert g["installed"] and not g["running"] and g["status"] == "installed" and g["slug"] == "gog_launch_regression" and g["primaryExe"] == "Game.exe"' +mkdir -p "$home/gog-play/Game/bin/x64" +printf 'Witcher 3 executable fixture' > "$home/gog-play/Game/bin/x64/witcher3.exe" +python3 - <<'PY' +import json +import os +from pathlib import Path +path = Path(os.environ["METALSHARP_HOME"]) / "gog" / "library.json" +library = json.loads(path.read_text()) +library["games"][0]["title"] = "The Witcher 3: Wild Hunt" +path.write_text(json.dumps(library)) +PY +rm -f "$home/gog-launch-args" "$home/gog-launch-env" +gog_witcher=$(curl --silent --fail --request POST --header 'Content-Type: application/json' --data '{"productId":"424242","engine":"auto"}' "http://127.0.0.1:$port/sharp-library/gog/play") +printf '%s' "$gog_witcher" | python3 -c 'import json, sys; v=json.load(sys.stdin); assert v["ok"] and v["game"]["title"] == "The Witcher 3: Wild Hunt"' +i=0 +while [ ! -f "$home/gog-launch-args" ] && [ "$i" -lt 50 ]; do + i=$((i + 1)) + sleep 0.02 +done +python3 - <<'PY' +import os +from pathlib import Path +home = Path(os.environ["METALSHARP_HOME"]) +args = (home / "gog-launch-args").read_text().splitlines() +assert args[args.index("--override-exe") + 1] == str(home / "gog-play/Game/bin/x64/witcher3.exe") +PY +gog_witcher_stop=$(curl --silent --fail --request POST --header 'Content-Type: application/json' --data '{"productId":"424242"}' "http://127.0.0.1:$port/sharp-library/gog/stop") +printf '%s' "$gog_witcher_stop" | python3 -c 'import json, sys; assert json.load(sys.stdin)["ok"]' +python3 - <<'PY' +import json +import os +from pathlib import Path +path = Path(os.environ["METALSHARP_HOME"]) / "gog" / "library.json" +library = json.loads(path.read_text()) +library["games"][0]["title"] = "GOG Launch Regression" +path.write_text(json.dumps(library)) +PY rm -f "$home/gog-launch-args" "$home/gog-launch-env" gog_vkd3d=$(curl --silent --fail --request POST --header 'Content-Type: application/json' --data '{"productId":"424242","engine":"vkd3d"}' "http://127.0.0.1:$port/sharp-library/gog/play") printf '%s' "$gog_vkd3d" | python3 -c 'import json, sys; v=json.load(sys.stdin); assert v["ok"] and v["game"]["title"] == "GOG Launch Regression"' @@ -305,7 +342,7 @@ import os from pathlib import Path home = Path(os.environ["METALSHARP_HOME"]) stops = (home / "wineserver-stop").read_text().splitlines() -assert len(stops) == 2 and all(line == f"-k|{home}/bottles/gog-prefix/prefix" for line in stops) +assert len(stops) == 3 and all(line == f"-k|{home}/bottles/gog-prefix/prefix" for line in stops) PY gog_launch_log=$(printf '%s' "$gog_vkd3d" | python3 -c 'import json, sys; print(json.load(sys.stdin)["logPath"])') i=0 diff --git a/app/src/renderer/App.vue b/app/src/renderer/App.vue index f109fe75e..842699f88 100644 --- a/app/src/renderer/App.vue +++ b/app/src/renderer/App.vue @@ -218,14 +218,26 @@ async function loadLibrary(force = false) { api("GET", "/ubisoft/library", undefined, 120_000), ]); if (lib && Array.isArray(lib.games)) { - const seenNames = new Set(lib.games.map((game) => game.name.trim().toLocaleLowerCase())); - const ubisoftGames = (ubisoft?.games ?? []).filter((game) => { + const ubisoftGames = ubisoft?.games ?? []; + const installedUbisoftNames = new Set( + ubisoftGames + .filter((game) => game.installed) + .map((game) => game.name.trim().toLocaleLowerCase()), + ); + // Prefer an installed Ubisoft copy over an uninstalled Steam ownership + // entry with the same title (e.g. Far Cry 5). Keep Steam precedence if + // the Steam copy is also installed. + const steamGames = lib.games.filter( + (game) => !(game.installed === false && installedUbisoftNames.has(game.name.trim().toLocaleLowerCase())), + ); + const seenNames = new Set(steamGames.map((game) => game.name.trim().toLocaleLowerCase())); + const distinctUbisoftGames = ubisoftGames.filter((game) => { const key = game.name.trim().toLocaleLowerCase(); if (seenNames.has(key)) return false; seenNames.add(key); return true; }); - const games = [...lib.games, ...ubisoftGames]; + const games = [...steamGames, ...distinctUbisoftGames]; library.value = { ...lib, games, diff --git a/docs/compatibility/GAMES-SUPPORTED.md b/docs/compatibility/GAMES-SUPPORTED.md index 4e0cea513..63b8e9ac0 100644 --- a/docs/compatibility/GAMES-SUPPORTED.md +++ b/docs/compatibility/GAMES-SUPPORTED.md @@ -1,6 +1,6 @@ # Games Supported -Updated: 2026-09-28 +Updated: 2026-09-30 Tested and working games organized by launch route. Only games confirmed playable are listed, either by users who use MetalSharp or by the Developer. Many more games likely work beyond this default list. @@ -55,6 +55,7 @@ Games were tested from an external 1TB M.2 SSD (~5000 MB/s over USB-C 3.1) on an | HELLDIVERS 2 | 553850 | Online Play, Medium Settings | | Assassin's Creed Odyssey | 812140 | | | Eve Online | 8500 | Launches through steam with Specific Flags that enable the launcher to load | +| Far Cry 5 | Ubisoft | | | Far Cry 6 | Ubisoft | | | TrackMania | Ubisoft | | | Marvel Rivals | 2767030 | Launches through Steam with -Windowed to avoid a mouse bug |