Compare commits

..

3 Commits

Author SHA1 Message Date
Nayan Sawyer
6c1bfbf0c4 remove tfc forge macro 2026-04-04 16:48:49 -04:00
Nayan Sawyer
0e455a2e40 add gitignore 2026-04-04 16:48:34 -04:00
Nayan Sawyer
28d8abcfff add proper readme for forge macro 2026-04-04 16:41:36 -04:00
11 changed files with 2 additions and 882 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
__pycache__/
.venv/

View File

@@ -1 +0,0 @@
__pycache__/

View File

@@ -1 +0,0 @@
3.12

View File

@@ -1 +0,0 @@
This project is for setting up a macro program to automate forging in terrafirmacraft.

View File

@@ -1,82 +0,0 @@
{
"anvil": {
"tl": [1020, 402],
"br": [1542, 1018],
"width": 522,
"height": 616,
"plans": [0.16283525, 0.2288961],
"weld": [0.82950192, 0.2288961],
"input1": [0.11877395, 0.36688312],
"input2": [0.22988506, 0.37175325],
"L": [0.34482759, 0.28246753],
"M": [0.45210728, 0.27597403],
"D": [0.35057471, 0.35876623],
"P": [0.45977011, 0.35714286],
"G": [0.56321839, 0.26785714],
"Y": [0.65900383, 0.26948052],
"O": [0.53448276, 0.37175325],
"R": [0.651341, 0.35876623],
"i1": [0.09003831, 0.64285714],
"i2": [0.19530147, 0.64285714],
"i3": [0.30056463, 0.64285714],
"i4": [0.40582779, 0.64285714],
"i5": [0.51109095, 0.64285714],
"i6": [0.6163541, 0.64285714],
"i7": [0.72161726, 0.64285714],
"i8": [0.82688042, 0.64285714],
"i9": [0.93214358, 0.64285714],
"i10": [0.09003831, 0.73376623],
"i11": [0.19530147, 0.73376623],
"i12": [0.30056463, 0.73376623],
"i13": [0.40582779, 0.73376623],
"i14": [0.51109095, 0.73376623],
"i15": [0.6163541, 0.73376623],
"i16": [0.72161726, 0.73376623],
"i17": [0.82688042, 0.73376623],
"i18": [0.93214358, 0.73376623],
"i19": [0.09003831, 0.82467532],
"i20": [0.19530147, 0.82467532],
"i21": [0.30056463, 0.82467532],
"i22": [0.40582779, 0.82467532],
"i23": [0.51109095, 0.82467532],
"i24": [0.6163541, 0.82467532],
"i25": [0.72161726, 0.82467532],
"i26": [0.82688042, 0.82467532],
"i27": [0.93214358, 0.82467532],
"h1": [0.09003831, 0.91558442],
"h2": [0.19530147, 0.91558442],
"h3": [0.30056463, 0.91558442],
"h4": [0.40582779, 0.91558442],
"h5": [0.51109095, 0.91558442],
"h6": [0.6163541, 0.91558442],
"h7": [0.72161726, 0.91558442],
"h8": [0.82688042, 0.91558442],
"h9": [0.93214358, 0.91558442]
},
"recipe": {
"tl": [1018, 459],
"br": [1543, 954],
"width": 525,
"height": 495,
"rlb": [0.06857143, 0.37575758],
"rrb": [0.94285714, 0.37979798],
"r1": [0.09714286, 0.15151515],
"r2": [0.20240602, 0.15151515],
"r3": [0.30766917, 0.15151515],
"r4": [0.41293233, 0.15151515],
"r5": [0.51819549, 0.15151515],
"r6": [0.62345865, 0.15151515],
"r7": [0.7287218, 0.15151515],
"r8": [0.83398496, 0.15151515],
"r9": [0.93924812, 0.15151515],
"r10": [0.09714286, 0.27127563],
"r11": [0.20240602, 0.27127563],
"r12": [0.30766917, 0.27127563],
"r13": [0.41293233, 0.27127563],
"r14": [0.51819549, 0.27127563],
"r15": [0.62345865, 0.27127563],
"r16": [0.7287218, 0.27127563],
"r17": [0.83398496, 0.27127563],
"r18": [0.93924812, 0.27127563]
}
}

View File

@@ -1,225 +0,0 @@
from __future__ import annotations
import argparse
import json
import re
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_slots_file(content: str) -> list[str]:
"""Split slot names on commas, spaces, tabs, and newlines; empty tokens are dropped."""
return [t for t in re.split(r"[\s,]+", content.strip()) if t]
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(
"-f",
"--file",
type=Path,
default=None,
metavar="PATH",
help=(
"Read slot names from this file (commas, spaces, and newlines separate tokens). "
"When set, positional SLOT arguments are ignored."
),
)
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. "
"Ignored when --file is set."
),
)
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)
if args.file is not None:
slots_path = args.file.expanduser().resolve()
if not slots_path.is_file():
print(f"Missing slots file: {slots_path}", file=sys.stderr)
sys.exit(1)
slots = parse_slots_file(slots_path.read_text(encoding="utf-8"))
if not slots:
print("Slots file is empty or contains no slot names.", file=sys.stderr)
sys.exit(1)
else:
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__":
main()

View File

@@ -1,9 +0,0 @@
[project]
name = "tfc-forge-macro"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"pynput>=1.7.6",
]

View File

@@ -1,19 +0,0 @@
Set sh = CreateObject("WScript.Shell")
Set fso = CreateObject("Scripting.FileSystemObject")
sh.CurrentDirectory = fso.GetParentFolderName(WScript.ScriptFullName)
Function QuoteForCmd(s)
QuoteForCmd = """" & Replace(s, """", """""") & """"
End Function
Dim extra, i
extra = ""
For i = 0 To WScript.Arguments.Count - 1
If Len(extra) > 0 Then extra = extra & " "
extra = extra & QuoteForCmd(WScript.Arguments(i))
Next
Dim cmdLine
cmdLine = "cmd /c uv run main.py"
If Len(extra) > 0 Then cmdLine = cmdLine & " " & extra
sh.Run cmdLine, 0, False

View File

@@ -1,45 +0,0 @@
_i1
R R O Y L L L
R R Y Y Y L L L
-input2
+q
_i2
R R O Y L L L
R R Y Y Y L L L
-input2
+q
_i3
R R O Y L L L
R R Y Y Y L L L
-input2
+q
_i4
R R O Y L L L
R R Y Y Y L L L
-input2
+q
_i5
R R O Y L L L
R R Y Y Y L L L
-input2
+q
_i6
R R O Y L L L
R R Y Y Y L L L
-input2
+q
_i7
R R O Y L L L
R R Y Y Y L L L
-input2
+q
_i8
R R O Y L L L
R R Y Y Y L L L
-input2
+q
_i9
R R O Y L L L
R R Y Y Y L L L
-input2
+q

View File

@@ -1,359 +0,0 @@
"""
Interactive coordinate calibration for TFC forge macro.
Run from a terminal: python setup.py
Flow:
1. Without -f/--full: requires an existing coords file (default coords.json). Only tl, br, width, and
height are updated for the anvil and recipe sections; all other keys are preserved.
If no coords file exists yet, run with -f/--full first.
2. With -f/--full: full calibration from scratch (empty maps).
3. Anvil GUI: click top-left, then bottom-right of the bounding box (defines tl, br, width, height).
4. With -f/--full: click only the first main inventory slot (i1); i2i27 and h1h9 are filled from grid steps (see SLOT_STEP_X / INV_ROW_STEP_Y below).
5. Recipe GUI: same top-left / bottom-right for the recipe panel.
6. With -f/--full: click the first recipe slot (r1); r2r18 are filled from grid steps; scroll buttons rlb/rrb are still clicked manually.
7. Writes coords.json. With -f/--full, every named offset is recorded.
Press Esc to abort at any time.
"""
from __future__ import annotations
import argparse
import copy
import json
import sys
import threading
from pathlib import Path
from queue import Queue
from typing import Any
from pynput import keyboard, mouse
DEFAULT_OUTPUT = Path(__file__).resolve().parent / "coords.json"
# Normalized grid steps (fraction of GUI width / height). One row = 9 slots left-to-right;
# inventory is 4 rows (i1i27 then hotbar h1h9); recipe panel is 2 rows (r1r18).
SLOT_STEP_X = 1.0 / 9.5
INV_ROW_STEP_Y = 1.0 / 11.0
RECIPE_ROW_STEP_Y = 1.0 / 8.35
def _empty_anvil_map() -> dict[str, Any]:
m: dict[str, Any] = {
"tl": (0, 0),
"br": (0, 0),
"width": 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
def _empty_recipe_map() -> dict[str, Any]:
m: dict[str, Any] = {
"tl": (0, 0),
"br": (0, 0),
"width": 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
# Normalized offsets relative to anvil GUI box (after tl/br/width/height are set).
ANVIL_UI_OFFSET_KEYS = [
"weld",
"input1",
"input2",
"L",
"M",
"D",
"P",
"G",
"Y",
"O",
"R",
]
RECIPE_SCROLL_KEYS = ["rrb","rlb"]
def fill_anvil_inventory_from_i1(coord_map: dict[str, Any]) -> None:
"""Fill i1i27 and h1h9 from i1's normalized offset and SLOT_STEP_X / INV_ROW_STEP_Y."""
ox0, oy0 = coord_map["i1"]
for k in range(1, 28):
idx = k - 1
row, col = idx // 9, idx % 9
coord_map[f"i{k}"] = (ox0 + col * SLOT_STEP_X, oy0 + row * INV_ROW_STEP_Y)
for k in range(1, 10):
col = k - 1
coord_map[f"h{k}"] = (ox0 + col * SLOT_STEP_X, oy0 + 3 * INV_ROW_STEP_Y)
def fill_recipe_slots_from_r1(coord_map: dict[str, Any]) -> None:
"""Fill r1r18 from r1's normalized offset and SLOT_STEP_X / RECIPE_ROW_STEP_Y."""
ox0, oy0 = coord_map["r1"]
for k in range(1, 19):
idx = k - 1
row, col = idx // 9, idx % 9
coord_map[f"r{k}"] = (ox0 + col * SLOT_STEP_X, oy0 + row * RECIPE_ROW_STEP_Y)
_ANVIL_UI_LABELS: dict[str, str] = {
"weld": "weld button",
"input1": "first anvil input slot",
"input2": "second anvil input slot",
"L": "Light Hit",
"M": "Medium Hit",
"D": "Heavy Hit",
"P": "Draw",
"G": "Punch",
"Y": "Bend",
"O": "Upset",
"R": "Shrink",
}
def _recipe_offset_label(key: str) -> str:
if key == "rlb":
return "recipe panel left scroll / page button"
if key == "rrb":
return "recipe panel right scroll / page button"
return f"recipe slot {key}"
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()}
def load_existing_coord_maps(path: Path) -> tuple[dict[str, Any], dict[str, Any]]:
"""Load anvil and recipe maps from JSON for rectangle-only updates. Deep-copied for mutation."""
try:
raw = json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as e:
raise ValueError(f"Could not read coords file {path}: {e}") from e
if not isinstance(raw, dict) or "anvil" not in raw or "recipe" not in raw:
raise ValueError(f"{path} must be a JSON object with 'anvil' and 'recipe' keys")
anvil = raw["anvil"]
recipe = raw["recipe"]
if not isinstance(anvil, dict) or not isinstance(recipe, dict):
raise ValueError("'anvil' and 'recipe' must be JSON objects")
return copy.deepcopy(anvil), copy.deepcopy(recipe)
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, full: bool) -> None:
if full:
anvil = _empty_anvil_map()
recipe = _empty_recipe_map()
else:
if not output_path.is_file():
raise SystemExit(
f"No coords file at {output_path}. Run with -f/--full once to record all offsets, "
"then use this mode to refresh only GUI rectangles after resolution or UI scale changes.\n"
f"Example: python {Path(__file__).name} -f"
)
anvil, recipe = load_existing_coord_maps(output_path)
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:
intro_anvil = (
"=== Anvil GUI ===\n"
"Open the anvil / forge screen. You will define the full GUI outer rectangle"
+ (" then every clickable offset.\n" if full else ".\n")
)
print(intro_anvil, 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,
)
if full:
ui_total = len(ANVIL_UI_OFFSET_KEYS)
for n, key in enumerate(ANVIL_UI_OFFSET_KEYS, start=1):
label = _ANVIL_UI_LABELS.get(key, key)
xy = session.wait_click(
f"Anvil UI {n}/{ui_total} ({key}): click the center of the {label}."
)
set_offset(anvil, key, xy[0], xy[1])
xy_i1 = session.wait_click(
"Inventory: click the center of slot i1 (top-left of the 9×3 main grid). "
"i2i27 and h1h9 are filled using horizontal step 1/9.5 and vertical 1/11 per row (4 rows total including hotbar)."
)
set_offset(anvil, "i1", xy_i1[0], xy_i1[1])
fill_anvil_inventory_from_i1(anvil)
session.wait_click("Pickup your ingot to move to the second input slot.")
session.wait_click("Move the ingot to the second input slot.")
plansclick = session.wait_click("Click the PLAN button of the anvil GUI box.")
set_offset(anvil, "plans", plansclick[0], plansclick[1])
print(
"\n=== Recipe GUI ===\n"
"Open the recipe selector panel (or the screen where it appears). Same corner convention.\n",
flush=True,
)
if not full:
session.wait_click("Click the OPEN button of the recipe GUI box.")
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,
)
if full:
for n, key in enumerate(RECIPE_SCROLL_KEYS, start=1):
xy = session.wait_click(
f"Recipe scroll {n}/{len(RECIPE_SCROLL_KEYS)} ({key}): "
f"click the center of the {_recipe_offset_label(key)}."
)
set_offset(recipe, key, xy[0], xy[1])
xy_r1 = session.wait_click(
"Recipe: click the center of slot r1 (top-left of the recipe grid). "
"r2r18 use horizontal step 1/9.5 and vertical 1/8.35 per row."
)
set_offset(recipe, "r1", xy_r1[0], xy_r1[1])
fill_recipe_slots_from_r1(recipe)
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:
parser = argparse.ArgumentParser(
description="Interactive coordinate calibration for TFC forge macro.",
)
parser.add_argument(
"-f",
"--full",
action="store_true",
help=(
"Record every offset from scratch: anvil UI (plans, weld, inputs, forge steps), i1 plus derived "
"inventory/hotbar grid, and recipe UI (r1 plus derived r2r18, then rlb/rrb). "
"Required the first time (no coords file). Without -f, an existing coords file is updated: only "
"anvil and recipe tl, br, width, and height."
),
)
parser.add_argument(
"-o",
"--output",
type=Path,
default=DEFAULT_OUTPUT,
help=f"Path to output coords.json (default: {DEFAULT_OUTPUT}).",
)
args = parser.parse_args()
try:
run_setup(args.output, args.full)
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__":
main()

140
tfc-forge-macro/uv.lock generated
View File

@@ -1,140 +0,0 @@
version = 1
revision = 3
requires-python = ">=3.12"
[[package]]
name = "evdev"
version = "1.9.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a5/f5/397b61091120a9ca5001041dd7bf76c385b3bfd67a0e5bcb74b852bd22a4/evdev-1.9.3.tar.gz", hash = "sha256:2c140e01ac8437758fa23fe5c871397412461f42d421aa20241dc8fe8cfccbc9", size = 32723, upload-time = "2026-02-05T21:54:24.987Z" }
[[package]]
name = "pynput"
version = "1.8.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "evdev", marker = "'linux' in sys_platform" },
{ name = "pyobjc-framework-applicationservices", marker = "sys_platform == 'darwin'" },
{ name = "pyobjc-framework-quartz", marker = "sys_platform == 'darwin'" },
{ name = "python-xlib", marker = "'linux' in sys_platform" },
{ name = "six" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f0/c3/dccf44c68225046df5324db0cc7d563a560635355b3e5f1d249468268a6f/pynput-1.8.1.tar.gz", hash = "sha256:70d7c8373ee98911004a7c938742242840a5628c004573d84ba849d4601df81e", size = 82289, upload-time = "2025-03-17T17:12:01.481Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/59/4f/ac3fa906ae8a375a536b12794128c5efacade9eaa917a35dfd27ce0c7400/pynput-1.8.1-py2.py3-none-any.whl", hash = "sha256:42dfcf27404459ca16ca889c8fb8ffe42a9fe54f722fd1a3e130728e59e768d2", size = 91693, upload-time = "2025-03-17T17:12:00.094Z" },
]
[[package]]
name = "pyobjc-core"
version = "12.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b8/b6/d5612eb40be4fd5ef88c259339e6313f46ba67577a95d86c3470b951fce0/pyobjc_core-12.1.tar.gz", hash = "sha256:2bb3903f5387f72422145e1466b3ac3f7f0ef2e9960afa9bcd8961c5cbf8bd21", size = 1000532, upload-time = "2025-11-14T10:08:28.292Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/64/5a/6b15e499de73050f4a2c88fff664ae154307d25dc04da8fb38998a428358/pyobjc_core-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:818bcc6723561f207e5b5453efe9703f34bc8781d11ce9b8be286bb415eb4962", size = 678335, upload-time = "2025-11-14T09:32:20.107Z" },
{ url = "https://files.pythonhosted.org/packages/f4/d2/29e5e536adc07bc3d33dd09f3f7cf844bf7b4981820dc2a91dd810f3c782/pyobjc_core-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:01c0cf500596f03e21c23aef9b5f326b9fb1f8f118cf0d8b66749b6cf4cbb37a", size = 677370, upload-time = "2025-11-14T09:33:05.273Z" },
{ url = "https://files.pythonhosted.org/packages/1b/f0/4b4ed8924cd04e425f2a07269943018d43949afad1c348c3ed4d9d032787/pyobjc_core-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:177aaca84bb369a483e4961186704f64b2697708046745f8167e818d968c88fc", size = 719586, upload-time = "2025-11-14T09:33:53.302Z" },
{ url = "https://files.pythonhosted.org/packages/25/98/9f4ed07162de69603144ff480be35cd021808faa7f730d082b92f7ebf2b5/pyobjc_core-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:844515f5d86395b979d02152576e7dee9cc679acc0b32dc626ef5bda315eaa43", size = 670164, upload-time = "2025-11-14T09:34:37.458Z" },
{ url = "https://files.pythonhosted.org/packages/62/50/dc076965c96c7f0de25c0a32b7f8aa98133ed244deaeeacfc758783f1f30/pyobjc_core-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:453b191df1a4b80e756445b935491b974714456ae2cbae816840bd96f86db882", size = 712204, upload-time = "2025-11-14T09:35:24.148Z" },
]
[[package]]
name = "pyobjc-framework-applicationservices"
version = "12.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyobjc-core" },
{ name = "pyobjc-framework-cocoa" },
{ name = "pyobjc-framework-coretext" },
{ name = "pyobjc-framework-quartz" },
]
sdist = { url = "https://files.pythonhosted.org/packages/be/6a/d4e613c8e926a5744fc47a9e9fea08384a510dc4f27d844f7ad7a2d793bd/pyobjc_framework_applicationservices-12.1.tar.gz", hash = "sha256:c06abb74f119bc27aeb41bf1aef8102c0ae1288aec1ac8665ea186a067a8945b", size = 103247, upload-time = "2025-11-14T10:08:52.18Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/37/a7/55fa88def5c02732c4b747606ff1cbce6e1f890734bbd00f5596b21eaa02/pyobjc_framework_applicationservices-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c8f6e2fb3b3e9214ab4864ef04eee18f592b46a986c86ea0113448b310520532", size = 32835, upload-time = "2025-11-14T09:36:11.855Z" },
{ url = "https://files.pythonhosted.org/packages/fc/21/79e42ee836f1010f5fe9e97d2817a006736bd287c15a3674c399190a2e77/pyobjc_framework_applicationservices-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bd1f4dbb38234a24ae6819f5e22485cf7dd3dd4074ff3bf9a9fdb4c01a3b4a38", size = 32859, upload-time = "2025-11-14T09:36:15.208Z" },
{ url = "https://files.pythonhosted.org/packages/66/3a/0f1d4dcf2345e875e5ea9761d5a70969e241d24089133d21f008dde596f5/pyobjc_framework_applicationservices-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:8a5d2845249b6a85ba9e320a9848468c3f8cd6f59605a9a43f406a7810eaa830", size = 33115, upload-time = "2025-11-14T09:36:18.384Z" },
{ url = "https://files.pythonhosted.org/packages/40/44/3196b40fec68b4413c92875311f17ccf4c3ff7d2e53676f8fc18ad29bd18/pyobjc_framework_applicationservices-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:f43c9a24ad97a9121276d4d571aa04a924282c80d7291cfb3b29839c3e2013a8", size = 32997, upload-time = "2025-11-14T09:36:21.58Z" },
{ url = "https://files.pythonhosted.org/packages/fd/bb/dab21d2210d3ef7dd0616df7e8ea89b5d8d62444133a25f76e649a947168/pyobjc_framework_applicationservices-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:1f72e20009a4ebfd5ed5b23dc11c1528ad6b55cc63ee71952ddb2a5e5f1cb7da", size = 33238, upload-time = "2025-11-14T09:36:24.751Z" },
]
[[package]]
name = "pyobjc-framework-cocoa"
version = "12.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyobjc-core" },
]
sdist = { url = "https://files.pythonhosted.org/packages/02/a3/16ca9a15e77c061a9250afbae2eae26f2e1579eb8ca9462ae2d2c71e1169/pyobjc_framework_cocoa-12.1.tar.gz", hash = "sha256:5556c87db95711b985d5efdaaf01c917ddd41d148b1e52a0c66b1a2e2c5c1640", size = 2772191, upload-time = "2025-11-14T10:13:02.069Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/95/bf/ee4f27ec3920d5c6fc63c63e797c5b2cc4e20fe439217085d01ea5b63856/pyobjc_framework_cocoa-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:547c182837214b7ec4796dac5aee3aa25abc665757b75d7f44f83c994bcb0858", size = 384590, upload-time = "2025-11-14T09:41:17.336Z" },
{ url = "https://files.pythonhosted.org/packages/ad/31/0c2e734165abb46215797bd830c4bdcb780b699854b15f2b6240515edcc6/pyobjc_framework_cocoa-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5a3dcd491cacc2f5a197142b3c556d8aafa3963011110102a093349017705118", size = 384689, upload-time = "2025-11-14T09:41:41.478Z" },
{ url = "https://files.pythonhosted.org/packages/23/3b/b9f61be7b9f9b4e0a6db18b3c35c4c4d589f2d04e963e2174d38c6555a92/pyobjc_framework_cocoa-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:914b74328c22d8ca261d78c23ef2befc29776e0b85555973927b338c5734ca44", size = 388843, upload-time = "2025-11-14T09:42:05.719Z" },
{ url = "https://files.pythonhosted.org/packages/59/bb/f777cc9e775fc7dae77b569254570fe46eb842516b3e4fe383ab49eab598/pyobjc_framework_cocoa-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:03342a60fc0015bcdf9b93ac0b4f457d3938e9ef761b28df9564c91a14f0129a", size = 384932, upload-time = "2025-11-14T09:42:29.771Z" },
{ url = "https://files.pythonhosted.org/packages/58/27/b457b7b37089cad692c8aada90119162dfb4c4a16f513b79a8b2b022b33b/pyobjc_framework_cocoa-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:6ba1dc1bfa4da42d04e93d2363491275fb2e2be5c20790e561c8a9e09b8cf2cc", size = 388970, upload-time = "2025-11-14T09:42:53.964Z" },
]
[[package]]
name = "pyobjc-framework-coretext"
version = "12.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyobjc-core" },
{ name = "pyobjc-framework-cocoa" },
{ name = "pyobjc-framework-quartz" },
]
sdist = { url = "https://files.pythonhosted.org/packages/29/da/682c9c92a39f713bd3c56e7375fa8f1b10ad558ecb075258ab6f1cdd4a6d/pyobjc_framework_coretext-12.1.tar.gz", hash = "sha256:e0adb717738fae395dc645c9e8a10bb5f6a4277e73cba8fa2a57f3b518e71da5", size = 90124, upload-time = "2025-11-14T10:14:38.596Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cd/0f/ddf45bf0e3ba4fbdc7772de4728fd97ffc34a0b5a15e1ab1115b202fe4ae/pyobjc_framework_coretext-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d246fa654bdbf43bae3969887d58f0b336c29b795ad55a54eb76397d0e62b93c", size = 30108, upload-time = "2025-11-14T09:47:04.228Z" },
{ url = "https://files.pythonhosted.org/packages/20/a2/a3974e3e807c68e23a9d7db66fc38ac54f7ecd2b7a9237042006699a76e1/pyobjc_framework_coretext-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7cbb2c28580e6704ce10b9a991ccd9563a22b3a75f67c36cf612544bd8b21b5f", size = 30110, upload-time = "2025-11-14T09:47:07.518Z" },
{ url = "https://files.pythonhosted.org/packages/0f/5d/85e059349e9cfbd57269a1f11f56747b3ff5799a3bcbd95485f363c623d8/pyobjc_framework_coretext-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:14100d1e39efb30f57869671fb6fce8d668f80c82e25e7930fb364866e5c0dab", size = 30697, upload-time = "2025-11-14T09:47:10.932Z" },
{ url = "https://files.pythonhosted.org/packages/ef/c3/adf9d306e9ead108167ab7a974ab7d171dbacf31c72fad63e12585f58023/pyobjc_framework_coretext-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:782a1a9617ea267c05226e9cd81a8dec529969a607fe1e037541ee1feb9524e9", size = 30095, upload-time = "2025-11-14T09:47:13.893Z" },
{ url = "https://files.pythonhosted.org/packages/bd/ca/6321295f47a47b0fca7de7e751ddc0ddc360413f4e506335fe9b0f0fb085/pyobjc_framework_coretext-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:7afe379c5a870fa3e66e6f65231c3c1732d9ccd2cd2a4904b2cd5178c9e3c562", size = 30702, upload-time = "2025-11-14T09:47:17.292Z" },
]
[[package]]
name = "pyobjc-framework-quartz"
version = "12.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyobjc-core" },
{ name = "pyobjc-framework-cocoa" },
]
sdist = { url = "https://files.pythonhosted.org/packages/94/18/cc59f3d4355c9456fc945eae7fe8797003c4da99212dd531ad1b0de8a0c6/pyobjc_framework_quartz-12.1.tar.gz", hash = "sha256:27f782f3513ac88ec9b6c82d9767eef95a5cf4175ce88a1e5a65875fee799608", size = 3159099, upload-time = "2025-11-14T10:21:24.31Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e9/9b/780f057e5962f690f23fdff1083a4cfda5a96d5b4d3bb49505cac4f624f2/pyobjc_framework_quartz-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7730cdce46c7e985535b5a42c31381af4aa6556e5642dc55b5e6597595e57a16", size = 218798, upload-time = "2025-11-14T10:00:01.236Z" },
{ url = "https://files.pythonhosted.org/packages/ba/2d/e8f495328101898c16c32ac10e7b14b08ff2c443a756a76fd1271915f097/pyobjc_framework_quartz-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:629b7971b1b43a11617f1460cd218bd308dfea247cd4ee3842eb40ca6f588860", size = 219206, upload-time = "2025-11-14T10:00:15.623Z" },
{ url = "https://files.pythonhosted.org/packages/67/43/b1f0ad3b842ab150a7e6b7d97f6257eab6af241b4c7d14cb8e7fde9214b8/pyobjc_framework_quartz-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:53b84e880c358ba1ddcd7e8d5ea0407d760eca58b96f0d344829162cda5f37b3", size = 224317, upload-time = "2025-11-14T10:00:30.703Z" },
{ url = "https://files.pythonhosted.org/packages/4a/00/96249c5c7e5aaca5f688ca18b8d8ad05cd7886ebd639b3c71a6a4cadbe75/pyobjc_framework_quartz-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:42d306b07f05ae7d155984503e0fb1b701fecd31dcc5c79fe8ab9790ff7e0de0", size = 219558, upload-time = "2025-11-14T10:00:45.476Z" },
{ url = "https://files.pythonhosted.org/packages/4d/a6/708a55f3ff7a18c403b30a29a11dccfed0410485a7548c60a4b6d4cc0676/pyobjc_framework_quartz-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0cc08fddb339b2760df60dea1057453557588908e42bdc62184b6396ce2d6e9a", size = 224580, upload-time = "2025-11-14T10:01:00.091Z" },
]
[[package]]
name = "python-xlib"
version = "0.33"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "six" },
]
sdist = { url = "https://files.pythonhosted.org/packages/86/f5/8c0653e5bb54e0cbdfe27bf32d41f27bc4e12faa8742778c17f2a71be2c0/python-xlib-0.33.tar.gz", hash = "sha256:55af7906a2c75ce6cb280a584776080602444f75815a7aff4d287bb2d7018b32", size = 269068, upload-time = "2022-12-25T18:53:00.824Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fc/b8/ff33610932e0ee81ae7f1269c890f697d56ff74b9f5b2ee5d9b7fa2c5355/python_xlib-0.33-py2.py3-none-any.whl", hash = "sha256:c3534038d42e0df2f1392a1b30a15a4ff5fdc2b86cfa94f072bf11b10a164398", size = 182185, upload-time = "2022-12-25T18:52:58.662Z" },
]
[[package]]
name = "six"
version = "1.17.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
]
[[package]]
name = "tfc-forge-macro"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "pynput" },
]
[package.metadata]
requires-dist = [{ name = "pynput", specifier = ">=1.7.6" }]