make setup record offsets (AI)

This commit is contained in:
Nayan Sawyer
2026-04-02 17:47:09 -04:00
parent 02bcf183b9
commit 0bea9bb23a

View File

@@ -1,137 +1,216 @@
""" """
Record mouse click coordinates to stdout. Run: python setup.py Interactive coordinate calibration for TFC forge macro.
Run from a terminal: python setup.py
Flow:
1. Anvil GUI: click top-left, then bottom-right of the bounding box (defines tl, br, width, height).
2. Click the center of each main inventory slot (i1i27) and hotbar slot (h1h9).
3. Recipe GUI: same top-left / bottom-right for the recipe panel.
4. Writes coords.json with full anvil and recipe maps (recipe button offsets stay 0 until you edit or extend this script).
Press Esc to abort at any time.
""" """
from __future__ import annotations from __future__ import annotations
import json
import sys
import threading
from pathlib import Path
from queue import Queue
from typing import Any
from pynput import keyboard, mouse from pynput import keyboard, mouse
anvil_coord_map = { DEFAULT_OUTPUT = Path(__file__).resolve().parent / "coords.json"
# The top left and bottom right corners of the anvil box
# These are to calculate the rest of the coordinates
def _empty_anvil_map() -> dict[str, Any]:
m: dict[str, Any] = {
"tl": (0, 0), "tl": (0, 0),
"br": (0, 0), "br": (0, 0),
"width": 0, "width": 0,
"height": 0, "height": 0,
"plans": (0.0, 0.0),
"weld": (0.0, 0.0),
"input1": (0.0, 0.0),
"input2": (0.0, 0.0),
"L": (0.0, 0.0),
"M": (0.0, 0.0),
"D": (0.0, 0.0),
"P": (0.0, 0.0),
"G": (0.0, 0.0),
"Y": (0.0, 0.0),
"O": (0.0, 0.0),
"R": (0.0, 0.0),
}
for i in range(1, 28):
m[f"i{i}"] = (0.0, 0.0)
for i in range(1, 10):
m[f"h{i}"] = (0.0, 0.0)
return m
# The offsets for the anvil
"plans": (0, 0),
"weld": (0, 0),
"input1": (0, 0),
"input2": (0, 0),
"L": (0, 0),
"M": (0, 0),
"D": (0, 0),
"P": (0, 0),
"G": (0, 0),
"Y": (0, 0),
"O": (0, 0),
"R": (0, 0),
# The offsets for inventory slots def _empty_recipe_map() -> dict[str, Any]:
"i1": (0, 0), m: dict[str, Any] = {
"i2": (0, 0),
"i3": (0, 0),
"i4": (0, 0),
"i5": (0, 0),
"i6": (0, 0),
"i7": (0, 0),
"i8": (0, 0),
"i9": (0, 0),
"i10": (0, 0),
"i11": (0, 0),
"i12": (0, 0),
"i13": (0, 0),
"i14": (0, 0),
"i15": (0, 0),
"i16": (0, 0),
"i17": (0, 0),
"i18": (0, 0),
"i19": (0, 0),
"i20": (0, 0),
"i21": (0, 0),
"i22": (0, 0),
"i23": (0, 0),
"i24": (0, 0),
"i25": (0, 0),
"i26": (0, 0),
"i27": (0, 0),
# The offsets for the hotbar slots
"h1": (0, 0),
"h2": (0, 0),
"h3": (0, 0),
"h4": (0, 0),
"h5": (0, 0),
"h6": (0, 0),
"h7": (0, 0),
"h8": (0, 0),
"h9": (0, 0),
}
recipe_coord_map = {
# The top left and bottom right corners of the recipe box
# These are to calculate the rest of the coordinates
"tl": (0, 0), "tl": (0, 0),
"br": (0, 0), "br": (0, 0),
"width": 0, "width": 0,
"height": 0, "height": 0,
"rlb": (0.0, 0.0),
"rrb": (0.0, 0.0),
}
for i in range(1, 19):
m[f"r{i}"] = (0.0, 0.0)
return m
# The offsets for the recipe box
"rlb": (0, 0), # Left button
"rrb": (0, 0), # Right button
"r1": (0, 0),
"r2": (0, 0),
"r3": (0, 0),
"r4": (0, 0),
"r5": (0, 0),
"r6": (0, 0),
"r7": (0, 0),
"r8": (0, 0),
"r9": (0, 0),
"r10": (0, 0),
"r11": (0, 0),
"r12": (0, 0),
"r13": (0, 0),
"r14": (0, 0),
"r15": (0, 0),
"r16": (0, 0),
"r17": (0, 0),
"r18": (0, 0),
}
def set_coord(coord_map, coord_name, x, y): ANVIL_INVENTORY_KEYS = [f"i{i}" for i in range(1, 28)] + [f"h{i}" for i in range(1, 10)]
if coord_name in ["tl", "br"]:
def apply_top_left_bottom_right(
coord_map: dict[str, Any], tl_corner: tuple[int, int], br_corner: tuple[int, int]
) -> None:
"""Set tl, br, width, height from top-left and bottom-right corners."""
if br_corner[0] <= tl_corner[0] or br_corner[1] <= tl_corner[1]:
raise ValueError(
f"Invalid box: top-left {tl_corner}, bottom-right {br_corner}. "
"Bottom-right should be farther right (larger x) and lower down (larger y) than top-left."
)
coord_map["tl"] = (tl_corner[0], tl_corner[1])
coord_map["br"] = (br_corner[0], br_corner[1])
coord_map["width"] = int(br_corner[0] - tl_corner[0])
coord_map["height"] = int(br_corner[1] - tl_corner[1])
def set_offset(coord_map: dict[str, Any], coord_name: str, x: int, y: int) -> None:
"""Store normalized offset relative to tl and box size (matches main.get_coord)."""
if coord_name in ("tl", "br"):
coord_map[coord_name] = (x, y) coord_map[coord_name] = (x, y)
return return
w = coord_map["width"]
h = coord_map["height"]
if w <= 0 or h <= 0:
raise ValueError("width and height must be set and positive before recording offsets")
tl = coord_map["tl"]
ox = (x - tl[0]) / w
oy = (y - tl[1]) / h
coord_map[coord_name] = (ox, oy)
offset_x = (x - coord_map["tl"][0]) / coord_map["width"]
offset_y = (y - coord_map["tl"][1]) / coord_map["height"] def _serialize_value(v: Any) -> Any:
coord_map[coord_name] = (offset_x, offset_y) if isinstance(v, tuple):
return [_serialize_value(x) for x in v]
if isinstance(v, float):
return round(v, 8)
return v
def coord_map_to_json_dict(coord_map: dict[str, Any]) -> dict[str, Any]:
return {k: _serialize_value(v) for k, v in coord_map.items()}
class _ClickSession:
def __init__(self) -> None:
self._q: Queue[tuple[int, int] | None] = Queue()
self._abort = threading.Event()
def on_click(self, x: float, y: float, button: mouse.Button, pressed: bool) -> None:
if not pressed or button != mouse.Button.left or self._abort.is_set():
return return
self._q.put((int(round(x)), int(round(y))))
def on_release(self, key: keyboard.Key | keyboard.KeyCode) -> bool | None:
def main() -> None:
print("Recording clicks (on press). Press Esc to stop.", flush=True)
def on_click(x: float, y: float, button: mouse.Button, pressed: bool) -> None:
if pressed:
print(f"{int(round(x))}\t{int(round(y))}\t{button}", flush=True)
def on_release(key: keyboard.Key | keyboard.KeyCode) -> bool | None:
if key == keyboard.Key.esc: if key == keyboard.Key.esc:
mouse_listener.stop() self._abort.set()
self._q.put(None)
return False return False
return None return None
mouse_listener = mouse.Listener(on_click=on_click) def wait_click(self, prompt: str) -> tuple[int, int]:
print(prompt, flush=True)
item = self._q.get()
if item is None:
raise SystemExit("Aborted (Esc).")
return item
def run_setup(output_path: Path) -> None:
anvil = _empty_anvil_map()
recipe = _empty_recipe_map()
session = _ClickSession()
mouse_listener = mouse.Listener(on_click=session.on_click)
mouse_listener.start() mouse_listener.start()
try: try:
with keyboard.Listener(on_release=on_release) as kbd_listener: with keyboard.Listener(on_release=session.on_release) as kbd_listener:
kbd_listener.join() print(
"=== Anvil GUI ===\n"
"Open the anvil / forge screen. You will define the full GUI rectangle, then inventory slots.\n",
flush=True,
)
tl_pt = session.wait_click("1/2 Click the TOP-LEFT corner of the anvil GUI (outer box).")
br_pt = session.wait_click("2/2 Click the BOTTOM-RIGHT corner of the same GUI box.")
apply_top_left_bottom_right(anvil, tl_pt, br_pt)
print(
f" Box: tl={anvil['tl']} br={anvil['br']} size={anvil['width']}x{anvil['height']}\n",
flush=True,
)
total = len(ANVIL_INVENTORY_KEYS)
for n, key in enumerate(ANVIL_INVENTORY_KEYS, start=1):
xy = session.wait_click(
f"Inventory {n}/{total}: click the center of slot {key} "
"(i1i27: main inventory, usually 9×3 left-to-right, top-to-bottom; then h1h9: hotbar)."
)
set_offset(anvil, key, xy[0], xy[1])
print(
"\n=== Recipe GUI ===\n"
"Open the recipe selector panel (or the screen where it appears). Same corner convention.\n",
flush=True,
)
rtl = session.wait_click("1/2 Click the TOP-LEFT corner of the recipe GUI box.")
rbr = session.wait_click("2/2 Click the BOTTOM-RIGHT corner of the recipe GUI box.")
apply_top_left_bottom_right(recipe, rtl, rbr)
print(
f" Box: tl={recipe['tl']} br={recipe['br']} size={recipe['width']}x{recipe['height']}\n",
flush=True,
)
print(
"Recipe controls (rlb, rrb, r1r18) are left at [0,0] in the file; calibrate them later if needed.\n",
flush=True,
)
payload = {
"anvil": coord_map_to_json_dict(anvil),
"recipe": coord_map_to_json_dict(recipe),
}
output_path.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
print(f"Wrote {output_path}", flush=True)
finally: finally:
mouse_listener.stop() mouse_listener.stop()
def main() -> None:
out = DEFAULT_OUTPUT
if len(sys.argv) > 1:
out = Path(sys.argv[1]).expanduser().resolve()
try:
run_setup(out)
except SystemExit as e:
print(str(e), flush=True)
raise
except ValueError as e:
print(f"Error: {e}", flush=True)
sys.exit(1)
# Backwards-compatible names for any imports
anvil_coord_map = _empty_anvil_map()
recipe_coord_map = _empty_recipe_map()
set_coord = set_offset
if __name__ == "__main__": if __name__ == "__main__":
main() main()