Builtin Library
README.md
probe
Get told when something happens in the running world, without editing anything.
A probe attaches to code that is already running. You say what to watch, what condition matters, and what to compute at that instant; you get a notice on a later tool call saying it happened and carrying the value. You never wait, you never guess a delay, and there is nothing to remember to undo — no source file is touched, nothing reloads, and cancelling removes every trace.
This is what to reach for instead of adding a log line to a component and reloading it, and instead of running a script, waiting, and capturing in the hope the timing was right.
The three things you can watch
| Tool | Fires when |
|---|---|
onCall | A named function runs. Sees its arguments and what it returned. |
onLine | Execution reaches a file and a line. Sees that frame's locals. |
onChange | A value becomes different from what it was. |
And three to manage them: list (what is armed and what it has seen), hits
(the detail behind the notices), cancel (take one off, or all).
What a probe tells you
Three parts, every time:
- Where it came from — which probe, and what it is attached to. Written by the probe, so several armed at once never blur together.
- What it is about — your
noticetext. Required. A value with no idea what it is about is not a report. - What your body returned — a string becomes the line, a table becomes the values beside it.
probe.onCall("modules.combat.applyDamage", {
notice = "a hit killed something",
when = "ret.killed",
run = "return ret.target .. ' at ' .. tostring(ret.hp) .. 'hp'",
})
That reads back as its source, then "a hit killed something", then
goblin at -40hp. Repeat hits collapse into one line with a count and the
values that differed, which is why the text stays fixed and the value varies.
Seeing the moment
Pass capture to have a hit bring back a picture as well: Luau, read against
the hit's context the way run is, that aims a capture tool at what the hit is
about and returns the picture.
probe.onCall("modules.combat.applyDamage", {
notice = "a hit killed something",
when = "ret.killed",
capture = "return tools.use('capture', 'fromEntity', { entity = args[1], angle = { 30, 20 } })",
freeze = true,
})
A capture waits for the renderer, so it runs as the hit is recorded rather
than inside the code the probe is attached to; the hit's notice goes out once
the picture is back and carries it under shot, delivered to you as a picture
beside the line, and the picture says which frame it is of. probe.hits holds
the same picture on the hit's row. On onCall, freeze also stops the world
at the hit. A capture that fails still reports the hit, with captureError
saying why.
Picking a target
- A function on a required module —
onCall, bymodule.field. - A local inside a module, or a helper a component keeps to itself —
onLine, by chunk and line. This is the one that reaches code no name can address. - A value: a component field, a light's intensity, a position —
onChange.
Pair any of them with when to name the moment rather than every step toward
it. when = "value >= 5" fires the first time it reaches five.
A line probe's when is evaluated inside the frame it stopped in, so it reads
that frame's variables by their own names and with their own types —
doubled >= 6. The other two read the context: ret.killed, value >= 5.
What it costs
onCallcosts one extra call on the wrapped function, and nothing anywhere else.onLinecosts nothing on an unarmed line, but a breakpoint drops the enclosing function out of native code until that script reloads. On a function that runs every frame that is real, so cancel it when done.onChangereads its expression once per frame while armed, which is also its limit: something that changes and changes back within a frame is not a change it can see. Probe the code that sets it withonLineinstead.
Every probe carries a hit limit and a cooldown, both defaulted low. A probe
that reaches its limit disarms and says so; hits the cooldown held back are
counted and shown by list, so a busy probe never leaves you believing it
fired only as often as it spoke.
Where it stops
Nothing about a probe is written to disk and nothing survives the engine stopping. A body runs inside a protected call and must not yield; three errors in a row disarm the probe and report why.
Globals are sealed read-only, so onCall cannot wrap one. A component's own
method is not a field on a module either — reach it with onLine.