S7-E uses this same route for v2 renderer events. Expanded identities C/c, M/m, T/t, Tab, Shift+Tab and F5 reach existing KeyCode and binding queries without graphical-specific action mappings. Session-version validation belongs to the transport, not InputManager. See S7-E validation for deterministic and real-game coverage.
Ichiloto owns input bindings and gameplay. Input sources supply key identities, never actions. Terminal input remains the default; this capability does not launch GPUI from a game, change project configuration, or render a graphical game frame.
IO\InputSources\InputSourceInterface has two methods:
public function poll(): ?KeyCode;
public function reset(bool $drainBufferedInput = false): void;Each poll returns one canonical key in arrival order, or null. Sources perform
bounded reads and do not call gameplay, change bindings, or own game-loop timing.
InputManager holds current/previous ?KeyCode state and dispatches the existing
KeyboardEvent with the key's value, including recognized repeated keys.
The gameplay-facing Input facade is unchanged.
InputManager::init() and setBindings() supply a missing info action for
older projects: i and I are offered only when not already bound elsewhere.
An explicit info entry, including empty keys, is preserved unchanged. If both
aliases conflict, Info remains discoverable as Unbound in Controls for explicit
rebinding. Info cycles two-line menu descriptions and wraps to the first page;
it does not change selection. Boot captures these effective bindings for Restore
Defaults. Loading never writes configuration; the existing explicit Controls
rebind/restore workflow retains persistence ownership.
S3 uses canonical comparison for isKeyPressed() with maintainer approval.
Previously that method compared raw terminal bytes with enum values, so special
keys such as Up and Enter always returned false even when isKeyDown() returned
true. These keys now report pressed consistently for terminal and renderer input.
This is an explicitly approved correction to the legacy behavior, not a preserved
quirk. No special-key whitelist or terminal-byte leakage is needed in InputManager.
The normalized contract also excludes terminal bytes outside the existing enum:
these yield null rather than dispatching an event whose getKey() is null.
All supported terminal key mappings are retained. Bindings remain case-sensitive;
axes, isButtonDown(), and isAnyKeyPressed() remain edge-triggered. Repeating
the same key is pressed but not a new down edge. Changing A directly to B is not
an A release; isKeyUp() still requires a following no-input sample.
ActionHints resolves semantic actions to display-only ActionHint and
ControlHint values. By default it reads the live InputBindings on every
redraw. Compact hints show one bound control: Enter for confirm and Escape for
cancel/back when those keys are actually bound, otherwise the first bound key.
This does not remove aliases or change input handling. The Controls screen keeps
the complete binding list through describeKeys().
The shared menu hint painter uses optional theme icon roles such as
input.keyboard.ENTER, with readable keycap labels when no matching image is
supplied. Action labels, control identity and artwork remain separate. Main Menu,
Equipment and Status consume the same mechanism; legacy window help strings
elsewhere have not all been migrated.
Themes may set showInputHints to false to omit persistent graphical helper
strips and their reserved layout space without changing actions or bindings.
The default remains true for compatibility. Last Legend uses the dedicated
Controls lookup instead, retaining useful action descriptions in menus and
dialogs. Controls still exposes all aliases, not only the compact primary key.
A PHP input context can supply an ActionHintProvider and replace its display
profile without changing menu composition or dispatching input. That is a
presentation boundary, not implemented gamepad detection or controller input.
The current native and terminal sources still report keyboard identities only.
The approved PC/Desktop Controls Art example uses Xbox glyphs (gamepad.xbox):
A/B/X/Y, shoulders/triggers, D-pad, Menu and View. PlayStation and Nintendo
families are later variants. This is a manually selected preview family, not
approved shipping mappings, device detection or authorization to implement a
controller backend; semantic actions and keyboard bindings remain unchanged.
Physical-controller work must drive the profile from meaningful active-device
input, handle focus/disconnection and keyboard/controller coexistence, and keep
device glyph changes independent of actions, focus and selection. Merely having
a controller connected must not make hints unusable for a keyboard player.
Remaining rollout belongs with semantic input migration: audit each legacy help owner against the keys/actions its controller actually accepts, then reuse these descriptors for terminal text and native glyphs. Do not globally replace strings with misleading remappable hints for a controller that still reads fixed keys. Editor authoring of icon-role paths must use the same project asset selection and safe round-trip contract as other presentation artwork.
The terminal source contains the former manager's pending-byte buffer, escape completion, splitting, and translation. It retains arrow keys, letter case, CR/LF Enter, Space, Escape, Tab/Shift-Tab, both Backspace bytes, existing Home/End, Insert/Delete/Page sequences, and F0-F12 mappings. It does not add previously unsupported function-key sequences just because the enum has additional cases.
The existing four-attempt, 1 ms escape-completion window and concatenated sequence ordering are retained. An injected stream supports deterministic tests without touching real STDIN; ordinary callers omit it. The stream remains caller-owned. Reads temporarily make it nonblocking and restore its prior blocking flag. Existing engine echo/cbreak/cursor/terminal-lifecycle APIs are unchanged.
use Ichiloto\Engine\IO\InputManager;
use Ichiloto\Engine\IO\InputSources\RendererInputSource;
InputManager::setInputSource(new RendererInputSource($client));getInputSource() lazily creates a terminal source. init($game) retains an
explicitly selected source. Selecting a source clears current and previous keys,
so the old source cannot cause a phantom press/release. Selection does not drain
either source or terminate a renderer; use resetState(true) when draining is
intended. resetState() always clears both manager states and delegates its flag.
| Source | reset(false) |
reset(true) |
|---|---|---|
| Terminal | Clear parsed pending bytes | Also drain unread stream bytes without waiting |
| Renderer | Clear client-cached keys | Also discard keys from one bounded upstream poll |
Terminal draining has a 64 KiB budget; excess input fails explicitly instead of trapping a scene transition behind a continuously writing peer. Renderer draining does not promise to discard keys arriving later or beyond the S2 per-poll budget. Both renderer reset modes retain queued ready, close, and error events. Neither reset nor input-source destruction shuts down the renderer.
Rendering\RendererClient is the sole consumer of its transport's pollEvents().
It delegates start(), send(), isRunning(), and shutdown() to S2 rather than
duplicating handshake or process control. The application owns this shared client
and explicitly shuts it down; the input source only borrows it.
pump() performs one zero-wait transport pass. Keys enter an ordered event FIFO;
ready, close_requested, and error enter a separate typed-event FIFO. pollKey()
and pollEvents() pump when their respective queue is empty. Applications must
consume non-key events as well as keys; no close/error is interpreted as a game
action or automatic quit policy. Shutdown retains final transport events and is
safe to repeat.
pollKey() still returns a string; retaining the original immutable event internally
allows opt-in trace correlation without adding wire or gameplay fields.
drainEvents() consumes already-pumped lifecycle events without polling again.
RendererRuntime uses it after its explicit pump, keeping each I/O pass bounded.
Both terminal and renderer paths still consume one key per handleInput().
They cannot distinguish deliberate repeated taps from OS repeats, because neither
contract supplies repeat identity. A burst can queue behind the frame cadence.
Do not deduplicate identities or batch only KeyboardEvents: gameplay also reads
current/previous key state once per update. See the latency investigation in
S7-E validation for evidence and the unresolved batching boundary.
Default bounds are 1024 keys, 256 non-key events, and 8 MiB combined
key/message payload bytes. Count limits also bound fixed event/queue overhead.
Limits are constructor-injectable. An over-capacity batch raises an explicit
RendererTransportException before changing prior queues; that rejected batch
is not silently accepted. The failure is latched, while already queued events
remain readable. Underlying fatal S2 exceptions propagate likewise.
RendererInputSource converts client key strings with KeyCode::tryFrom(), not
another mapping table. An unsupported identity raises RendererProtocolException
with a bounded diagnostic. S2's wire parser remains independent of engine enums.
Consuming or resetting keys never steals lifecycle/error events from the client.
Future presentation code must share this same client, not independently poll its transport. The standalone S2 smoke tool still owns its own separate connection.
Build the unchanged reference renderer with cargo build --locked, then run:
php tools/gpui-input-smoke.php \
--renderer=/absolute/path/to/gpui-renderer \
--asset-root=/absolute/existing/assets \
--duration=60A blank native grid is expected. Native keys are reported independently from renderer lifecycle events:
RENDERER ready
INPUT key=up KeyCode=UP
INPUT key=W KeyCode=W
RENDERER close_requested
Enter in the launching terminal requests protocol shutdown. Enter, Escape, and q in the native window are only keys, not quit actions. Native close is surfaced separately before cleanup. The duration defaults to 60 seconds (allowed 0-300). Lifecycle diagnostics go to stderr. The tool generates no frames or game actions.
See the S3 validation and handoff record for tested boundaries and the approved compatibility correction. Runtime selection, presentation, and Last Legend integration remain out of scope.
Not implemented. The sections above describe the current event-only source contract. This extension is queued in the integration roadmap for G4 field readiness after the current cinematic ownership work; capture does not change current APIs or authorize an immediate implementation.
Gameplay and UI consume semantic actions, navigation/focus and input values. Control hints remain separate from action identity and artwork, preserving keyboard bindings while permitting controller glyphs and remapping. Shared contracts must accommodate device/control identity, press/release/held state, simultaneous controls, analog magnitude, focus/connection changes and coexistence. Future gamepads must not be permanently modeled as synthetic keyboard presses. PHP owns bindings, action contexts, movement and timing; native sources report normalized controls. No render callback may drive gameplay.
Renderer must inspect the pinned GPUI key-down/up and focus APIs before Engine
and Renderer agree the smallest negotiated transition/reset extension. Preserve
legacy event semantics; do not reinterpret them silently or assume an upgrade.
Track stable control identity separately from text/case so modifier changes do
not strand held keys. Repeated down events create neither new physical edges nor
extra movement. GPUI's previously unreliable is_held repeat flag is not enough.
Clear held state on focus loss, connection failure and shutdown; elapsed silence
is not release evidence. Preserve platform shortcuts and text-entry separation.
Replace single-current-key state for stateful sources with bounded event/held
processing before the normal gameplay update. Preserve taps that press and
release between updates, multiple bindings for one action and event ordering.
Do not run gameplay once per input event or drain unboundedly. Keep held actions,
pressed/released edges and UI navigation repeat distinct; do not redefine every
existing isButtonDown() consumer as held. Terminal keeps an explicit event-only
adapter and cannot claim reliable physical key release.
- Derive one intent from all held movement actions. Opposing directions cancel per axis; Down plus Right produces a diagonal without alternating key presses.
- Use elapsed time and explicit travel distance, not OS repeat rate or painted frames. The stable PHP field metric must account for rectangular horizontal and vertical step dimensions; equal cell frequency is not equal apparent speed. Native scaling, DPI and resize must not alter gameplay speed. Avoid scattered axis-specific speed corrections. Future analog input retains partial magnitude.
- Keep integer cell commits initially, using a distance/time accumulator or scheduled step duration rather than truncating a normalized fractional vector. Direction changes grant no free step; blocked movement banks no burst; catch-up after stalls is bounded. Leave presentation-position interpolation separate: this does not deliver smooth subcell FIELD rendering.
- Commit a diagonal through one validated operation, not two cardinal moves. The destination and both orthogonal clearances must be valid against walls, map edges, NPCs and authored gates. Clearance probes have no movement events, triggers or encounter RNG. Only the occupied destination receives existing movement/event effects, exactly once.
- Document how a committed diagonal counts for step-based systems before implementation. Do not silently rebalance encounter frequency; unresolved gameplay policy goes through the Engine coordinator, not Renderer.
- Follow both camera axes using the existing policy and only the necessary completed field recomposition, retaining T1's gains. Use existing four-direction art with a documented deterministic facing rule, independent of movement vector; a new eight-direction art batch is not a prerequisite.
- Preserve authored route
secondsPerStep, completion and ownership. Reuse the validated movement boundary without replacing route timing with the new free- movement clock. Dialogue, menus, cinematics and battles cancel pending walking; returning cannot replay stale presses or confirm twice. Coordinate Player, input, Camera and session edits with real-subject cinematic takeover.
Deterministic tests must cover held Down/add Right/release Right/release Down; all diagonals and opposing pairs; multiple bindings; repeat-down; quick taps and modifier changes; focus/reset/disconnect/failure/context changes. Compare equal travel over equal simulated time, with a documented one-step quantization bound, under different update subdivisions and both rectangular and square metrics. Verify resize independence, blocked/stalled timing, corner/NPC/gate/map-edge collision, both-axis camera tracking and exactly-once destination effects. Retain keyboard menu, terminal, cinematic route, T1 composition and graphical exclusion regression coverage.
Native acceptance uses one bounded ordinary-Game GPUI pass: simultaneous keys, partial/all release, unobstructed horizontal/vertical/diagonal travel, corners, scrolling, focus loss and dialogue/menu/cinematic entry and return. Measure travel relative to the field, not only a camera-followed on-screen Player. Verify silence before launch without changing Andrew's normal configuration; no scratch-runtime family or user-data changes. State tested platforms honestly.
Physical controllers, controller libraries, enhanced terminal key reporting, continuous subcell animation, renderer upgrades, broad platform ports, new art admission and publishing are not part of this queued first delivery. Before implementation, read the original canonical and workspace policies and establish shared-file ownership; preserve existing work, author files, saves and settings.