Roblox macros that play from outside the game: Anime Vanguards, and a falling-notes piano event.
114 lines
3.9 KiB
Python
114 lines
3.9 KiB
Python
"""The loop every macro runs inside: pause, stop, focus, cleanup."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import time
|
|
|
|
from . import screen, sound, winput
|
|
from .hotkeys import Hotkeys, Signals
|
|
|
|
|
|
class Stopped(Exception):
|
|
"""Raised out of wait() when the stop key is pressed."""
|
|
|
|
|
|
class Runner:
|
|
TICK = 0.02
|
|
|
|
def __init__(self, name: str, keys: dict | None = None, require_focus: bool = True,
|
|
signals: Signals | None = None):
|
|
keys = keys or {}
|
|
self.name = name
|
|
self.require_focus = require_focus
|
|
self.hotkeys = Hotkeys(
|
|
pause=keys.get("pause", "f7"),
|
|
stop=keys.get("stop", "f8"),
|
|
)
|
|
# A window's buttons press the same two things the F-keys do, and they arrive here.
|
|
self.signals = signals or Signals()
|
|
# Whatever the macro wants a front end to be able to show.
|
|
self.stats: dict = {}
|
|
self.paused = True # armed but not running until you say so
|
|
self.stopped = False
|
|
self._said = None
|
|
|
|
# ------------------------------------------------------------ output
|
|
def log(self, message: str) -> None:
|
|
print(f"[{time.strftime('%H:%M:%S')}] {message}", flush=True)
|
|
|
|
def status(self, message: str) -> None:
|
|
"""Log only when it changes - a loop would otherwise print it forever."""
|
|
if message != self._said:
|
|
self._said = message
|
|
self.log(message)
|
|
|
|
# ------------------------------------------------------------- state
|
|
def _handle_controls(self) -> None:
|
|
for pressed in self.hotkeys.poll() + self.signals.poll():
|
|
if pressed == "stop":
|
|
self.stopped = True
|
|
elif pressed == "pause":
|
|
self.paused = not self.paused
|
|
if self.paused:
|
|
winput.release_all()
|
|
sound.stopped()
|
|
else:
|
|
sound.started()
|
|
self.log("paused" if self.paused else "running")
|
|
|
|
def ready(self) -> bool:
|
|
"""Running, not paused, and Roblox is in front."""
|
|
if self.paused:
|
|
return False
|
|
if self.require_focus and not screen.roblox_focused():
|
|
return False
|
|
return True
|
|
|
|
def wait(self, seconds: float) -> None:
|
|
"""Sleep, but stay responsive and never sleep through a stop."""
|
|
remaining = max(float(seconds), 0.0)
|
|
while True:
|
|
self._handle_controls()
|
|
if self.stopped:
|
|
raise Stopped()
|
|
|
|
if self.paused:
|
|
self.status(f"paused — {self.hotkeys.describe()}")
|
|
time.sleep(self.TICK)
|
|
continue
|
|
|
|
if self.require_focus and not screen.roblox_focused():
|
|
if winput.held_keys or winput.held_buttons:
|
|
winput.release_all()
|
|
self.status("waiting for Roblox to be the front window")
|
|
time.sleep(self.TICK)
|
|
continue
|
|
|
|
if remaining <= 0:
|
|
return
|
|
slice_ = min(self.TICK, remaining)
|
|
time.sleep(slice_)
|
|
remaining -= slice_
|
|
|
|
def gate(self) -> None:
|
|
"""Block until running, paused-off and focused. A wait of nothing."""
|
|
self.wait(0)
|
|
|
|
# -------------------------------------------------------------- main
|
|
def loop(self, tick) -> None:
|
|
"""Drive a macro until it stops or the stop key is pressed."""
|
|
self.log(f"{self.name} armed. {self.hotkeys.describe()}")
|
|
if not screen.find_roblox():
|
|
self.log("Roblox is not open — start it and this will pick it up.")
|
|
try:
|
|
while not self.stopped:
|
|
self.gate()
|
|
tick()
|
|
except Stopped:
|
|
sound.alarm()
|
|
self.log("stopped.")
|
|
except KeyboardInterrupt:
|
|
self.log("interrupted.")
|
|
finally:
|
|
winput.release_all()
|