mirror of
https://github.com/opus-tango/public-python.git
synced 2026-07-17 23:22:43 +00:00
main program executes macros (AI)
This commit is contained in:
@@ -1,5 +1,194 @@
|
|||||||
def main():
|
from __future__ import annotations
|
||||||
print("Hello from tfc-forge-macro!")
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from pynput.keyboard import Controller as KeyboardController, Key
|
||||||
|
from pynput.mouse import Button, Controller as MouseController
|
||||||
|
|
||||||
|
DEFAULT_COORDS_PATH = Path(__file__).resolve().parent / "coords.json"
|
||||||
|
|
||||||
|
# Timing tuned for games that poll input each frame: move must settle before click,
|
||||||
|
# and a short pause after mouse-up helps the last click flush before the process exits.
|
||||||
|
MOVE_SETTLE_SEC = 0.02
|
||||||
|
CLICK_HOLD_SEC = 0.03
|
||||||
|
POST_CLICK_SEC = 0.03
|
||||||
|
|
||||||
|
DEFAULT_TEST_SLOTS = ["i1", "i2", "i3", "i4", "i5", "i6", "i7", "i8", "i9", "i1"]
|
||||||
|
|
||||||
|
|
||||||
|
def get_coord(coord_map: dict[str, Any], coord_name: str) -> tuple[float, float]:
|
||||||
|
"""Pixel position for a named slot. tl/br are absolute pixels; others use normalized offsets."""
|
||||||
|
if coord_name in ("tl", "br"):
|
||||||
|
v = coord_map[coord_name]
|
||||||
|
return (float(v[0]), float(v[1]))
|
||||||
|
offsets = coord_map[coord_name]
|
||||||
|
tl = coord_map["tl"]
|
||||||
|
width = coord_map["width"]
|
||||||
|
height = coord_map["height"]
|
||||||
|
x = tl[0] + (offsets[0] * width)
|
||||||
|
y = tl[1] + (offsets[1] * height)
|
||||||
|
return (x, y)
|
||||||
|
|
||||||
|
|
||||||
|
def load_coords(path: Path) -> dict[str, Any]:
|
||||||
|
data: dict[str, Any] = json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
if "anvil" not in data or "recipe" not in data:
|
||||||
|
raise ValueError("coords.json must contain 'anvil' and 'recipe' keys")
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_coord_map(payload: dict[str, Any], name: str) -> dict[str, Any]:
|
||||||
|
"""Return the anvil or recipe sub-map that contains this slot name."""
|
||||||
|
if name in ("width", "height"):
|
||||||
|
raise ValueError(f"{name!r} is metadata, not a clickable slot.")
|
||||||
|
for section in ("anvil", "recipe"):
|
||||||
|
m = payload[section]
|
||||||
|
if name not in m:
|
||||||
|
continue
|
||||||
|
val = m[name]
|
||||||
|
if isinstance(val, (int, float)):
|
||||||
|
raise ValueError(f"{name!r} in {section!r} is not a coordinate pair.")
|
||||||
|
return m
|
||||||
|
raise KeyError(f"Unknown slot {name!r} (not in anvil or recipe).")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_slot_token(token: str) -> tuple[str, bool]:
|
||||||
|
"""Underscore prefix means hold Shift for that click (_i2 -> i2 with shift)."""
|
||||||
|
if token.startswith("_"):
|
||||||
|
return token[1:], True
|
||||||
|
return token, False
|
||||||
|
|
||||||
|
|
||||||
|
def run_sequence(
|
||||||
|
payload: dict[str, Any],
|
||||||
|
slots: list[str],
|
||||||
|
delay_ms: float,
|
||||||
|
move_settle_sec: float = MOVE_SETTLE_SEC,
|
||||||
|
click_hold_sec: float = CLICK_HOLD_SEC,
|
||||||
|
post_click_sec: float = POST_CLICK_SEC,
|
||||||
|
) -> None:
|
||||||
|
mouse = MouseController()
|
||||||
|
keyboard = KeyboardController()
|
||||||
|
|
||||||
|
for i, token in enumerate(slots):
|
||||||
|
name, use_shift = parse_slot_token(token)
|
||||||
|
if not name:
|
||||||
|
raise ValueError(f"Empty slot name in token {token!r}")
|
||||||
|
|
||||||
|
coord_map = resolve_coord_map(payload, name)
|
||||||
|
x, y = get_coord(coord_map, name)
|
||||||
|
xi, yi = int(round(x)), int(round(y))
|
||||||
|
|
||||||
|
mouse.position = (xi, yi)
|
||||||
|
time.sleep(move_settle_sec)
|
||||||
|
|
||||||
|
if use_shift:
|
||||||
|
keyboard.press(Key.shift)
|
||||||
|
time.sleep(move_settle_sec)
|
||||||
|
|
||||||
|
try:
|
||||||
|
mouse.press(Button.left)
|
||||||
|
time.sleep(click_hold_sec)
|
||||||
|
mouse.release(Button.left)
|
||||||
|
finally:
|
||||||
|
if use_shift:
|
||||||
|
keyboard.release(Key.shift)
|
||||||
|
|
||||||
|
time.sleep(post_click_sec)
|
||||||
|
|
||||||
|
if i + 1 < len(slots) and delay_ms > 0:
|
||||||
|
time.sleep(delay_ms / 1000.0)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Move the mouse to slots from coords.json and left-click.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-s",
|
||||||
|
"--start-delay",
|
||||||
|
type=int,
|
||||||
|
default=700,
|
||||||
|
metavar="MS",
|
||||||
|
help="Milliseconds to wait before starting the sequence (default: 1000).",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-d",
|
||||||
|
"--delay",
|
||||||
|
type=int,
|
||||||
|
default=200,
|
||||||
|
metavar="MS",
|
||||||
|
help="Milliseconds to wait after each click before the next (default: 200).",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--move-settle-ms",
|
||||||
|
type=float,
|
||||||
|
default=MOVE_SETTLE_SEC * 1000,
|
||||||
|
metavar="MS",
|
||||||
|
help=(
|
||||||
|
"Milliseconds to wait after moving the cursor before mouse down "
|
||||||
|
f"(default: {MOVE_SETTLE_SEC * 1000:.0f}). Helps clicks register on the new position."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--post-click-ms",
|
||||||
|
type=float,
|
||||||
|
default=POST_CLICK_SEC * 1000,
|
||||||
|
metavar="MS",
|
||||||
|
help=(
|
||||||
|
"Milliseconds to wait after each mouse up (including the last) "
|
||||||
|
f"(default: {POST_CLICK_SEC * 1000:.0f}). Helps the final click flush before exit."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--click-hold-ms",
|
||||||
|
type=float,
|
||||||
|
default=CLICK_HOLD_SEC * 1000,
|
||||||
|
metavar="MS",
|
||||||
|
help=f"Milliseconds to hold the mouse button down (default: {CLICK_HOLD_SEC * 1000:.0f}).",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-c",
|
||||||
|
"--coords-file",
|
||||||
|
type=Path,
|
||||||
|
default=None,
|
||||||
|
help=f"Path to coords.json (default: {DEFAULT_COORDS_PATH}).",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"slots",
|
||||||
|
nargs="*",
|
||||||
|
metavar="SLOT",
|
||||||
|
help="Slot names from coords.json, e.g. M D P G i9 h6. Use _NAME for shift-click.",
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
path = args.coords_file.expanduser().resolve() if args.coords_file else DEFAULT_COORDS_PATH
|
||||||
|
|
||||||
|
if not path.is_file():
|
||||||
|
print(f"Missing coords file: {path}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
slots = args.slots if args.slots else DEFAULT_TEST_SLOTS
|
||||||
|
|
||||||
|
time.sleep(args.start_delay / 1000)
|
||||||
|
|
||||||
|
try:
|
||||||
|
payload = load_coords(path)
|
||||||
|
run_sequence(
|
||||||
|
payload,
|
||||||
|
slots,
|
||||||
|
args.delay,
|
||||||
|
move_settle_sec=args.move_settle_ms / 1000.0,
|
||||||
|
click_hold_sec=args.click_hold_ms / 1000.0,
|
||||||
|
post_click_sec=args.post_click_ms / 1000.0,
|
||||||
|
)
|
||||||
|
except (json.JSONDecodeError, ValueError, KeyError) as e:
|
||||||
|
print(e, file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
Reference in New Issue
Block a user