mirror of
https://github.com/opus-tango/tfc-forge-macro.git
synced 2026-07-17 23:12:44 +00:00
make setup record offsets (AI)
This commit is contained in:
@@ -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 (i1–i27) and hotbar slot (h1–h9).
|
||||||
|
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
|
|
||||||
"tl": (0, 0),
|
|
||||||
"br": (0, 0),
|
|
||||||
"width": 0,
|
|
||||||
"height": 0,
|
|
||||||
|
|
||||||
# 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
|
|
||||||
"i1": (0, 0),
|
|
||||||
"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
|
def _empty_anvil_map() -> dict[str, Any]:
|
||||||
"h1": (0, 0),
|
m: dict[str, Any] = {
|
||||||
"h2": (0, 0),
|
"tl": (0, 0),
|
||||||
"h3": (0, 0),
|
"br": (0, 0),
|
||||||
"h4": (0, 0),
|
"width": 0,
|
||||||
"h5": (0, 0),
|
"height": 0,
|
||||||
"h6": (0, 0),
|
"plans": (0.0, 0.0),
|
||||||
"h7": (0, 0),
|
"weld": (0.0, 0.0),
|
||||||
"h8": (0, 0),
|
"input1": (0.0, 0.0),
|
||||||
"h9": (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
|
||||||
|
|
||||||
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),
|
|
||||||
"br": (0, 0),
|
|
||||||
"width": 0,
|
|
||||||
"height": 0,
|
|
||||||
|
|
||||||
# The offsets for the recipe box
|
def _empty_recipe_map() -> dict[str, Any]:
|
||||||
"rlb": (0, 0), # Left button
|
m: dict[str, Any] = {
|
||||||
"rrb": (0, 0), # Right button
|
"tl": (0, 0),
|
||||||
"r1": (0, 0),
|
"br": (0, 0),
|
||||||
"r2": (0, 0),
|
"width": 0,
|
||||||
"r3": (0, 0),
|
"height": 0,
|
||||||
"r4": (0, 0),
|
"rlb": (0.0, 0.0),
|
||||||
"r5": (0, 0),
|
"rrb": (0.0, 0.0),
|
||||||
"r6": (0, 0),
|
}
|
||||||
"r7": (0, 0),
|
for i in range(1, 19):
|
||||||
"r8": (0, 0),
|
m[f"r{i}"] = (0.0, 0.0)
|
||||||
"r9": (0, 0),
|
return m
|
||||||
"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):
|
|
||||||
if coord_name in ["tl", "br"]:
|
|
||||||
coord_map[coord_name] = (x, y)
|
|
||||||
return
|
|
||||||
|
|
||||||
offset_x = (x - coord_map["tl"][0]) / coord_map["width"]
|
ANVIL_INVENTORY_KEYS = [f"i{i}" for i in range(1, 28)] + [f"h{i}" for i in range(1, 10)]
|
||||||
offset_y = (y - coord_map["tl"][1]) / coord_map["height"]
|
|
||||||
coord_map[coord_name] = (offset_x, offset_y)
|
|
||||||
return
|
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)
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
def _serialize_value(v: Any) -> Any:
|
||||||
|
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
|
||||||
|
self._q.put((int(round(x)), int(round(y))))
|
||||||
|
|
||||||
|
def on_release(self, key: keyboard.Key | keyboard.KeyCode) -> bool | None:
|
||||||
|
if key == keyboard.Key.esc:
|
||||||
|
self._abort.set()
|
||||||
|
self._q.put(None)
|
||||||
|
return False
|
||||||
|
return None
|
||||||
|
|
||||||
|
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()
|
||||||
|
try:
|
||||||
|
with keyboard.Listener(on_release=session.on_release) as kbd_listener:
|
||||||
|
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} "
|
||||||
|
"(i1–i27: main inventory, usually 9×3 left-to-right, top-to-bottom; then h1–h9: 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, r1–r18) 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:
|
||||||
|
mouse_listener.stop()
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
print("Recording clicks (on press). Press Esc to stop.", flush=True)
|
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)
|
||||||
|
|
||||||
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:
|
|
||||||
mouse_listener.stop()
|
|
||||||
return False
|
|
||||||
return None
|
|
||||||
|
|
||||||
mouse_listener = mouse.Listener(on_click=on_click)
|
|
||||||
mouse_listener.start()
|
|
||||||
try:
|
|
||||||
with keyboard.Listener(on_release=on_release) as kbd_listener:
|
|
||||||
kbd_listener.join()
|
|
||||||
finally:
|
|
||||||
mouse_listener.stop()
|
|
||||||
|
|
||||||
|
# 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()
|
||||||
|
|||||||
Reference in New Issue
Block a user