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"