fix(port): held_direction() polls the stick instead of reconstructing it
Reported by a human on a real controller: hold the left stick down, the cursor moves one item and stops. The repeat never runs. `held_direction()`'s own comment says "Polled at the DEVICE, never through Input.is_action_pressed". It was not. The d-pad and keyboard branches polled; the STICK branch read `_latched`, which is a reconstruction of the stick's position from the event history. That reconstruction is only as good as the last event seen. A stick held still sends nothing, and one event reading below RELEASE -- a spring settling, a deadzone-shaped value, a driver emitting a zero on focus change -- clears it with no event afterwards to set it back. The port then believes the stick is centred while the player is holding it, which is precisely the symptom reported. Now polls `Input.get_joy_axis()` against the game's own 0.61, which is what the comment always meant. The latch stays as a fallback for INJECTED events, so the script harness and verify-input keep testing something. 🔴 Every instrument here missed this because every instrument SUPPLIES the input it measures: verify-input ticks repeat_due() directly, --script sends InputEventAction which bypasses the input map, and the new --script=hold: injects its own axis event. All three agreed with each other and none read a device. Same shape as the 2026-09-01 report that opened gamepad.gd, one level deeper, with the lesson already written at the top of that file. So this adds the two things that would have caught it: --script=hold:down:2.0 hold one real axis deflection and log every move --input-probe print what the devices report, on change ⚠️ The fix itself is NOT verified. It matches the symptom exactly and was found by reading, but only a human holding a stick can confirm it, and the probe exists so the answer is measured either way. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -549,6 +549,7 @@ func _process(delta: float) -> void:
|
||||
view.queue_redraw()
|
||||
_overlay_process(delta)
|
||||
_menu_repeat(delta)
|
||||
_input_probe()
|
||||
|
||||
if _player != null:
|
||||
# `--skip-at=SECONDS` presses (A) at a wall-clock moment DURING a movie,
|
||||
@@ -962,6 +963,25 @@ func _unhandled_input(event: InputEvent) -> void:
|
||||
## a screen with no menu would be a second, subtly different input path, and the
|
||||
## first thing this port learned about input is that a second path is where the
|
||||
## defect hides.
|
||||
## `--input-probe` prints what the DEVICES report, whenever it changes.
|
||||
##
|
||||
## 🔴 WHY IT EXISTS. On 2026-09-13 a human held a real stick and the cursor moved
|
||||
## once. Every instrument here disagreed with them and agreed with each other —
|
||||
## `verify-input` ticks `repeat_due()` directly, `--script=hold:` injects its own
|
||||
## axis event — because all of them SUPPLY the input they then measure. None
|
||||
## could see a controller. This is the one line that reads the device and says
|
||||
## what it says, so the next report of this shape starts from a measurement.
|
||||
var _probe_last := ""
|
||||
|
||||
func _input_probe() -> void:
|
||||
if not _args().has("input-probe"):
|
||||
return
|
||||
var line := _pad.probe_line()
|
||||
if line != _probe_last:
|
||||
_probe_last = line
|
||||
print("probe: %s" % line)
|
||||
|
||||
|
||||
func _menu_repeat(delta: float) -> void:
|
||||
var step := _pad.repeat_due(delta)
|
||||
if step == 0:
|
||||
@@ -1349,6 +1369,45 @@ func _run_script() -> void:
|
||||
var until := Time.get_ticks_msec() + int(secs * 1000.0)
|
||||
while Time.get_ticks_msec() < until:
|
||||
await get_tree().process_frame
|
||||
elif token.begins_with("hold:"):
|
||||
# 🔴 THE ONE THING `--script` STRUCTURALLY COULD NOT DO, AND THE ONE
|
||||
# THING F1 IS ABOUT. Every other step sends an `InputEventAction`,
|
||||
# which BYPASSES the input map and is not an analog axis — the
|
||||
# lesson `gamepad.gd` opens with. A held direction is exactly an
|
||||
# analog axis that is not re-sent, so nothing in this harness could
|
||||
# ever observe the repeat, and the first report that it does not
|
||||
# work came from a human with a real controller.
|
||||
#
|
||||
# This injects ONE real `InputEventJoypadMotion` at full deflection
|
||||
# and then sends NOTHING for the duration — which is what a stick
|
||||
# held still actually looks like — logging every focus change.
|
||||
var bits := token.split(":")
|
||||
var secs2 := float(bits[2]) if bits.size() > 2 else 1.0
|
||||
var dir := -1.0 if bits[1] == "up" else 1.0
|
||||
print("script[%d] hold %s for %.2f s at %.2f s"
|
||||
% [i + 1, bits[1], secs2, _elapsed])
|
||||
var m := InputEventJoypadMotion.new()
|
||||
m.axis = JOY_AXIS_LEFT_Y
|
||||
m.axis_value = dir
|
||||
Input.parse_input_event(m)
|
||||
var seen := _focus_label(view.focused_id)
|
||||
var moves := 0
|
||||
var t0 := _elapsed
|
||||
while _elapsed - t0 < secs2:
|
||||
await get_tree().process_frame
|
||||
var now := _focus_label(view.focused_id)
|
||||
if now != seen:
|
||||
moves += 1
|
||||
print(" hold move %d -> %s at %+.3f s"
|
||||
% [moves, now, _elapsed - t0])
|
||||
seen = now
|
||||
# Release, so the latch re-arms for whatever follows.
|
||||
var rel := InputEventJoypadMotion.new()
|
||||
rel.axis = JOY_AXIS_LEFT_Y
|
||||
rel.axis_value = 0.0
|
||||
Input.parse_input_event(rel)
|
||||
print(" hold produced %d move(s) in %.2f s (held_direction=%d at end)"
|
||||
% [moves, secs2, _pad.held_direction()])
|
||||
elif token == "wait":
|
||||
pass
|
||||
elif SCRIPT_ACTIONS.has(token):
|
||||
|
||||
@@ -252,16 +252,37 @@ func accepts(event: InputEvent) -> bool:
|
||||
## exclude — so the repeat would contradict the threshold on the same stick.
|
||||
## That is the input-map lesson again: assert the device, not the layer above it.
|
||||
func held_direction() -> int:
|
||||
# The stick, from the latch `accepts()` already maintains, so the repeat and
|
||||
# the first step read one state and cannot disagree about hysteresis.
|
||||
var stick := int(_latched.get(JOY_AXIS_LEFT_Y, 0))
|
||||
if stick != 0:
|
||||
return stick
|
||||
# 🔴 THE STICK IS NOW ACTUALLY POLLED. It used to read `_latched` — a
|
||||
# reconstruction of the stick's position from the event history — while the
|
||||
# comment above said "polled at the DEVICE". A human holding a real stick
|
||||
# got exactly one step and no repeat (2026-09-13), and the harness could not
|
||||
# see it, because the harness fed the same events the latch was built from.
|
||||
#
|
||||
# `_latched` only changes when an event ARRIVES. A stick held perfectly
|
||||
# still sends nothing, and any one event that reads below `RELEASE` — a
|
||||
# spring settling, a deadzone-shaped value, a driver that emits a zero on
|
||||
# focus change — clears it with no event afterwards to set it back. The
|
||||
# position was then wrong until the player moved the stick again, which is
|
||||
# indistinguishable from "the repeat does not work".
|
||||
#
|
||||
# `get_joy_axis()` is the position itself, tested against the game's own
|
||||
# 0.61, which is what the paragraph above always meant.
|
||||
for device in Input.get_connected_joypads():
|
||||
var v := Input.get_joy_axis(device, JOY_AXIS_LEFT_Y)
|
||||
if v >= ENTER:
|
||||
return 1
|
||||
if v <= -ENTER:
|
||||
return -1
|
||||
if Input.is_joy_button_pressed(device, JOY_BUTTON_DPAD_UP):
|
||||
return -1
|
||||
if Input.is_joy_button_pressed(device, JOY_BUTTON_DPAD_DOWN):
|
||||
return 1
|
||||
# The latch, for events that were INJECTED rather than read off a device:
|
||||
# `Input.parse_input_event()` does not move `get_joy_axis()`, so the script
|
||||
# harness and `verify-input` would otherwise test nothing at all here.
|
||||
var stick := int(_latched.get(JOY_AXIS_LEFT_Y, 0))
|
||||
if stick != 0:
|
||||
return stick
|
||||
if Input.is_key_pressed(KEY_UP):
|
||||
return -1
|
||||
if Input.is_key_pressed(KEY_DOWN):
|
||||
@@ -269,6 +290,29 @@ func held_direction() -> int:
|
||||
return 0
|
||||
|
||||
|
||||
## What the devices actually report, for a human to read while holding a stick.
|
||||
##
|
||||
## The 2026-09-13 report — "I hold it down, it moves one item and stops" — could
|
||||
## not be diagnosed from here: every instrument in this repo feeds its own
|
||||
## events, so all of them agreed with each other and none of them agreed with
|
||||
## the controller. This prints the raw state so the next such report starts from
|
||||
## a measurement instead of a guess.
|
||||
func probe_line() -> String:
|
||||
var parts := PackedStringArray()
|
||||
for device in Input.get_connected_joypads():
|
||||
parts.append("[%d] %s Y=%+.3f X=%+.3f dpad=%s%s" % [
|
||||
device, Input.get_joy_name(device),
|
||||
Input.get_joy_axis(device, JOY_AXIS_LEFT_Y),
|
||||
Input.get_joy_axis(device, JOY_AXIS_LEFT_X),
|
||||
"U" if Input.is_joy_button_pressed(device, JOY_BUTTON_DPAD_UP) else "-",
|
||||
"D" if Input.is_joy_button_pressed(device, JOY_BUTTON_DPAD_DOWN) else "-"])
|
||||
if parts.is_empty():
|
||||
parts.append("no joypad")
|
||||
return "%s latched=%d held_direction=%d (ENTER=%.2f RELEASE=%.2f)" % [
|
||||
" ".join(parts), int(_latched.get(JOY_AXIS_LEFT_Y, 0)), held_direction(),
|
||||
ENTER, RELEASE]
|
||||
|
||||
|
||||
## One repeat step, or 0. Call once per frame with the frame's delta.
|
||||
##
|
||||
## The FIRST step is not this function's: it comes from the event edge in
|
||||
|
||||
Reference in New Issue
Block a user