From 2f79acad8dce7503df91c3d3652d2cd49a6c8f78 Mon Sep 17 00:00:00 2001 From: alexpolo1 Date: Tue, 15 Sep 2026 18:03:31 +0200 Subject: [PATCH] fix(restart): a self-restarted bot resumes playing instead of waiting for F11 Both self-restart paths (Bot.restart_or_exit, GameController "could not recover") spawn a replacement process that waited for the resume key like a manual launch. With nobody at the keyboard every self-recovery became a permanent stall - the "bot is stuck all the time" report on 2026-09-15. - Replacement processes get BOTTY_AUTOSTART=1; main.py starts the bot when it is set and D2R is running. A manual launch still waits for the key. - The controller path now shares Bot's restart cap (reset on reaching town), so an unrecoverable screen stops after 5 restarts instead of looping. Co-Authored-By: Claude Opus 5 --- src/bot.py | 6 +++++- src/game_controller.py | 11 +++++++++- src/main.py | 11 ++++++++++ test/test_self_restart_autostart.py | 32 +++++++++++++++++++++++++++++ 4 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 test/test_self_restart_autostart.py diff --git a/src/bot.py b/src/bot.py index c7c6085..ceb0a51 100644 --- a/src/bot.py +++ b/src/bot.py @@ -348,7 +348,11 @@ class Bot: Logger.info(f"Restarting bot — game kept running (attempt {n}/{self._MAX_CONSECUTIVE_RESTARTS}, {delay}s backoff)") time.sleep(delay) import subprocess - subprocess.Popen([sys.executable, os.path.abspath(sys.argv[0])]) + # The replacement must start playing on its own. Without this it sat at + # "press the resume key" with nobody there, so every self-recovery + # became a permanent stall (2026-09-15: idle from 17:50 until noticed). + subprocess.Popen([sys.executable, os.path.abspath(sys.argv[0])], + env={**os.environ, "BOTTY_AUTOSTART": "1"}) os._exit(0) else: Logger.info("Shut down botty") diff --git a/src/game_controller.py b/src/game_controller.py index b338ec4..d426c85 100644 --- a/src/game_controller.py +++ b/src/game_controller.py @@ -156,8 +156,17 @@ class GameController: if max_game_length_reached and not self.game_stats.get_failure_reason(): self.game_stats.set_failure_reason("Max game length reached (stuck)") self.game_stats.log_end_game(failed=max_game_length_reached) + # Same bound as Bot.restart_or_exit (shared counter file, reset when a + # game completes): the replacement auto-starts, so an unrecoverable + # screen must not restart forever. + n = Bot._bump_restart_count() + if n > Bot._MAX_CONSECUTIVE_RESTARTS: + Logger.error(f"Restarted {n - 1}x without completing a game — stopping instead of looping.") + Bot._reset_restart_count() + safe_exit(1) import subprocess, sys, os as _os - subprocess.Popen([sys.executable, _os.path.abspath(sys.argv[0])]) + subprocess.Popen([sys.executable, _os.path.abspath(sys.argv[0])], + env={**_os.environ, "BOTTY_AUTOSTART": "1"}) _os._exit(0) else: Logger.error("Could not recover from a max game length violation. Quitting botty.") diff --git a/src/main.py b/src/main.py index 1d3ef40..4f7de71 100644 --- a/src/main.py +++ b/src/main.py @@ -244,6 +244,17 @@ def main(): keyboard.add_hotkey(Config().advanced_options['resume_key'], lambda: start_or_pause_bot(controllers)) keyboard.add_hotkey(Config().advanced_options["exit_key"], lambda: on_exit(controllers)) + # A self-restart (Bot.restart_or_exit) sets BOTTY_AUTOSTART: resume playing + # instead of waiting for a resume key nobody is there to press. A normal manual + # launch still waits for the key. restart_or_exit's counter bounds the loop. + if os.environ.get("BOTTY_AUTOSTART") == "1": + from utils.restart import process_exists + if os.name != "nt" or process_exists("D2R.exe"): + Logger.info("Auto-starting after a bot self-restart (no resume key needed)") + start_or_pause_bot(controllers) + else: + Logger.warning("Self-restart found D2R not running — launch D2R, then press the resume key") + # Hermes Agent control socket — TCP server on localhost:18899 # Accepts commands: start, pause, stop, status try: diff --git a/test/test_self_restart_autostart.py b/test/test_self_restart_autostart.py new file mode 100644 index 0000000..4c610d0 --- /dev/null +++ b/test/test_self_restart_autostart.py @@ -0,0 +1,32 @@ +"""A bot that restarts itself must resume playing on its own. + +Both self-restart paths spawn a replacement process. That process used to wait for +the resume key like a manual launch, so every self-recovery became a permanent stall +with nobody there to press F11 (2026-09-15). +""" +import inspect + + +def test_bot_restart_passes_autostart_to_replacement(): + from bot import Bot + + src = inspect.getsource(Bot.restart_or_exit) + assert '"BOTTY_AUTOSTART": "1"' in src + + +def test_controller_restart_passes_autostart_and_is_bounded(): + from game_controller import GameController + + src = inspect.getsource(GameController) + assert '"BOTTY_AUTOSTART": "1"' in src + popen = src.index('"BOTTY_AUTOSTART": "1"') + bump = src.rfind("Bot._bump_restart_count()", 0, popen) + assert bump != -1, "controller restart must share Bot's restart cap before spawning" + + +def test_main_autostarts_only_when_flagged(): + import main + + src = inspect.getsource(main.main) + flag = src.index('os.environ.get("BOTTY_AUTOSTART") == "1"') + assert "start_or_pause_bot(controllers)" in src[flag:flag + 600]