diff --git a/app/src/main/java/com/droiddeck/launcher/core/GameEnvironmentOptions.kt b/app/src/main/java/com/droiddeck/launcher/core/GameEnvironmentOptions.kt index 078ae8165..3ad45c1fa 100644 --- a/app/src/main/java/com/droiddeck/launcher/core/GameEnvironmentOptions.kt +++ b/app/src/main/java/com/droiddeck/launcher/core/GameEnvironmentOptions.kt @@ -32,6 +32,7 @@ object GameEnvironmentOptions { Option("MESA_EXTENSION_MAX_YEAR", "", 0, Type.TEXT), Option("MESA_GL_VERSION_OVERRIDE", "", 0, Type.TEXT), Option("PULSE_LATENCY_MSEC", "", 0, Type.NUMBER), + Option("DROIDDECK_ANTICHEAT_OFFLINE", "1", R.string.game_env_anticheat_offline, Type.TOGGLE, listOf("0", "1")), Option("WINE_LARGE_ADDRESS_AWARE", "0", 0, Type.TOGGLE, listOf("0", "1")), Option("WINEDLLOVERRIDES", "", 0, Type.TEXT), Option("GALLIUM_HUD", "simple", 0, Type.MULTIPLE, listOf("simple", "fps", "frametime")), diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 109f9f55c..f300f0384 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -86,6 +86,7 @@ Set to 1 to keep this game off droiddeck-fsync; it uses droiddeck-esync where a pack exists, otherwise droiddeck-ntsync. Set to 1 to run this game on droiddeck-fsync, or 0 to put droiddeck-esync first where a pack fits its Proton. Set to 0 to disable Proton’s gamepad-navigation helper for this game. + Set to 1 to start an Easy Anti-Cheat game without its anti-cheat. The game must support offline play; online modes that require Easy Anti-Cheat remain unavailable. DXVK overlay for D3D9–11. fps displays frame rate; unset hides it. DXVK settings separated by semicolons. Recent builds use dxvk.maxFrameRate for frame limiting; prefer the session frame limiter for ordinary play. D3D12 frame-rate limit. 0 disables the limit. diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-game-env b/tools/linuxfs/overlay/usr/local/bin/droiddeck-game-env index 5f937bfee..9f470ebd8 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-game-env +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-game-env @@ -119,6 +119,28 @@ def skip_red_launcher(command): return command +def offline_without_anticheat(command, env): + """Start the configured game executable when the player opts into launching without EAC.""" + if env.get("DROIDDECK_ANTICHEAT_OFFLINE") != "1" or len(command) <= 2: + return command + folder = os.path.dirname(command[2]) + try: + with open(os.path.join(folder, "EasyAntiCheat", "settings.json"), encoding="utf-8-sig") as source: + executable = json.load(source).get("executable") + except (OSError, ValueError, AttributeError): + return command + if not isinstance(executable, str) or not executable: + return command + relative = os.path.normpath(executable.replace("\\", "/")) + game = os.path.join(folder, relative) + if os.path.isabs(relative) or relative.startswith("..") or not os.path.isfile(game): + return command + if os.path.normcase(os.path.abspath(game)) == os.path.normcase(os.path.abspath(command[2])): + return command + print("droiddeck-game-env: started without Easy Anti-Cheat (offline only)", file=sys.stderr) + return command[:2] + [game] + command[3:] + + def engine_fixes(exe, args): """Environment and extra arguments a game needs because of what it is built with.""" game = Game(exe, args) @@ -307,6 +329,8 @@ def main(): env = apply_config(env, {"version": 1}, app_id, fixes) except (OSError, ValueError, TypeError, AttributeError): print("droiddeck-game-env: invalid configuration; using inherited environment", file=sys.stderr) + if app_id and len(command) > 1 and command[1] == "waitforexitandrun": + command = offline_without_anticheat(command, env) if mic_enabled == "0": env["BANNER_AUDIO_DIRECT_MIC"] = "0" env = force_ssbs(command, env) diff --git a/tools/tests/test_game_environment.py b/tools/tests/test_game_environment.py index 26612ec38..dd4ec8bcf 100644 --- a/tools/tests/test_game_environment.py +++ b/tools/tests/test_game_environment.py @@ -179,6 +179,27 @@ class GameEnvironmentTest(unittest.TestCase): (other / "Other.exe").touch() self.assertEqual(MODULE["engine_fixes"](str(other / "Other.exe"), []), ({}, [], [])) + def test_easy_anti_cheat_games_start_offline_only_when_opted_in(self): + with tempfile.TemporaryDirectory() as tmp: + game = Path(tmp) / "ELDEN RING/Game" + (game / "EasyAntiCheat").mkdir(parents=True) + for name in ("start_protected_game.exe", "eldenring.exe"): + (game / name).touch() + (game / "EasyAntiCheat/settings.json").write_text('\ufeff{ "executable" : "eldenring.exe" }', encoding="utf-8") + command = ["proton", "waitforexitandrun", str(game / "start_protected_game.exe"), "-arg"] + offline = MODULE["offline_without_anticheat"] + self.assertEqual(offline(command, {}), command) + self.assertEqual(offline(command, {"DROIDDECK_ANTICHEAT_OFFLINE": "0"}), command) + self.assertEqual(offline(command, {"DROIDDECK_ANTICHEAT_OFFLINE": "1"}), + ["proton", "waitforexitandrun", str(game / "eldenring.exe"), "-arg"]) + (game / "EasyAntiCheat/settings.json").write_text('{ "executable" : "../../escape.exe" }') + (Path(tmp) / "escape.exe").touch() + self.assertEqual(offline(command, {"DROIDDECK_ANTICHEAT_OFFLINE": "1"}), command) + (game / "EasyAntiCheat/settings.json").write_text('{ "executable" : "missing.exe" }') + self.assertEqual(offline(command, {"DROIDDECK_ANTICHEAT_OFFLINE": "1"}), command) + plain = ["proton", "waitforexitandrun", str(Path(tmp) / "Other.exe")] + self.assertEqual(offline(plain, {"DROIDDECK_ANTICHEAT_OFFLINE": "1"}), plain) + def test_arkham_city_gets_full_x87_precision_over_the_preset(self): with tempfile.TemporaryDirectory() as tmp: city = Path(tmp) / "Batman Arkham City GOTY/Binaries/Win32"