Windows refuses SetForegroundWindow from a process that does not already own
the foreground. The refusal is silent-ish and was swallowed, so the window never
came forward and every subsequent key and click went to whatever window WAS
focused. Measured on a live client: pressing the inventory key changed 0.89 mean
pixel value (i.e. nothing happened); after the fix, 16.44.
Attach our input queue to the current foreground thread for the duration of the
call — the documented way round the foreground lock — and return True only if
D2R actually ended up foreground, instead of returning True for "no exception
was raised".
Also harden tools/firebolt_roam.py with two preflights, because both failure
modes are silent — the character just stands there:
- refuse to start if D2R cannot be brought to the foreground
- refuse to start if the attack hotkey does not change the right-skill icon,
i.e. no skill is bound to it (--skip-skill-check overrides)
Verified live: with nothing bound to F1 the tool now exits with an actionable
message rather than roaming silently casting nothing.
Co-Authored-By: Claude Opus 5 <[email protected]>
138 lines
5.7 KiB
Python
138 lines
5.7 KiB
Python
"""
|
|
Roam wherever the character is standing and cast one skill at whatever moves.
|
|
|
|
Built for levelling a fresh character in an open area (Blood Moor, Cold Plains).
|
|
Unlike src/run/cold_plains.py this does NOT use the waypoint, the town manager
|
|
or the bot state machine — it starts from wherever the character already is, so
|
|
it needs no waypoints and works at clvl 1.
|
|
|
|
<botty-env-python> tools/firebolt_roam.py --key f1 --minutes 10
|
|
|
|
Put Fire Bolt on that key in game. At clvl 12 put Fireball on the same key and
|
|
change nothing else.
|
|
|
|
Press the stop key (default F12) at any time to stop. Ctrl+C also works.
|
|
|
|
SAFETY: there is no potion, chicken or death handling here. It will happily keep
|
|
casting while dying. Watch it rather than leaving it unattended.
|
|
"""
|
|
import argparse
|
|
import math
|
|
import os
|
|
import random
|
|
import sys
|
|
import time
|
|
|
|
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
sys.path.insert(0, os.path.join(ROOT, "src"))
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser(add_help=True)
|
|
ap.add_argument("--key", default="f1", help="hotkey holding the attack skill (default: f1)")
|
|
ap.add_argument("--button", default="right", choices=["left", "right"],
|
|
help="mouse button the skill casts on (default: right)")
|
|
ap.add_argument("--minutes", type=float, default=10.0, help="stop after this long (default: 10)")
|
|
ap.add_argument("--radius", type=int, default=600, help="ignore targets further than this (px)")
|
|
ap.add_argument("--casts", type=int, default=4, help="casts per engagement before rescanning")
|
|
ap.add_argument("--cast-delay", type=float, default=0.25, help="seconds between casts")
|
|
ap.add_argument("--stop-key", default="f12", help="press to stop (default: f12)")
|
|
ap.add_argument("--skip-skill-check", action="store_true",
|
|
help="don't verify the hotkey actually selects a skill")
|
|
opts = ap.parse_args()
|
|
|
|
import screen
|
|
from config import Config
|
|
from input_layer import keyboard, mouse
|
|
from screen import convert_abs_to_monitor
|
|
from target_detect import get_visible_targets
|
|
from utils.misc import focus_d2r_window, wait
|
|
|
|
screen.find_and_set_window_position(force=True)
|
|
if not screen.get_offset_state():
|
|
print("ERROR: D2R window not found. Is the game running and visible?")
|
|
return 2
|
|
|
|
# Without this every key and click goes to whatever window IS focused and
|
|
# the tool silently does nothing at all — no error, just a character
|
|
# standing still. Refuse to start rather than pretend to work.
|
|
if not focus_d2r_window():
|
|
print("ERROR: could not bring D2R to the foreground — input would go to "
|
|
"another window. Click on D2R and re-run.")
|
|
return 3
|
|
|
|
# Verify a hotkey actually changes the right-skill icon, i.e. the key really
|
|
# has a skill on it. A fresh character has nothing bound, in which case this
|
|
# would roam around casting nothing.
|
|
import numpy as np
|
|
rx, ry, rw, rh = Config().ui_roi["skill_right"]
|
|
icon_before = screen.grab(force_new=True)[ry:ry+rh, rx:rx+rw].astype(np.int16)
|
|
keyboard.send(opts.key)
|
|
wait(0.45, 0.6)
|
|
icon_after = screen.grab(force_new=True)[ry:ry+rh, rx:rx+rw].astype(np.int16)
|
|
if np.abs(icon_after - icon_before).mean() < 2 and not opts.skip_skill_check:
|
|
print(f"ERROR: pressing '{opts.key}' does not change the right-skill "
|
|
f"icon — no skill is bound to it. Assign your attack skill to "
|
|
f"{opts.key.upper()} in game (right-click it on the skill tree), "
|
|
f"or pass --skip-skill-check to override.")
|
|
return 4
|
|
|
|
force_move = Config().char["force_move"]
|
|
deadline = time.time() + opts.minutes * 60
|
|
kills_attempted = 0
|
|
roams = 0
|
|
|
|
print(f"roaming: key={opts.key} button={opts.button} radius={opts.radius} "
|
|
f"for {opts.minutes:g} min — press {opts.stop_key.upper()} to stop")
|
|
|
|
def stopping() -> bool:
|
|
try:
|
|
return keyboard.is_pressed(opts.stop_key)
|
|
except Exception:
|
|
return False
|
|
|
|
try:
|
|
while time.time() < deadline:
|
|
if stopping():
|
|
print("\nstop key pressed")
|
|
break
|
|
|
|
targets = get_visible_targets(radius_max=opts.radius)
|
|
if targets:
|
|
kills_attempted += 1
|
|
pos = targets[0].center_monitor
|
|
keyboard.send(opts.key)
|
|
wait(0.06, 0.12)
|
|
for _ in range(opts.casts):
|
|
if stopping():
|
|
break
|
|
x = pos[0] + random.randint(-8, 8)
|
|
y = pos[1] + random.randint(-8, 8)
|
|
mouse.move(x, y, randomize=5, delay_factor=[0.2, 0.4])
|
|
mouse.click(button=opts.button)
|
|
wait(opts.cast_delay, opts.cast_delay * 1.4)
|
|
else:
|
|
roams += 1
|
|
# y is halved because D2's projection is isometric — an equal
|
|
# x/y step travels visibly further vertically than horizontally.
|
|
angle = random.uniform(0, 2 * math.pi)
|
|
dist = random.randint(150, 260)
|
|
pos_abs = (math.cos(angle) * dist, math.sin(angle) * dist * 0.5)
|
|
mx, my = convert_abs_to_monitor(pos_abs)
|
|
mouse.move(mx, my, randomize=5, delay_factor=[0.2, 0.4])
|
|
keyboard.send(force_move)
|
|
wait(0.9, 1.4)
|
|
|
|
left = max(0, deadline - time.time())
|
|
print(f"\r engagements={kills_attempted} roams={roams} {left:5.0f}s left ",
|
|
end="", flush=True)
|
|
except KeyboardInterrupt:
|
|
print("\ninterrupted")
|
|
|
|
print(f"\ndone: {kills_attempted} engagements, {roams} roam steps")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|