Venus Lua API › Systems
UIFsm
The UI state machine component — request transitions, read current state.
lua/venus_lua/src/lua_ui_fsm.rs
UIFsm
The FsmComponent attached to a UI object by FSM.AttachToObject (or a layout-declared
FSM component). It is the object you request transitions on.
Every per-state callback receives it as argument 2, pre-resolved — these run every frame,
so a per-call lookup there would recur forever:
M.main_menu = {
update = function(self, fsm, data, time, dt)
if start_clicked then fsm:RequestTransition("game") end
end,
}
Module-level UI events are different. They are dispatched by UIScriptBehavior — the general
UI scripting component, which knows nothing about state machines — so nothing is injected. Fetch
the FSM yourself; it fires once per click, not once per frame:
function M.OnClickRelease(self, source_name)
local fsm = UIFsm.Get(self.object)
if fsm then fsm:RequestTransition("game") end
end
The rule is who dispatches you decides what you are handed.
self is the per-instance context table, shared by both. Transitions used to be requested by
assigning self._pending — a magic string field on that same shared table, which collided across
states and silently last-write-wins between a state and its ancestors.
Static calls use the type table (UIFsm.Get(obj)); instance methods use colon syntax
(fsm:RequestTransition(...)).
Types
UIFsm — an opaque handle (userdata) to the FSM component on a UI object. A handle may be
stale (its component or object destroyed); it is safe to keep and pass around. Passing a value
that is not a UIFsm raises a type error, but calling a method on a stale handle never raises — it
returns the documented default.
There is no Create. An FsmComponent is inert without a template and a dispatch, both of which
only the attach path can wire. Create one with FSM.AttachToObject, then reach it with UIFsm.Get.
Functions
UIFsm.Get( UIObject uiObject ) : UIFsm or nil
The FSM component on uiObject, or nil if it has none (no warning).
UIFsm.Exists( UIFsm handle ) : boolean
Whether handle still refers to a live component. false for a non-UIFsm value.
Methods
fsm:RequestTransition( string stateName ) : true, or false, string reason
Requests a transition to stateName. Returns true alone on success, or false plus one of:
| Reason | Meaning |
|---|---|
"unknown_state" |
No state by that name in the template. Also returned for a missing or non-string argument. |
"busy" |
A transition is already in flight. Requests are rejected, never queued. |
"already_pending" |
Another request already claimed this update’s slot. |
"vetoed" |
The source state’s can_transition guard returned false. |
"not_ready" |
The component is uninitialized, or the handle is stale. |
Timing. A request made from outside an FSM update — a button handler, Script.Update —
executes immediately. A request made from inside a state callback takes effect the moment the
update walk finishes; the FSM cannot be mutated mid-walk.
One request per walk, first wins. Callbacks run leaf→root, so a child state’s request beats its
parent’s — the specific handler wins over the general one, which is what a hierarchy implies. The
loser gets "already_pending" rather than being silently dropped.
Completion callbacks cannot transition. enter_complete, exit_complete, and
child_complete all run before the machine latches its new state, so a request from one is
rejected "busy". Chain from the next update instead.
can_transition must be pure. The guard is evaluated when the request is made and again
when it is applied, so a guard with side effects will observe two calls.
fsm:CurrentState() : string or nil
The current state’s name. nil if the component is uninitialized or the handle is stale.
fsm:IsTransitioning() : boolean
Whether a transition is in flight. false on a stale handle.
fsm:Active() : boolean or nil
The component’s own enabled flag — what Activate()/Deactivate() write. nil on a stale handle.
This is intent, not effect: the flag can be on while the owning object is deactivated, in which
case the machine still does not update. Use ActiveInHierarchy() for the effective answer.
fsm:ActiveInHierarchy() : boolean or nil
true only when the own flag is enabled and the owning object is active in hierarchy — i.e. the
component is actually running. This is the predicate the engine uses to gate update dispatch. nil
on a stale handle.
fsm:Activate() : void
Sets the own enabled flag. The enable callback runs only if the owning object is also active.
Silently no-ops on a stale handle.
fsm:Deactivate() : void
Clears the own enabled flag; the disable callback runs only if the component was actually active.
Silently no-ops on a stale handle.
Example
-- A state that leaves on a keypress, and reports why if it can't.
M.pause = {
update = function(self, fsm, data, time, dt)
if Input.KeyDown("esc") then
local ok, reason = fsm:RequestTransition("game")
if not ok then
print("[pause] could not resume: " .. reason)
end
end
end,
}
-- Reaching another object's FSM from outside.
local fsm = UIFsm.Get(panel)
if fsm and not fsm:IsTransitioning() then
fsm:RequestTransition("main_menu")
end