Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/src-c/include/metalsharp_backend/steam_actions.h
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down
21 changes: 16 additions & 5 deletions app/src-c/runtime/epic.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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,
Expand All @@ -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};
Expand Down Expand Up @@ -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;
}

Expand Down
35 changes: 26 additions & 9 deletions app/src-c/runtime/gog.c
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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)) {
Expand All @@ -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);
Expand Down
21 changes: 20 additions & 1 deletion app/src-c/runtime/steam_actions.c
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down Expand Up @@ -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";
Expand Down
41 changes: 41 additions & 0 deletions app/src-c/tests/runtime_routes_test.c
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
39 changes: 38 additions & 1 deletion app/src-c/tests/smoke.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"'
Expand Down Expand Up @@ -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
Expand Down
18 changes: 15 additions & 3 deletions app/src/renderer/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -218,14 +218,26 @@ async function loadLibrary(force = false) {
api<SteamLibrary>("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,
Expand Down
3 changes: 2 additions & 1 deletion docs/compatibility/GAMES-SUPPORTED.md
Original file line number Diff line number Diff line change
@@ -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.

Expand Down Expand Up @@ -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 |
Expand Down
Loading