fix(input): restore working controls + add input harness + build fixes

Consolidated from the qa-input-collision-fix-0.1.0 work, applied onto main so
only the changes main genuinely lacks are carried (main already merged the
walkable-demo/collision work independently).

- fix(input): populate the UE 5.8 FInputMappingContextMappingData
  DefaultKeyMappings field in ue_create_input_assets.py (the runtime reads
  GetMappings()->DefaultKeyMappings.Mappings; the script previously wrote only
  the deprecated 'mappings' array, so the cooked game had 0 runtime mappings
  and every key silently did nothing — in packaged AND PIE). Regenerated
  IMC_Default.uasset. WASD/mouse/Space/R/Escape verified working.
- fix(build): drop editor-only APIs that break the game target build —
  UStaticMesh::PostEditChange() (GigLineAssetTools) and AActor::GetActorLabel()
  (character ground probe). GigLine and GigLineEditor now both compile clean.
- tools: OS-level uinput input harness (input_emulation.py + input_test.py) and
  run_pie.sh for PIE isolation; document the PIE-vs-packaged input test in
  Docs/build.md. Removed the temporary SIM/EI/screenshot diagnostics from the
  character.
This commit is contained in:
zwitschi committed 2026-11-08 13:43:33 +01:00
1 parent 6b4e1252c5
commit 4832e66c00
8 files changed
+639 -56

No files matched your search

+172
View File
@@ -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_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_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_test.py # default smoke sequence
python3 Tools/input_test.py --sequence move:2,look:3,jump:1,reset:1,pause:1
python3 Tools/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())