diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-game-env b/tools/linuxfs/overlay/usr/local/bin/droiddeck-game-env index 608eefd5f..deeac0965 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-game-env +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-game-env @@ -243,6 +243,59 @@ def ubisoft_overlay_off(env): print("droiddeck-game-env: Ubisoft Connect overlay left as is (%s)" % error, file=sys.stderr) +PZ_JVM_FLAGS = ("-XX:+UseG1GC", "-XX:TieredStopAtLevel=1") + + +def zomboid_vm_args(args): + updated = [arg for arg in args if not re.fullmatch(r"-XX:\+Use\w*GC", arg) or arg == "-XX:+UseG1GC"] + for flag in PZ_JVM_FLAGS: + if flag not in updated: + updated.append(flag) + return updated + + +def zomboid_rewrite(node): + changed = False + if isinstance(node, dict): + for key, value in node.items(): + if key == "vmArgs" and isinstance(value, list) and all(isinstance(arg, str) for arg in value): + updated = zomboid_vm_args(value) + if updated != value: + node[key] = updated + changed = True + elif isinstance(value, dict): + changed |= zomboid_rewrite(value) + return changed + + +def project_zomboid_jvm(command): + if len(command) <= 2: + return + exe = Path(command[2]) + if not exe.stem.lower().startswith("projectzomboid"): + return + settings = exe.with_suffix(".json") + try: + with settings.open(encoding="utf-8-sig") as source: + data = json.load(source) + if not zomboid_rewrite(data): + return + handle, staged = tempfile.mkstemp(prefix=".droiddeck-", dir=settings.parent) + try: + with os.fdopen(handle, "w", encoding="utf-8") as target: + json.dump(data, target, indent=4) + target.write("\n") + os.chmod(staged, settings.stat().st_mode & 0o7777) + os.replace(staged, settings) + except BaseException: + os.unlink(staged) + raise + print("droiddeck-game-env: Project Zomboid: G1 garbage collector and C1 JIT only (ZGC overheats and stalls here)", + file=sys.stderr) + except (OSError, ValueError, AttributeError) as error: + print("droiddeck-game-env: Project Zomboid JVM settings left as is (%s)" % error, file=sys.stderr) + + def offline_without_anticheat(command, env): if (env.get("DROIDDECK_ANTICHEAT_OFFLINE") != "1" or len(command) <= 2 or command[1] != "waitforexitandrun" @@ -931,6 +984,7 @@ def main(): if waiting: command = skip_red_launcher(command) ubisoft_overlay_off(env) + project_zomboid_jvm(command) fixes, extra, notes = engine_fixes(command[2], command[3:]) if len(command) > 2 else ({}, [], []) if not gl_default: fixes.pop("vblank_mode", None) diff --git a/tools/tests/test_game_environment.py b/tools/tests/test_game_environment.py index 1d2ea6441..6d51eef0d 100644 --- a/tools/tests/test_game_environment.py +++ b/tools/tests/test_game_environment.py @@ -2026,6 +2026,51 @@ class DirectAudioPrefixTest(unittest.TestCase): self.assertIsNone(self.link("system32")) +class ProjectZomboidJvmTest(unittest.TestCase): + ARGS = ["-Djava.awt.headless=true", "-Xmx3072m", "-XX:+UseZGC", "-XX:-CreateCoredumpOnCrash"] + + def launch(self, tmp, args=None, stem="ProjectZomboid64"): + settings = Path(tmp) / (stem + ".json") + settings.write_text(json.dumps({"mainClass": "zombie/gameStates/MainScreenState", "vmArgs": args or self.ARGS})) + command = ["proton", "waitforexitandrun", str(Path(tmp) / (stem + ".exe"))] + return settings, command + + def test_zgc_becomes_g1_and_the_c1_jit_flag_is_added_once(self): + with tempfile.TemporaryDirectory() as tmp: + settings, command = self.launch(tmp) + MODULE["project_zomboid_jvm"](command) + data = json.loads(settings.read_text()) + self.assertEqual(data["vmArgs"], ["-Djava.awt.headless=true", "-Xmx3072m", "-XX:-CreateCoredumpOnCrash", + "-XX:+UseG1GC", "-XX:TieredStopAtLevel=1"]) + self.assertEqual(data["mainClass"], "zombie/gameStates/MainScreenState") + before = settings.read_text() + MODULE["project_zomboid_jvm"](command) + self.assertEqual(settings.read_text(), before) + + def test_other_collectors_and_windows_specific_lists_are_rewritten(self): + with tempfile.TemporaryDirectory() as tmp: + settings, command = self.launch(tmp) + settings.write_text(json.dumps({"vmArgs": ["-XX:+UseParallelGC", "-Xmx1g"], + "windows": {"10.0.17134": {"vmArgs": ["-XX:+UseZGC", "-XX:+UseG1GC"]}}})) + MODULE["project_zomboid_jvm"](command) + data = json.loads(settings.read_text()) + flags = ["-XX:+UseG1GC", "-XX:TieredStopAtLevel=1"] + self.assertEqual(data["vmArgs"], ["-Xmx1g"] + flags) + self.assertEqual(data["windows"]["10.0.17134"]["vmArgs"], flags) + + def test_other_games_and_unreadable_files_are_left_alone(self): + with tempfile.TemporaryDirectory() as tmp: + settings, command = self.launch(tmp, stem="Other") + before = settings.read_text() + MODULE["project_zomboid_jvm"](command) + self.assertEqual(settings.read_text(), before) + settings.write_text("{") + MODULE["project_zomboid_jvm"](command[:2] + [str(Path(tmp) / "Other.exe")]) + (Path(tmp) / "ProjectZomboid64.json").write_text("{") + MODULE["project_zomboid_jvm"](command[:2] + [str(Path(tmp) / "ProjectZomboid64.exe")]) + self.assertEqual((Path(tmp) / "ProjectZomboid64.json").read_text(), "{") + + class UbisoftOverlayTest(unittest.TestCase): SETTINGS = ( "language:\n code: en-US\n"