mirror of
https://github.com/opus-tango/public-python.git
synced 2026-07-18 07:32:45 +00:00
Compare commits
3 Commits
40e9d93c05
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c1bfbf0c4 | ||
|
|
0e455a2e40 | ||
|
|
28d8abcfff |
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
__pycache__/
|
||||||
|
.venv/
|
||||||
1
tfc-forge-macro/.gitignore
vendored
1
tfc-forge-macro/.gitignore
vendored
@@ -1 +0,0 @@
|
|||||||
__pycache__/
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
3.12
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
This project is for setting up a macro program to automate forging in terrafirmacraft.
|
|
||||||
@@ -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]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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()
|
|
||||||
@@ -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",
|
|
||||||
]
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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); i2–i27 and h1–h9 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); r2–r18 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 (i1–i27 then hotbar h1–h9); recipe panel is 2 rows (r1–r18).
|
|
||||||
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 i1–i27 and h1–h9 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 r1–r18 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). "
|
|
||||||
"i2–i27 and h1–h9 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). "
|
|
||||||
"r2–r18 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 r2–r18, 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
140
tfc-forge-macro/uv.lock
generated
@@ -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" }]
|
|
||||||
Reference in New Issue
Block a user