Builtin Library
README.md
actions
A name-registered action registry over Zin.bindings + Zin.context.
A name maps to a list of binding descriptors and one declared context;
polling queries (held, pressed, released, value, heldTime,
repeated) evaluate those bindings against the live snapshot, and the
handler surface (bind / onPressed / onReleased / onChanged /
onHeld) fires the same state from inside Zin.tick, after axes and
chords advance.
A control asset replaced this. A <name>.inputBinding/ folder inside an
.inputMap/ carries the label, the kind, and a binding for
keyboard-and-mouse, gamepad and touch, and a world activates the map and
subscribes to the controls it uses:
public = {
inputMap = Field.assetRef("inputMap", "@builtin::inputMaps.default", NoSync, "The controls this component reads."),
speed = Field.number(5, NoSync, "World units per second at full deflection."),
jumps = Field.number(0, NoSync, "How many times the jump control was pressed."),
}
local controls
function awake()
controls = public.inputMap:activate()
controls.move:onInput(function(v, dt, active)
if not active then return end
local p = public.entity.position
local step = public.speed * dt
public.entity.position = { p.x + v.x * step, p.y, p.z - v.y * step }
end)
controls.jump:onPressed(function() public.jumps += 1 end)
end
function onDestroy()
public.inputMap:deactivate(controls)
end
Zin.map.bake(name) writes the currently active map out as an .inputMap
asset. man topics/input covers the control model.
Exports
Registry:
M.define(spec: ActionSpec)— define / replace actions.M.remove(name: string)— remove one action (also tears down handlers).M.clear()— wipe every action + handler.M.has(name: string) -> booleanM.names() -> { string }M.get(name: string) -> ActionEntry?— the registry record behind a name.M.active(name: string) -> boolean— true iff the action's context matches the top of the stack.
Polling queries (gated by context):
M.held(name) / pressed(name) / released(name) -> booleanM.value(name) -> number | Vec2 | 0/1M.heldTime(name) -> number?M.repeated(name, opts?) -> boolean
Event-driven handlers:
M.bind(name, fn, opts?) -> ActionHandle?— register a handler.M.disconnect(handle: ActionHandle) -> booleanM.onPressed / onReleased / onChanged / onHeld(name, fn, opts?) -> ActionHandle?M.handlerCount(name) -> number
Internal hooks (wired by zinput/init.luau):
M._setAllocator(fn: () -> number)— shared id allocator.M._setEnsureBindingsFn(fn)/M._setEnsureLiveFn(fn)— the cold-read hooks.M._owns(handle: number) -> booleanM._dispatchHandlers(firstTickThisFrame: boolean?)— called byZin.tick.M._clearHandlers()— test isolation.
Types:
ActionEntry = { context: string, bindings: { any } }ActionSpec = { [string]: any }— define-spec entry shapes.ActionState = "Begin" | "Change" | "End" | "Held"BindOpts = { priority: number?, fire: { ActionState }?, once: boolean? }ActionHandle = number
Notes
- Define-spec shorthand:
binding— single binding under the default context.{ binding1, binding2 }— multiple bindings under the default context.{ context = "...", binding = ... }/{ context, bindings = {...} }— explicit context.
- Context gating is strict: the action's context string must equal
Zin.context.current(). There is no inheritance up the stack. - Handlers run from
Zin.tickafter axes/chords advance. With no tick host (orZin.autoTick.start()), handlers never fire. - Returning
"sink"from a handler stops further (lower-priority) handlers on the same action this tick. Anything else continues. - A handler also ends with the context that bound it: one a component
binds ends when that component instance goes (removed, hot-reloaded, or
its entity despawned), one a module body binds at the module's next run.
scopes.list()names each as aluaurow under the context it follows. remove(name)andclear()also wipe handlers bound to those actions — handler state never outlives its action.- The handler allocator is swapped at boot (
_setAllocator) so handles drawn here share one namespace withZin.input.on*.