npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@forgeax/engine-input

v0.1.37

Published

Frame-start scanned, frozen InputSnapshot Resource (logical keyboard key and physical code held/press/release edges + mouse.position/movementDelta + mouse.button(0|1|2)) + browser PointerLock backend for forgeax-engine.

Readme

@forgeax/engine-input

Frame-start scanned, frozen InputSnapshot Resource for forgeax-engine. Multi-device surface frozen before user systems run each frame: keyboard (logical key + physical code held/press/release edges) + mouse (position / movementDelta / button held/press/release edges / wheelDelta) + gamepad (7 readpoints: button / buttonValue / justPressed / justReleased / axis / standardMapping / connected) + pointer (per-pointerId position / pressure / delta / phase event queue) + virtualAxis (named joystick readpoint). Higher-level abstractions: action indirection (declare once, forget device — snap.action(name) / snap.getAxis / snap.getVector) + gesture recognizer (pinch / rotate / swipe / long-press / double-tap — snap.gesture + snap.gestureEvents). Capability probe (snap.capabilities) available at attach time.

inputBackendPlugin(backend) provides a host-owned backend without taking its lifetime. ownedInputBackendPlugin(backend, dispose) is the canvas-App form: DOM listener release is an effect of the same provider Fiber. World snapshot resources and scan systems remain a separate consumer capability.

4 步 recipe

import { Update, World } from '@forgeax/engine-ecs';
import {
  attachBrowserInputBackend,
  INPUT_BACKEND_KEY,
  INPUT_MAP_KEY,
  InputFrameStartScan,
  type InputSnapshot,
} from '@forgeax/engine-input';

// 1. attach browser backend (PointerLock + keyboard/mouse listeners) to a canvas
const detach = attachBrowserInputBackend(canvas);

// 2. insert the backend as a Resource + add the frame-start scan system token;
//    it freezes the snapshot into the 'InputSnapshot' Resource before any user
//    system runs (charter P5 split: backend = producer, scan-system + Resource
//    = consumer). Insert the backend BEFORE the system so its ParamValidation
//    finds the resource on the first tick.
const world = new World();
world.insertResource(INPUT_BACKEND_KEY, detach.backend);

// 2b. (optional) declare an InputMap to enable action readpoints.
//     Duplicate action names → last-wins. Per-action deadzone default: 0.2.
world.insertResource(INPUT_MAP_KEY, [
  { action: 'jump', bindings: [{ type: 'key', key: ' ' }, { type: 'gamepadButton', button: 0 }] },
  { action: 'moveLeft',  bindings: [{ type: 'key', key: 'a' }, { type: 'gamepadAxis', axis: 0, sign: -1 }] },
  { action: 'moveRight', bindings: [{ type: 'key', key: 'd' }, { type: 'gamepadAxis', axis: 0, sign: 1 }] },
  { action: 'moveUp',    bindings: [{ type: 'key', key: 'w' }, { type: 'gamepadAxis', axis: 1, sign: -1 }] },
  { action: 'moveDown',  bindings: [{ type: 'key', key: 's' }, { type: 'gamepadAxis', axis: 1, sign: 1 }] },
]);

world.addSystem(Update, InputFrameStartScan);

// 3. user system reads the frozen snapshot via the standard Resource API;
//    multi-device surface: keyboard + mouse + gamepad + pointer + virtualAxis
//    + action + gesture
world.addSystem(Update, {
  name: 'first-person-camera',
  after: ['input-frame-start-scan'],
  queries: [],
  fn: (w) => {
    const snap = w.getResource<InputSnapshot>('InputSnapshot');
    // action: declare once, forget device — same isPressed() for keyboard AND gamepad
    if (snap.action('jump').justPressed()) {/* jump (space or gamepad A) */}
    const move = snap.getVector('moveLeft', 'moveRight', 'moveUp', 'moveDown');
    // move.x / move.y: circular deadzone [−1,1], WASD diagonal magnitude 1
    if (snap.keyboard.down('w')) {/* move forward */}
    const { x, y } = snap.mouse.movementDelta;
    if (snap.mouse.button(0)) {/* primary down */}
    // gamepad: button / buttonValue / justPressed / justReleased / axis
    if (snap.gamepad(0).button(0)) {/* gamepad A pressed */}
    const lx = snap.gamepad(0).axis(0); // left stick X
    // pointer: per-pointerId position + pressure + delta
    const touch = snap.pointer(0);
    if (touch.active) {/* use touch.x, touch.y, touch.pressure */}
    // virtualAxis: named joystick readpoint (zero-vector for unbound)
    const joy = snap.virtualAxis('move');
    // gesture: pinch/rotate continuous values + one-frame lifecycle events
    const gs = snap.gesture.pinchScale;
    const ga = snap.gesture.rotationAngle;
    const gevs = snap.gestureEvents; // GestureEvent[] one-frame lifecycle
    // capability probe (frozen at attach time)
    const hasGamepad = snap.capabilities.gamepad;
  },
});

// 4. on shutdown: detach the listeners so multiple Engine instances do not
//    leak handlers (charter P3 explicit failure: detach is part of the contract)
detach();

多设备表面(charter F2 minimal surface)

keyboard (6 read points)

| 调用 | 返回 | 语义 | |:--|:--|:--| | snap.keyboard.down(key) | boolean | key 当前帧首仍处于按下状态(document.hasFocus() 失焦时保持上一帧状态) | | snap.keyboard.justPressed(key) | boolean | 上一帧未按、本帧按下(一帧寿命;首帧 held key 视为刚按下) | | snap.keyboard.up(key) | boolean | key 上一帧 down、本帧 up 的边沿(一帧内 true,下一帧自动消失) | | snap.keyboard.downCode(code) | boolean | KeyboardEvent.code physical code 当前帧按下;例如 KeyA 不随键盘布局改变 | | snap.keyboard.justPressedCode(code) | boolean | physical code 上一帧未按、本帧按下(一帧寿命) | | snap.keyboard.upCode(code) | boolean | physical code 上一帧按下、本帧释放(一帧寿命) |

键盘和鼠标的 press/release 是 backend 在事件到达时记录的边沿,而不 是扫描时用 held 状态反推。因而一次 keydown → keyuppointerdown → pointerup 若发生在两个帧首扫描之间,仍会在下一份 InputSnapshot 中同时留下 press 与 release;扫描后各边沿只保留一帧。 InputFrameStartScan 每帧只调用一次 sample(),因此用户系统不会看到 半更新的输入状态,也不应自行读取 DOM 事件。

mouse(5 读点)

| 调用 | 返回 | 语义 | |:--|:--|:--| | snap.mouse.position | { x: number; y: number } | undefined | 最新 canvas-pixel 鼠标坐标;悬停时也更新,不需要按键或 pointer capture;尚未收到鼠标事件时为 undefined | | snap.mouse.movementDelta | { x: number; y: number } | PointerLock movementX/Y 自上次扫描以来的累加;扫描后 backend 自动清零 | | snap.mouse.pointerLocked | boolean | 合并锁态——W3C pointerlockchangepointerLockElement === canvas)OR host lockProvider.requestLock() 置位;required 字段,非 optional。consumer 写 if (snap.mouse.pointerLocked) 判断"仅锁定态消费 look delta"。与 movementDelta 同位——两个事实在同一属性路径下(charter F1 单点可索引) | | snap.mouse.button(i) | booleani: 0 \| 1 \| 2) | 对齐 W3C MouseEvent.button:0=主键 / 1=辅助键 / 2=次键。i: 3 触发 TS 编译错误 | | snap.mouse.justPressed(i) / justReleased(i) | boolean | 鼠标按钮帧边沿;首帧 held 按钮视为刚按下 | | snap.mouse.wheelDelta | number | 每帧 sign-discrete 滚轮 notches 累加(W3C WheelEvent.deltaY:正=下滚/负=上滚);frame-start 冻结后清零。OOS-7:轨迹板高精度滚动未展开(sub-notch 量值保留给未来 ScrollSnapshot) |

gamepad(7 读点)

| 调用 | 返回 | 语义 | |:--|:--|:--| | snap.gamepad(i).connected | boolean | slot i 已连接(index-based,null-padded 自动跳过)。越界 / 断连 → false | | snap.gamepad(i).standardMapping | boolean | Standard layout available: true when browser reports mapping==='standard' or the SDL controller DB has normalized a non-standard layout (D-1 redline). false = not standard AND DB miss/unavailable — explicitly detectable (AC-10/E-2). When false, all readpoints return empty signals. | | snap.gamepad(i).button(b) | booleanb: 0\|1\|...\|16) | 按钮 b 当前帧按下(held)。越界字面量触发 TS 编译错误 | | snap.gamepad(i).buttonValue(b) | number | 按钮 b 模拟值(0..1);扳机半程典型 0.5。OOS-4:不施加 gamepad 死区(轴 deadzone 排除) | | snap.gamepad(i).justPressed(b) | booleanb: 0\|1\|...\|16) | 按钮 b 边沿——上帧未按、本帧按下(一帧寿命) | | snap.gamepad(i).justReleased(b) | booleanb: 0\|1\|...\|16) | 按钮 b 边沿——上帧按、本帧释放(一帧寿命) | | snap.gamepad(i).axis(a) | numbera: 0\|1\|2\|3) | 轴 a 原始值(-1..1)。越界字面量触发 TS 编译错误。OOS-4:不施加死区——轴 raw 值直读 |

pointer(per-pointerId reader)

| 调用 | 返回 | 语义 | |:--|:--|:--| | snap.pointer(id).active | boolean | pointerId 当前活跃(触点在 pointerMap 内)。不存在 → false + 全字段空信号 | | snap.pointer(id).x / .y | number | 当前帧末最后 pointermove 的 canvas-pixel 坐标(DPR 换算:(clientX-rect.left)*(canvas.width/rect.width))。getBoundingClientRect 缺失 → clientX/Y 直存(fallback) | | snap.pointer(id).pressure | number | W3C PointerEvent.pressure(0..1)。无压感网 → 缺省 0.5 或 0 | | snap.pointer(id).pointerType | string | 'mouse' / 'pen' / 'touch'——W3C PointerEvent.pointerType | | snap.pointer(id).delta | { x: number; y: number } | 跨帧位移(本帧冻结位置 − 上帧冻结位置)。一帧内 N 次 pointermove → delta 为末端 − 上帧末端(AC-09:非 0,避 Bevy #12442 反例) | | snap.pointerEvents | readonly PointerPhaseEvent[] | 本帧相位事件队列(down / move / up / cancel),一帧寿命——sample() 末尾 drain |

virtualAxis(独立命名空间)

| 调用 | 返回 | 语义 | |:--|:--|:--| | snap.virtualAxis(name) | { x: number; y: number } | 名为 name 的虚拟摇杆向量:|vec|<deadzone(0,0)|vec|>radius → clamp 到模长 1.0;中段 → clamp(cur−origin, radius)/radius。不存在 → (0,0)(空信号)。fixed 模式 origin=anchor;floating 模式 origin=落点 |

capabilities(一次性探针)

| 调用 | 返回 | 语义 | |:--|:--|:--| | snap.capabilities | { gamepad: boolean; pointer: boolean } | 后端 attach 时冻结的环境探针(typeof navigator.getGamepads === 'function' / typeof PointerEvent !== 'undefined')。不须每帧重查 |

action(声明式设备无关语义层)

4-member ActionBinding discriminant union: 'key' | 'mouseButton' | 'gamepadButton' | 'gamepadAxis'. Declared via INPUT_MAP_KEY Resource (ActionConfig[]). Per-action deadzone default: 0.2 (Godot prior-art). Aggregation: OR for pressed, MAX for strength/raw. Duplicate action names: last-wins (D-8).

| 调用 | 返回 | 语义 | |:--|:--|:--| | snap.action(name).isPressed() | boolean | 任意绑定源当前激活(OR 聚合)。未注册 action → false | | snap.action(name).justPressed() | boolean | 上帧未按、本帧按下(一帧寿命)。首帧无 prev → justPressed=pressed | | snap.action(name).justReleased() | boolean | 上帧按下、本帧释放(一帧寿命) | | snap.action(name).strength | number | 死区重映射模拟强度 [0, 1]。\|raw\|<deadzone → 0;\|raw\|≥deadzoneinverse_lerp(deadzone, 1, \|raw\|)。数字绑定 → 1.0 | | snap.getAxis(neg, pos) | number | strength(pos) − strength(neg),值域 [-1, 1]。一端/两端未注册 → 该端贡献 0;同 action 双端 → 恒 0 (E-12) | | snap.getVector(nX, pX, nY, pY, opts?) | {x: number, y: number} | 四行动方向合成 2D 向量,单次径向死区(三分支公式,Godot input.cpp F2):length≤deadzone → (0,0);length>1 → 单位圆 clamp;中段 → vec·inverse_lerp(deadzone, 1, length)/length。默认 deadzone = 4 个 action 的 per-action deadzone 均值;opts.deadzone 覆盖。读 action 的 raw(非 strength)避免逐轴死区叠加成方形死区。 |

WASD 斜向:键盘 a/d/w/s 绑为 getVector('moveLeft','moveRight','moveUp','moveDown') → 斜向模长 1(非 √2;AC-06)。

gesture(触摸手势 recognizer,五类)

Five gesture recognizers running in the backend closure (C-3 single legal cross-frame state). All timers advance off injected now() (D-3), decoupled from pointer-event frequency. Two output channels (D-4): continuous values via snap.gesture, one-frame lifecycle events via snap.gestureEvents. No active gesture → identity empty signal (pinchScale=1.0, rotationAngle=0; AC-12).

| 调用 | 返回 | 语义 | |:--|:--|:--| | snap.gesture.pinchScale | number | 双指捏合缩放比值(累计);无手势 → 1.0。begin 时复位为 1.0,2→1 指抬 end 后冻结,回 2 指 new begin 再复 1.0 (D-11) | | snap.gesture.rotationAngle | number | 双指旋转累计角度(弧度);无手势 → 0。与 pinch 共用同一双指 tracker,同帧 begin/end 成对 | | snap.gestureEvents | readonly GestureEvent[] | 本帧手势生命周期事件队列(一帧寿命,对齐 pointerEvents)。GestureEvent 是封闭判别联合:pinch-begin/end/cancelrotate-begin/end/cancelswipe(含 direction)、long-pressdouble-tap。每个 event 带 pointerType'mouse' \| 'pen' \| 'touch',AC-19 消费路径) |

手势阈值默认表

| 常量 | 值 | 来源 | |:--|:--|:--| | 默认 per-action deadzone | 0.2 | Godot prior-art (F2) | | LONG_PRESS_DURATION_MS | 500 | LayaAir prior-art | | LONG_PRESS_SLOP | 10(canvas px) | LayaAir prior-art | | DOUBLE_TAP_INTERVAL_MS | 350 | LayaAir prior-art | | DOUBLE_TAP_DISTANCE | 10(canvas px) | LayaAir prior-art | | SWIPE_VELOCITY_THRESHOLD | 0.5(px/ms) | up 前 100ms 位移窗口推速度 | | SWIPE_WINDOW_MS | 100 | 速度计算滑动窗口 (D-10) |

engine.run() 启动前调用返回空快照(不抛异常;charter P3 例外:构造阶段无事件源,空快照即"显式无信号")。

形态铁律

  • Resource 形态唯一入口 — 用户胶水通过 world.getResource<InputSnapshot>('InputSnapshot') 消费;不暴露 world.input 平行 API(charter P4 一致抽象)
  • PointerLock 内部消化requestPointerLock 用户激活、pointerlockchange 状态机、movementX/Y 单位陷阱、firstMouse flag 全部封装在 browser-backend.tsInputSnapshot 表面仅暴露 snap.mouse.pointerLocked(合并锁态 boolean)与 snap.mouse.movementDelta(累加 delta),不暴露 W3C DOM 细节(plan-strategy OOS-1)
  • 帧首扫描后冻结InputFrameStartScan system token(顶层 defineSystem)先于其他用户系统跑(用 before 约束),从 INPUT_BACKEND_KEY resource 取 backend、扫描累积器后调用 world.insertResource 写入冻结快照;当帧内任何系统调 snap.* 看到的都是同一份值(architecture-principles #2 Derive)。backend 经 world.insertResource(INPUT_BACKEND_KEY, backend) 注入,descriptor 的 resources:[INPUT_BACKEND_KEY] 走结构化 ParamValidation(依赖未注入 -> invalid,非裸 throw)
  • OOS-1 范围外 — PointerLock 进入/退出事件、unadjustedMovement 选项、hot-reload 不在本 MVP 内;后续作为独立 feat 拆出

错误模型(charter P3 显式失败)

| 行为 | 形态 | |:--|:--| | engine.run() 启动前调用 snap.* | 返回空快照(down/up=false / movementDelta={x:0,y:0} / button=false);不抛 | | keyboard.down(unknownKey) | 返回 false(不抛;未按下即 down=false) | | mouse.button(3) | TS 编译错误(字面量类型 0 \| 1 \| 2 收紧);运行时表面不暴露 | | snap.gamepad(i) 越界 / i 断连 | connected=false + 全读点 false/0(空信号不抛) | | snap.gamepad(i) 非标准布局 | connected=true + standardMapping=false + 全读点空信号(区分断连——显式可检测) | | snap.gamepad(i).standardMapping 语义 | true when browser reports mapping==='standard' or SDL controller DB has normalized a non-standard layout (D-1 redline). false = not standard AND DB miss/unavailable — the downgrade is explicitly detectable (AC-10/E-2) | | snap.action(name) 未注册 | 返回空信号:isPressed()=false / strength=0 / justPressed()=false / justReleased()=false(不抛;AC-01/09) | | snap.gesture 无活跃手势 | pinchScale=1.0 / rotationAngle=0(恒等空信号,不抛;AC-12) | | snap.gestureEvents 无生命周期事件 | 空数组 [](不抛) | | snap.gamepad(i).button(17) / .axis(4) | TS 编译错误(字面量 union 0\|1\|...\|16 / 0\|1\|2\|3 收紧) | | typeof navigator.getGamepads !== 'function' | snap.capabilities.gamepad=false + snap.gamepad(0..N) 全空信号(不抛) | | snap.pointer(id) 不存在 | active=false + 全字段空信号(不抛) | | snap.virtualAxis(name) 不存在 | 零向量 { x: 0, y: 0 }(不抛) | | pointer 移出 canvas | 越界坐标原样保留不 clamp——x/y 可为负或超出 canvas 尺寸(文档化行为,charter P3) |

边界行为

| 场景 | 行为 | |:--|:--| | 越界坐标 | pointer x/y 原样保留不 clamp(可为负或超出 canvas 尺寸);charter P3:显式文档化不隐式行为 | | touch-action | attach 时浏览器 backend 内部设置 canvas.style.touchAction='none'(existential 探测——fake canvas 无 .style 时静默跳过);detach 恢复原值。AI 用户不需手动设 CSS | | 失焦(blur / visibilitychange hidden) | pointerMap 全清 + 每活跃触点 push cancel 相位事件进入队列;W3C/provider pointer lock 立即释放;gamepad justPressed/justReleased 集合复位,并以一次 focus-reset sample 边界抑制旧 snapshot 的 mouse/action release——下帧不喷幽灵边沿。恢复焦点后下次 sample() 自然恢复 | | assemble form — action surface | Assemble-form hosts must manually world.insertResource(INPUT_MAP_KEY, map) to enable action readpoints (D-7). Canvas-form createApp does this automatically. | | controller-db 子导出懒加载 | @forgeax/engine-input/controller-db is dynamically imported only on first connected non-standard gamepad (D-2). Until then (or on DB miss), non-standard slots maintain the Feat1 empty-signal behavior (standardMapping=false). Standard gamepads never trigger the load (C-5). | | 手势 cancel on blur | onBlur resets all active gesture recognizers: emits cancel events for each active gesture type, resets continuous values to identity, and clears timers (AC-18). Prevent ghost gestures on refocus. | | pointer-lock gate = game gate AND host predicate | onCanvasClick evaluates gameGate && (predicate?.() ?? true). gameGate defaults to true and is set via InputBackend.setPointerLockAllowed(). predicate is BrowserInputBackendOptions.pointerLockAllowed (frozen at attach). Both must be true for a click to request lock. | | setPointerLockAllowed(false) immediate release | When gameGate transitions from true to false and a lock is active, the backend immediately releases: W3C path calls exitPointerLock(), provider path calls exitLock() + clears providerLocked. This solves the "mode switches to top-down while locked" boundary. | | lockProvider error → onLockError | W3C requestPointerLock promise rejection and provider requestLock throw/reject both route through onLockError({ path, cause }). Provider failure also rolls back providerLocked = false. The callback is wired by attachInputAuto into AppError({ code: 'app-pointer-lock-failed' }) on the app's onError fan-out. |

Pointer Lock is attempted from the trusted canvas click when the two explicit game/host gates allow it; document.hasFocus() is not a third gate because a browser may report a transient false value during the same activation. Both a rejected W3C request and the browser pointerlockerror event are converted to the same onLockError diagnostic and are caught before they can become an unhandled Promise rejection. Lock state remains a boolean readpoint in the snapshot; DOM error events never cross the consumer boundary.

Pointer-lock contract: PointerLockProvider / lockProvider / setPointerLockAllowed / pointerLocked

Four anchors cover the full pointer-lock surface across the input-backend boundary.

PointerLockProvider (type SSOT)

// @forgeax/engine-input — exported from barrel
interface PointerLockProvider {
  requestLock(): void | Promise<void>;  // request pointer capture (W3C replacement)
  exitLock(): void;                      // release pointer capture
}

A single object (not two flat callbacks) — structural typing requires both halves, preventing "requestLock-only" half-injected states. The engine never learns why locking is (dis)allowed; it delegates to the abstract callback.

BrowserInputBackendOptions.lockProvider (injection point)

// BrowserInputBackendOptions — optional field, parallel to pointerLockAllowed
interface BrowserInputBackendOptions {
  pointerLockAllowed?: () => boolean;
  lockProvider?: PointerLockProvider;      // absent => fall back to W3C requestPointerLock()
  // ... other options
}

When lockProvider is present, onCanvasClick calls requestLock() instead of requestPointerLock(). The provider's exitLock() replaces exitPointerLock() in release paths (ESC provider branch, blur, detach, setPointerLockAllowed(false) immediate release).

InputBackend.setPointerLockAllowed (command gate)

// InputBackend protocol — optional method
interface InputBackend {
  sample(): InputBackendSample;
  setPointerLockAllowed?(allowed: boolean): void;  // game-side gate
  detach(): void;
}
  • setPointerLockAllowed(true) — allow pointer-lock on next trusted click
  • setPointerLockAllowed(false) — block pointer-lock AND immediately release any active lock (W3C path: exitPointerLock; provider path: exitLock() + clears providerLocked)
  • Default state: true (allow lock)
  • Optional — backends without pointer-lock support omit this method

This works together with the pre-existing host predicate pointerLockAllowed?: () => boolean (evaluated fresh on each click, e.g. "is input focus in the game quadrant?"). Two gates AND-combine: both must be true for lock; neither duplicates the other's information.

InputBackendSample.pointerLocked (readpoint)

// InputBackendSample — required field (not optional)
interface InputBackendSample {
  // ... movementX/Y, buttons, downKeys, upKeys, etc.
  readonly pointerLocked: boolean;  // W3C pointerlockchange OR provider requestLock engage
}

Frozen into snap.mouse.pointerLocked at frame-start. Consumers read if (snap.mouse.pointerLocked) to gate look/camera rotation — both facts (movementDelta + pointerLocked) sit at the same attribute path for single-point indexing (charter F1).

相关包

  • @forgeax/engine-ecs — 提供 World + Resource 存储 + Schedule(消费方契约依赖:world.insertResource('InputSnapshot', frozenSnapshot)
  • @forgeax/engine-runtime — 在 Engine.create({ canvas }) 流水线里调用 attachBrowserInputBackend(canvas)world.insertResource(INPUT_BACKEND_KEY, backend)world.addSystem(Update, InputFrameStartScan),对 AI 用户屏蔽组装细节

Simulation input boundary

Use InputSnapshot as the frame-start boundary for simulation input. A simulation trace stores the normalized value consumed by a fixed tick; it does not store DOM events, browser objects, or a render frame.

const snapshot = inputBackend.snapshot();
const sample = { tick: fixedTick + 1, input: snapshot.action('moveForward').isPressed() };

The input package freezes the snapshot. ECS owns record, restore, trace, participant registration, comparison tolerance, and closed simulation errors. The current snapshot does not retain previous snapshots after edge derivation; holding one reader must not retain an unbounded frame history. Use the same snapshot source for source and fresh target Worlds. Do not add a second input recorder, a replay action, or a browser event queue to Remote.

The simulation connector accepts package-produced snapshots from the public snapshotFromSample / createInputSnapshot path. If an InputSnapshot Resource is replaced by a structurally similar value without package-owned provenance, the World update does not throw or publish a partial sample; finish() returns the existing simulation-trace-invalid error with detail.path === 'InputSnapshot.provenance'. Consumers do not author or copy private provenance fields. Dispose the failed controller, provide a corrected snapshot source, and install the connector on a fresh target World.

If a trace fails, inspect code, expected, hint, and detail, repair the named input boundary, then create a fresh target. RHI tape replay and game replay have different owners and are not input snapshot formats.