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 <[email protected]>
This commit is contained in:
alexpolo1
2026-09-15 18:03:31 +02:00
co-authored by Claude Opus 5
parent b0d8fdf028
commit 2f79acad8d
4 changed files with 58 additions and 2 deletions
+5 -1
View File
@@ -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")
+10 -1
View File
@@ -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.")
+11
View File
@@ -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:
+32
View File
@@ -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]