#!/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())