refactor(tools): group the sixty scripts by what they talk to
One directory with sixty scripts stopped being navigable, and the grouping is also the sandbox boundary:
Tools/unreal/ commandlets and the two map builders, with ue_common.py and ue_placement.py as the
shared imports (they need the engine)
Tools/blender/ builders, exporters, the FBX preset, the MPFB character pipeline, blender_screenshots
(they need the Blender Flatpak)
Tools/review/ the visual loop: render_screenshots.sh, judge_screenshot.py, hero_compare.py,
build_comparison_md.py, e2e_asset_check.py, hero_iterate.sh, visual_loop.py,
visual_targets.json
Tools/gen/ image and texture generation: the ComfyUI driver, deterministic texture writers,
reference fetching
Tools/input/ OS-level input injection
root _common.py, check-lfs.sh, addon_compat_probe.py, sibling_asset_audit.py, tests/
Every moved script now resolves the repository root as ../.. and puts the tools root plus its siblings on
sys.path, so 'import ue_common' still works from unreal/. The test suite bootstrap does the same for the five
folders. render_screenshots.sh takes the folder as a third argument to run_cmdlet, and the e2e driver,
hero_iterate.sh and the comparison generator were repointed.
51 README rows now link to their new location, the README gains a Layout section naming each folder's
sandbox, and the architecture notes and Blender README follow the moves.
Verified: 108 tests pass, prettier is clean, judge_screenshot.py and hero_compare.py run from their new
homes, and the e2e run was aborted mid-way (it had been rebuilding an empty DemoMap for 15 minutes) and is
being rerun against the new layout.
This commit is contained in:
1 parent
f681d83736
commit
4d27109668
168 files changed
+799
-407
No files matched your search
@@ -0,0 +1,172 @@
|
||||
#!/usr/bin/env python3
|
||||
"""QA harness: inject scripted OS-level keyboard/mouse input at the running game.
|
||||
|
||||
This is the tool that proves whether OS input actually reaches the game. It uses
|
||||
`Tools/input/input_emulation.py` to create a virtual input device directly on
|
||||
`/dev/uinput`, so a focused game window receives the injected keys and mouse
|
||||
movement as genuine hardware input — indistinguishable from a real keyboard.
|
||||
|
||||
Typical workflow
|
||||
----------------
|
||||
The game must already be running and focused (it will not be launched here; the
|
||||
user brings up the window). Run this script from a separate terminal:
|
||||
|
||||
python3 Tools/input/input_test.py --sequence move:2,look:3,jump:1,reset:1,pause:1
|
||||
|
||||
It prints a per-step timeline (seconds) to stdout, which you can compare against
|
||||
the `GigLine IN:` rows in the game's `Saved/Logs`. If the game log shows
|
||||
`MoveForward fired` / `Look fired` for the injected steps, OS input is reaching
|
||||
the pawn and the remaining problem is not input delivery.
|
||||
|
||||
Usage
|
||||
-----
|
||||
python3 Tools/input/input_test.py # default smoke sequence
|
||||
python3 Tools/input/input_test.py --sequence move:2,look:3,jump:1,reset:1,pause:1
|
||||
python3 Tools/input/input_test.py --device /dev/uinput --name "GigLine QA Input"
|
||||
|
||||
Steps supported in `--sequence` (comma-separated, `name:count`):
|
||||
move hold W forward (each unit = ~0.5 s)
|
||||
back hold S
|
||||
strafe hold D
|
||||
left hold A
|
||||
look nudge mouse right (rotate camera)
|
||||
jump press Space
|
||||
reset press R
|
||||
pause press Escape
|
||||
click left mouse click
|
||||
|
||||
Design
|
||||
------
|
||||
* The device is kept open for the whole run and destroyed on exit (context
|
||||
manager), so nothing persists after the harness quits.
|
||||
* Each step logs an absolute timestamp so you can line it up with the game log
|
||||
wall-clock times (the game logs to `Saved/Logs/GigLine.log` with the same
|
||||
system clock).
|
||||
* No third-party packages: only the Python standard library.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import sys
|
||||
import time
|
||||
|
||||
from input_emulation import InputDevice, UInputError
|
||||
|
||||
# Duration of one "unit" for hold-based movement steps, in seconds.
|
||||
MOVE_STEP_S = 0.5
|
||||
|
||||
# Handlers map step names to a callable(dev) that performs the key/mouse action.
|
||||
def _move(dev):
|
||||
dev.hold("w", MOVE_STEP_S)
|
||||
|
||||
|
||||
def _back(dev):
|
||||
dev.hold("s", MOVE_STEP_S)
|
||||
|
||||
|
||||
def _strafe(dev):
|
||||
dev.hold("d", MOVE_STEP_S)
|
||||
|
||||
|
||||
def _left(dev):
|
||||
dev.hold("a", MOVE_STEP_S)
|
||||
|
||||
|
||||
def _look(dev):
|
||||
# nudge the mouse +240 px on X repeatedly so the camera has time to respond.
|
||||
for _ in range(6):
|
||||
dev.move(240, 0, delay=0.03)
|
||||
time.sleep(0.15)
|
||||
|
||||
|
||||
def _jump(dev):
|
||||
dev.key("space", delay=0.05)
|
||||
|
||||
|
||||
def _reset(dev):
|
||||
dev.key("r", delay=0.05)
|
||||
|
||||
|
||||
def _pause(dev):
|
||||
dev.key("esc", delay=0.05)
|
||||
|
||||
|
||||
def _click(dev):
|
||||
dev.bclick("left")
|
||||
|
||||
|
||||
STEPS = {
|
||||
"move": _move, "back": _back, "strafe": _strafe, "left": _left,
|
||||
"look": _look, "jump": _jump, "reset": _reset, "pause": _pause,
|
||||
"click": _click,
|
||||
}
|
||||
|
||||
|
||||
def parse_sequence(text):
|
||||
"""'move:2,look:1,jump:1' -> ordered list of (name, count)."""
|
||||
seq = []
|
||||
for item in (text or "").split(","):
|
||||
item = item.strip()
|
||||
if not item:
|
||||
continue
|
||||
if ":" in item:
|
||||
name, _, count = item.partition(":")
|
||||
count = int(count or "1")
|
||||
else:
|
||||
name, count = item, 1
|
||||
if name not in STEPS:
|
||||
raise ValueError(f"unknown step {name!r}; known: {sorted(STEPS)}")
|
||||
seq.append((name, count))
|
||||
if not seq:
|
||||
seq = [("move", 2), ("look", 1), ("jump", 1), ("reset", 1), ("pause", 1)]
|
||||
return seq
|
||||
|
||||
|
||||
def run_sequence(dev, seq):
|
||||
t0 = time.time()
|
||||
for name, count in seq:
|
||||
for _ in range(count):
|
||||
stamp = time.time() - t0
|
||||
print(f"[{stamp:6.3f}s] -> {name}")
|
||||
# A brief settle so distinct keystrokes are logged separately.
|
||||
time.sleep(0.1)
|
||||
STEPS[name](dev)
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
ap = argparse.ArgumentParser(description=__doc__,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
ap.add_argument("--sequence",
|
||||
help="comma-separated steps and repeats, e.g. move:2,look:3")
|
||||
ap.add_argument("--device", default="/dev/uinput", help="uinput device path")
|
||||
ap.add_argument("--name", default="GigLine QA Input", help="virtual device name")
|
||||
ap.add_argument("-v", "--verbose", action="store_true",
|
||||
help="log every injected event via logging")
|
||||
args = ap.parse_args(argv)
|
||||
|
||||
if args.verbose:
|
||||
logging.basicConfig(level=logging.INFO,
|
||||
format="%(asctime)s.%(msecs)03d %(name)s %(message)s",
|
||||
datefmt="%H:%M:%S")
|
||||
try:
|
||||
seq = parse_sequence(args.sequence)
|
||||
except ValueError as exc:
|
||||
ap.error(str(exc))
|
||||
|
||||
try:
|
||||
with InputDevice(name=args.name, path=args.device) as dev:
|
||||
print(f"# virtual device {args.name!r} active; send sequence now")
|
||||
print(f"# steps: {' '.join(f'{n}x{c}' for n, c in seq)}")
|
||||
run_sequence(dev, seq)
|
||||
except UInputError as exc:
|
||||
print(f"ERROR: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
except KeyboardInterrupt:
|
||||
print("\n# interrupted; device destroyed on exit")
|
||||
return 130
|
||||
print("# done; inputs sent")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in new issue
Block a user