Files
gig-line/Tools/ue_create_input_assets.py
T
zwitschi 52b1212cf3 qa: make the demo walkable — terrain collision root cause, spawn, scripted QA
The reported fall-through was a misdiagnosis: the player never fell. The
imported Terrain_DEM had only a bounding-box simple collision proxy, so a
downward trace anywhere in the block hit the terrain at Z 819 (the mesh
bbox max) and the street sat under an invisible 8 m lid. Standing on that
box looked plausible, which is why it read as working.

- add UGigLineAssetTools (C++): force CTF_UseComplexAsSimple on a static
  mesh. UE 5.8 dropped bUseComplexAsSimpleCollision and Python cannot set
  the flag; the builder also saves the asset, since the flag lives there
  and a map-only save silently reverts it.
- spawn (-13336, -1150, 600): inside the carved corridor (the cut is a
  10.7 m half-width around the road centreline), lands on the road at 456.
- fix ResetToStart returning to the mid-air PlayerStart captured in
  BeginPlay: StartLocation is now captured on the first grounded frame.
- HighResShot 1280x720 instead of "1280 720", which is a multiplier.
- add -qaselftest: scripted walk/reset/pause on a core ticker plus a
  swept ground transect. Results: walk 746 cm/2 s with Z steady, reset
  0 cm from spawn, pause/unpause 1/0, transect 84 samples 0 holes.
- rebase onto main, keeping the frontage materials and placement fixes.
2026-11-08 11:56:19 +01:00

171 lines
7.0 KiB
Python

# Creates the Enhanced Input assets the first-person character loads:
# /Game/Input/IA_Move Axis2D, WASD + left stick
# /Game/Input/IA_Look Axis2D, mouse XY + right stick
# /Game/Input/IA_Jump axis1d, Space / gamepad face button
# /Game/Input/IMC_Default InputMappingContext holding the three mappings
#
# Run headless (imports/dirty packages are saved before exit):
# <engine>/Engine/Binaries/Linux/UnrealEditor-Cmd <uproject> -run=pythonscript \
# -script=<repo>/Tools/ue_create_input_assets.py -nullrhi -unattended -nopause \
# -stdout -FullStdOutLogOutput
#
# The result goes to /tmp/gigline/input-assets.txt because 5.8's pythonscript
# commandlet swallows print() output with -stdout.
#
# Mapping choices:
# - Look stays unit-free (no scalar modifier): the character applies its own
# degrees-per-second, and a modifier here would double-scale.
# - Move is a 2D axis: W = +Y, S = -Y, D = +X, A = -X via Negate on the Y axis.
# For keyboard keys the Negate modifier's Y axis maps to the axis's Y.
import unreal
ASSET_DIR = "/Game/Input"
_KEEP_ALIVE = []
def negate_y_modifier():
"""A negate-by-Y modifier, built with its Y flag set and X/Z cleared.
Returns the object plus its falsy reference; constructing the default
modifier gives all axes true, so each flag is set explicitly. The caller owns
keep-alive.
"""
neg = unreal.InputModifierNegate()
neg.set_editor_property("x", False)
neg.set_editor_property("y", True)
neg.set_editor_property("z", False)
return neg
RESULT = "/tmp/gigline/input-assets.txt"
lines = []
def log(msg):
lines.append(str(msg))
unreal.log(str(msg))
def fresh(name, cls, factory):
"""Load the asset if it exists, otherwise create it.
Deleting then recreating in the same commandlet run fails: create_asset
returns None for a path deleted earlier in the process. Overwriting in place
is also what keeps the assets' identities stable across reruns.
"""
path = f"{ASSET_DIR}/{name}"
existing = unreal.EditorAssetLibrary.load_asset(path)
if existing is not None:
return existing
return unreal.AssetToolsHelpers.get_asset_tools().create_asset(
name, ASSET_DIR, cls, factory())
def save(asset):
unreal.EditorAssetLibrary.save_loaded_asset(asset, only_if_is_dirty=False)
def make_key(name):
"""Build an FKey by name.
`unreal.InputCoreTypes` is not exposed to Python in 5.8 and `unreal.Key` does
not accept a constructor argument, but the struct's `import_text` parses the
key's export text, which is just its name.
"""
key = unreal.Key()
key.import_text(name)
return key
def build():
if not unreal.EditorAssetLibrary.does_directory_exist(ASSET_DIR):
unreal.EditorAssetLibrary.make_directory(ASSET_DIR)
# --- actions ---------------------------------------------------------
# Move keys are one-dimensional: W/S carry forward/back on the value's single
# component and D/A carry strafe, combined by the character. A 2D value would
# need per-key axis modifiers, and modifiers do not survive the save path here.
ia_move = fresh("IA_Move", unreal.InputAction, unreal.InputAction_Factory)
# Boolean (pressed = 1), not AXIS1D: the C++ handler reads Get<bool>() and
# moves at full walk speed. An axis action from a keyboard key can deliver 0
# depending on the trigger, which showed up as "character stands, W does not
# move" in QA. WASD are four separate per-direction actions below.
ia_move.set_editor_property("value_type", unreal.InputActionValueType.BOOLEAN)
save(ia_move)
ia_look = fresh("IA_Look", unreal.InputAction, unreal.InputAction_Factory)
ia_look.set_editor_property("value_type", unreal.InputActionValueType.AXIS2D)
save(ia_look)
ia_jump = fresh("IA_Jump", unreal.InputAction, unreal.InputAction_Factory)
ia_jump.set_editor_property("value_type", unreal.InputActionValueType.BOOLEAN)
save(ia_jump)
# --- mapping context -------------------------------------------------
imc = fresh("IMC_Default", unreal.InputMappingContext, unreal.InputMappingContext_Factory)
# Nine entries, one per key. No modifiers: the character negates S and A in
# C++ instead, because a modifier object attached to a mapping struct
# serialises as None on save in this build (the struct's object array does not
# keep the modifier alive across the save path). One less moving part, and the
# negation is visible where it is used.
# Two actions feed the one-dimensional move: IA_Move for forward/back, and a
# second action for strafe, both reporting on the value's single component.
ia_strafe = fresh("IA_Strafe", unreal.InputAction, unreal.InputAction_Factory)
ia_strafe.set_editor_property("value_type", unreal.InputActionValueType.BOOLEAN)
save(ia_strafe)
ia_back = fresh("IA_MoveBack", unreal.InputAction, unreal.InputAction_Factory)
ia_back.set_editor_property("value_type", unreal.InputActionValueType.BOOLEAN)
save(ia_back)
ia_left = fresh("IA_StrafeLeft", unreal.InputAction, unreal.InputAction_Factory)
ia_left.set_editor_property("value_type", unreal.InputActionValueType.BOOLEAN)
save(ia_left)
# Reset and pause are boolean: one press, no accumulator.
ia_reset = fresh("IA_Reset", unreal.InputAction, unreal.InputAction_Factory)
ia_reset.set_editor_property("value_type", unreal.InputActionValueType.BOOLEAN)
save(ia_reset)
ia_pause = fresh("IA_Pause", unreal.InputAction, unreal.InputAction_Factory)
ia_pause.set_editor_property("value_type", unreal.InputActionValueType.BOOLEAN)
save(ia_pause)
mapping_table = [
(ia_move, "W"), (ia_back, "S"), (ia_strafe, "D"), (ia_left, "A"),
(ia_move, "Gamepad_LeftY"), (ia_strafe, "Gamepad_LeftX"),
(ia_look, "MouseX"), (ia_look, "MouseY"), (ia_look, "Gamepad_Right2D"),
(ia_jump, "SpaceBar"), (ia_jump, "Gamepad_FaceButton_Bottom"),
(ia_reset, "R"), (ia_pause, "Escape"),
]
mappings = []
for action, key_name in mapping_table:
try:
entry = unreal.EnhancedActionKeyMapping()
entry.set_editor_property("action", action)
entry.set_editor_property("key", make_key(key_name))
mappings.append(entry)
except Exception as exc: # noqa: BLE001
log(f"skip {action.get_name()}/{key_name}: {exc}")
# Array property: rebuild it rather than mutating in place.
imc.set_editor_property("mappings", mappings)
save(imc)
log(f"IA_Move={ia_move.get_path_name()} IA_Look={ia_look.get_path_name()} "
f"IA_Jump={ia_jump.get_path_name()}")
log(f"IMC_Default={imc.get_path_name()} mappings={len(mappings)}")
log("assets=" + ",".join(unreal.EditorAssetLibrary.list_assets(ASSET_DIR)))
try:
build()
except Exception as exc: # noqa: BLE001 - surface the failure in the result file
import traceback
log(f"FAILED: {type(exc).__name__}: {exc}")
log(traceback.format_exc())
with open(RESULT, "w") as handle:
handle.write("\n".join(lines))