Log inGet started
World

Builtin Library

Branchmain
Branchesmainpublish
Commit3bb1da2647

README.md

observe

What became of a device event, and why a live control did not fire. Backs Zin.observe.

A control can be live, its device can be producing input this frame, and the game can still receive nothing. The input layer works out why every frame — a gate said no, a live map stood the group down, the UI took the pointer, nothing subscribed — and until something reads that decision it is thrown away. This module keeps it.

The one question

local Zin = require("@builtin::modules.zinput")

Zin.observe.whySilent("look")
--> {
--    name = "look",
--    layer = "scheme",
--    map = "default",
--    reason = "gateRefused",
--    means = "the control's own gate answered no. A look control gates on
--             the player steering, so a cursor crossing the window reads
--             nothing.",
--    kind = "axis2",
--    subscribers = 1,
--    declaredClasses = { "kbm", "gamepad", "touch" },
--    carriedBy = { "scheme" },
--  }

One reason, from a closed set, with the particulars behind it. Zin.observe.reasons() lists the whole set with a sentence each, so what an answer means is readable from inside the engine rather than only from the source.

The closed set, nearest cause first

ReasonWhat it says
noMapActiveNo input map is activated at all, so there are no controls.
unknownControlNo live map declares a control under that name.
groupSuppressedA live map stands this control's group down.
contextInactiveThe control declares a context that is not on top of the stack.
heldOnArrivalA device the control binds was already held when the control went live. The control reads the hold as held; pressed and released wait for the next press.
gateErroredThe control's own gate raised.
gateRefusedThe control's own gate answered no.
noBindingForDeviceThe control declares no binding for any device class this session has.
belowDeadzoneThe devices produced a reading and the control's own deadzone zeroed it.
noSubscriberThe control is delivering and nothing subscribed to it.
atRestThe control is live and its devices are not being driven.
deliveringThe control is delivering its value to its subscribers.

atRest and unknownControl are the two an outside reader could never tell apart: a value reader answers 0 for both. This is where they separate.

Which layer answered

Three naming layers coexist: the live maps' controls, and the Zin.actions and Zin.axes registries a control asset replaced. Their name sets are separate — toolMove is a live control and not an action, move_forward is an action and not a control — so a name that is live in one reads as silence in the others. Every answer carries layer (the one that answered) and carriedBy (every layer that has the name at all).

One control, everything about it

Zin.observe.control("move")
--> {
--    name = "move", live = true, carriedBy = { "scheme" },
--    entries = { { map = "default", kind = "axis2", subscribers = 1,
--                  gated = false, classes = { kbm = {...}, gamepad = {...} } } },
--    lastTick = { value = { x = 1, y = 0 }, active = true, delivered = true },
--    why = { reason = "delivering", ... },
--  }

The frame

Zin.observe.frame()
--> { frameId = 41207, window = "one tick of binding dispatch — the most
--    recent one", context = "default", maps = {...}, controls = {...},
--    cost = { frameId = 41207, maps = 3, controls = 27, ms = 0.09 } }

The window is one tick — the most recent one — rather than a sum since anything last looked, and reading takes nothing away from the next reader. Two observers in the same frame both get the truth.

cost.ms is that one tick's own milliseconds. The same number lands in profiler.stats("*zin.scheme.advance*") as script.zin.scheme.advance, where it carries the average and the peak across every tick as well.

Reading it without running a script

/runtime/input serves the same answers as a file, beside the engine's own account of the frame's device events:

/runtime/input            the whole document
/runtime/input/events     the frame's device events, each with `consumed_by_ui`
/runtime/input/device     pointer lock, UI focus, the session's device classes
/runtime/input/mapping    this module's account of the tick
/runtime/input/controls/<name>   one control's entry from that account

input.observe() returns the same document to a script.

Building this layer's half costs a frame of work, so it is built only while something is reading: a read of either surface arms it for a window of frames, and the document's mappingStatus says whether the half it carries is current, stale, arming (this read armed it — read again), or unarmed. Zin.observe.armedFrames() is how many frames that window has left; each frame that passes takes one off it, and the per-frame publish spends the window rather than renewing it, so it runs out once the last reader stops asking. A window that runs out — or that Zin.observe.arm(false) closes — drops the report the engine was carrying with it, so a frame nobody observes costs what it cost before anything ever read. Zin.observe.arm(true) holds it open for a tool that wants it continuously.

Which frame an answer covers

Zin.observe.frame() and Zin.observe.control() report the most recent tick of binding dispatch. Zin.observe.whySilent() resolves against the devices as they are at the moment of the call, so it answers for a control the tick has never reached and for a name that does not exist.

The engine publishes its own half at the end of a frame, after the frame has been drawn and before the frame's events are cleared. A read taken from a script during frame N therefore answers for frame N-1.