Venus Lua API

Scripting reference for the Venus engine, exposed to Lua 5.5. Every page here is
transcribed directly from the FFI bindings in lua/venus_lua/src/lua_*.rs — the registration
arrays in that source are the single source of truth for what Lua can call.

Two audiences:

  • Scripters — look up a type in the sidebar, read its methods.
  • Auditors — the Coverage & Audit page lists every type and its authored/stub
    status, so the exposed surface can be reviewed for gaps and unintended exposure.

The engine language reference (standard library, syntax) is the stock
Lua 5.5 manual. This site documents only the engine
API added on top of it.

How to read a signature

Entries follow the convention `signature` : **ReturnType** with a description that always
states the null / error behavior.

GameObject.Create( ) : GameObject
A static call on the type table — dot syntax. Creates and returns a new object.

gameObject:SetLocalPosition( Vector3 position ) : void
An instance method — colon syntax. The receiver (here gameObject, a lower-cased type name)
is passed implicitly as self; you do not write it in the argument list.

Conventions used throughout:

  • Receiver naming. Instance methods show a lower-cased receiver (gameObject:, light:,
    kc:). Static/constructor calls use the capitalized type table (GameObject., Light.).
  • Properties appear as a getter / setter pair: Foo() reads, SetFoo(v) writes.
  • Null / stale handles are safe to hold. Calling a method on a destroyed or null handle
    logs a warning and returns nil (or a documented default such as false / 0) — it does
    not raise. Each entry states its specific behavior.
  • void means the function returns nothing (return 0 values to Lua).
  • Multi-value returns are written **number, number, number** — e.g.
    gameObject:WorldPosition() returns three f64 numbers, not a Vector3, to preserve
    precision at range.
  • Native math types (Vector2/3/4, Quaternion) are zero-allocation Lua values, not
    handles. Many setters that take a Vector3 also accept three loose numbers.

Page status

Each type page is badged:

  • authored — signatures verified against the
    function bodies, null behavior and examples written.
  • stub — the complete method list is present (the
    audit surface), but signatures are best-effort drafts pending verification.

Categories

Core · Math · Rendering · Physics · Animation · Audio · UI · Input & Time · Systems — see
the sidebar. Start with GameObject, Vector3, or
Physics.

Coverage & Audit

The flat completeness view: every documented type/global, the binding source it comes from, and
its authoring status. 49 pages — all authored: signatures verified against the function
bodies, with per-entry null/error behavior. Where verification surfaced a defect, the page
carries a > **Known issue:** blockquote (23 pages do, 29 blockquotes in total).

Completeness is maintained by hand (no extraction tool): each page’s method list is transcribed
directly from the *_LIB / *_MEMBERS_LIB / *_METHODS (luaL_Reg) arrays in its source file.
When a binding changes, re-transcribe from those arrays and update the page. Every Lua-facing
name in the bindings is a c"..." literal in one of those arrays, so the arrays are the
authoritative surface to diff against.

  • authored — signatures verified against the
    function bodies; null behavior and an example written.
  • stub — complete method list present; signatures are
    best-effort drafts pending verification.
Type Category Source (lua/venus_lua/src/) Status
GameObject Core lua_gameobject.rs authored
Behavior Core lua_behavior.rs authored
Script Core lua_script.rs authored
Vector2 Math lua_math.rs authored
Vector3 Math lua_math.rs authored
Vector4 Math lua_math.rs authored
Quaternion Math lua_math.rs authored
Camera Rendering lua_camera.rs authored
Light Rendering lua_light.rs authored
Lighting Rendering lua_lighting.rs authored
Material Rendering lua_material.rs authored
Mesh Rendering lua_mesh.rs authored
MeshRenderer Rendering lua_mesh_renderer.rs authored
SkinnedMeshRenderer Rendering lua_skinned_mesh_renderer.rs authored
Texture Rendering lua_texture.rs authored
RenderTexture Rendering lua_render_texture.rs authored
Shader Rendering lua_shader.rs authored
Physics Physics lua_physics.rs authored
Collider Physics lua_collider.rs authored
KinematicController Physics lua_kinematic_controller.rs authored
DynamicCharacter Physics lua_dynamic_character.rs authored
FixedRigidBody Physics lua_fixed_rigid_body.rs authored
FixedJoint Physics lua_fixed_rigid_body.rs authored
AnimationClip Animation lua_anim_clip.rs authored
AnimController Animation lua_anim_controller.rs authored
AnimState Animation lua_anim_controller.rs authored
AnimationMask Animation lua_anim_mask.rs authored
AudioSource Audio lua_audio_source.rs authored
AudioClip Audio lua_audio_clip.rs authored
AudioBuffer Audio lua_audio_buffer.rs authored
UI / UIObject UI lua_ui.rs authored
UIText UI lua_ui_text.rs authored
UITexture UI lua_ui_texture.rs authored
UIButton UI lua_ui_button.rs authored
Font UI lua_ui_text.rs authored
Input Input & Time lua_input.rs authored
Time Input & Time lua_time.rs authored
Screen Input & Time lua_screen.rs authored
Network Systems lua_network.rs authored
Mod Systems lua_mod.rs authored
Gizmo Systems lua_gizmo.rs authored
FSM Systems lua_fsm.rs authored
FixedUpdateFSM Systems lua_fixed_update_fsm.rs authored
UIFsm Systems lua_ui_fsm.rs authored
ItemDB Systems lua_inventory.rs authored
Inventory Systems lua_inventory.rs authored
Equipment Systems lua_inventory.rs authored
Save Systems lua_save.rs authored
SaveRecord Systems lua_save.rs authored

Venus Lua API › Core

GameObject

Scene node — hierarchy, transforms, lifecycle.

authored lua/venus_lua/src/lua_gameobject.rs

GameObject

A node in the 3D scene graph: a transform (position / rotation / scale), a place in the parent
→ child hierarchy, and a lifecycle (active / inactive / destroyed). Components — renderers,
colliders, lights, controllers, scripts — attach to a GameObject and read its transform.

Static calls use the type table (GameObject.Create()); instance methods use colon syntax on a
handle (gameObject:SetLocalPosition(...)).

Types

GameObject — an opaque handle (userdata) to a scene node. A handle may be null or stale
(its object destroyed); it is safe to keep and pass around. Calling a method on a null/stale
handle logs a warning and returns nil (or a documented default) — it never raises.
tostring(gameObject) yields GameObject{name}, or GameObject{NULL} if dead.

Creation & Destruction

GameObject.Create( ) : GameObject
Creates a new, active GameObject at the origin. Returns nil and logs an error if the scene
object pool is exhausted.

GameObject.CreateInactive( ) : GameObject
Same as Create, but the object starts disabled (its components do not run until activated).

GameObject.Instantiate( GameObject source ) : GameObject
Recursively clones source and its children. Returns nil and logs a warning if source is
null.

GameObject.Get( string name ) : GameObject
Finds a GameObject by its unique name. Returns nil and logs a warning if no match exists.

GameObject.Exists( GameObject object ) : boolean
Returns true if the handle refers to a live object. Safe on null/stale handles (returns
false) — this is the one call that never warns.

gameObject:Destroy( ) : void
Destroys the object (and its children) if it exists; logs a warning otherwise. Destruction is
deferred to the next safe point, so calling it on self from a callback is safe.

Functions

Read-only queries. Every getter returns nil and logs a warning on a null/stale handle unless
noted.

gameObject:Active( ) : boolean
The object’s own enabled flag, ignoring ancestors.

gameObject:ActiveInHierarchy( ) : boolean
true only if this object and every ancestor are active.

gameObject:Parent( ) : GameObject
The parent, or nil if the object is a scene root (a root is not an error — no warning).

gameObject:Root( ) : GameObject
The topmost ancestor (itself if already a root).

gameObject:Children( ) : table
An array (1-based) of the direct children.

gameObject:ChildCount( ) : integer
Number of direct children.

gameObject:GetChild( integer index ) : GameObject
The child at index (1-based); nil if out of range.

gameObject:FindChild( string name ) : GameObject
Depth-first search for a descendant named name; nil if none.

gameObject:IsChildOf( GameObject parent ) : boolean
true if parent is this object’s ancestor.

gameObject:IsRoot( ) : boolean
true if the object has no parent.

gameObject:FirstChild( ) : GameObject
First direct child, or nil.

gameObject:NextSibling( ) : GameObject
Next sibling under the shared parent, or nil.

gameObject:PreviousSibling( ) : GameObject
Previous sibling, or nil.

gameObject:Forward( ) : Vector3
World-space forward axis (+Z rotated by world rotation).

gameObject:Right( ) : Vector3
World-space right axis.

gameObject:Up( ) : Vector3
World-space up axis.

gameObject:TransformPoint( Vector3 point ) : Vector3
Transforms point from local to world space (scale, rotation, translation). Accepts a Vector3
or three numbers.

gameObject:TransformVector( Vector3 vector ) : Vector3
Local → world using scale and rotation (no translation).

gameObject:TransformDirection( Vector3 direction ) : Vector3
Local → world using rotation only.

gameObject:InverseTransformPoint( Vector3 point ) : Vector3
World → local (scale, rotation, translation).

gameObject:InverseTransformVector( Vector3 vector ) : Vector3
World → local using scale and rotation.

gameObject:InverseTransformDirection( Vector3 direction ) : Vector3
World → local using rotation only.

Properties (get / set)

Setters return void and no-op with a warning on a null/stale handle.

ID

gameObject:ID( ) : string — the object’s unique name.
gameObject:SetID( string name ) : void — renames the object.

LocalPosition

gameObject:LocalPosition( ) : Vector3 — position relative to the parent.
gameObject:SetLocalPosition( Vector3 position ) : void — accepts a Vector3 or three
numbers; three numbers preserve f64 precision through the floating-origin split.

LocalRotation

gameObject:LocalRotation( ) : Quaternion — rotation relative to the parent.
gameObject:SetLocalRotation( Quaternion rotation ) : void

LocalScale

gameObject:LocalScale( ) : number — uniform local scale.
gameObject:SetLocalScale( number scale ) : void

LocalEulerAngles

gameObject:LocalEulerAngles( ) : Vector3 — local rotation as YXZ Euler angles, in
degrees.
gameObject:SetLocalEulerAngles( Vector3 degrees ) : void

WorldPosition

gameObject:WorldPosition( ) : number, number, number — the precise world position as three
f64 numbers x, y, z (not a Vector3, which would be f32-lossy at range):
local x, y, z = go:WorldPosition().
gameObject:SetWorldPosition( Vector3 position ) : void — accepts a Vector3 or three f64
numbers.

WorldRotation

gameObject:WorldRotation( ) : Quaternion
gameObject:SetWorldRotation( Quaternion rotation ) : void

WorldScale

gameObject:WorldScale( ) : number — accumulated world scale.
gameObject:SetWorldScale( number scale ) : void

EulerAngles

gameObject:EulerAngles( ) : Vector3 — world rotation as YXZ Euler angles, in degrees.
gameObject:SetEulerAngles( Vector3 degrees ) : void

Layer

gameObject:Layer( ) : integer — the render/collision layer index.
gameObject:SetLayer( integer layer ) : void

Euler angle order variants

For each of the six rotation orders there are four accessors: world/local getter (returns a
Vector3 of degrees) and world/local setter (takes a Vector3 or three numbers of degrees).
EulerAngles / SetEulerAngles above are the default (YXZ) forms.

Order Get world Get local Set world Set local
XYZ EulerAnglesXYZ() LocalEulerAnglesXYZ() SetEulerAnglesXYZ(v) SetLocalEulerAnglesXYZ(v)
XZY EulerAnglesXZY() LocalEulerAnglesXZY() SetEulerAnglesXZY(v) SetLocalEulerAnglesXZY(v)
YXZ EulerAnglesYXZ() LocalEulerAnglesYXZ() SetEulerAnglesYXZ(v) SetLocalEulerAnglesYXZ(v)
YZX EulerAnglesYZX() LocalEulerAnglesYZX() SetEulerAnglesYZX(v) SetLocalEulerAnglesYZX(v)
ZXY EulerAnglesZXY() LocalEulerAnglesZXY() SetEulerAnglesZXY(v) SetLocalEulerAnglesZXY(v)
ZYX EulerAnglesZYX() LocalEulerAnglesZYX() SetEulerAnglesZYX(v) SetLocalEulerAnglesZYX(v)

Methods

Actions that mutate the object. All no-op with a warning on a null/stale handle.

gameObject:Activate( ) : void
Enables the object and recursively updates children.

gameObject:Deactivate( ) : void
Disables the object and recursively updates children.

gameObject:SetParent( GameObject parent ) : void
Reparents the object, keeping it in the scene. parent may be a GameObject handle or a
string name. No-op with a warning if either object is invalid or the reparent would form a cycle.

gameObject:MakeRoot( ) : void
Detaches from the current parent, making the object a scene root.

gameObject:DetachChildren( ) : void
Unparents every direct child, making each a root.

gameObject:Translate( Vector3 delta ) : void
Moves the object by delta. Accepts a Vector3 or three numbers.

gameObject:Rotate( Quaternion rotation ) : void
Applies rotation on top of the current rotation.

gameObject:RotateAround( Vector3 point, Vector3 axis, number angle ) : void
Rotates the object angle radians about the world-space axis through point. point and
axis each accept a Vector3 or three numbers. (Note: unlike the SetEulerAngles* setters,
which take degrees, this angle is radians.)

gameObject:SetPositionAndRotation( Vector3 position, Quaternion rotation ) : void
Sets world position and rotation in one call. position accepts a Vector3 or three f64
numbers (three numbers preserve precision).

gameObject:LookAt( Vector3 target ) : void
gameObject:LookAt( Vector3 target, Vector3 up ) : void
Orients the object’s forward axis toward target. The optional up biases roll (defaults to
world up). target / up accept a Vector3 or three numbers.

Example

-- Orbit a child marker around its parent and keep it facing the parent's origin.
-- (Adapted from data/scripts/camera_orbit.lua.)
function Script.Update(self)
    local go = self.object
    local t  = Time.Time()
    local r  = 5.0

    -- Position relative to the parent, on a circle in the XZ plane.
    go:SetLocalPosition(math.cos(t) * r, 1.5, math.sin(t) * r)

    -- Face the parent's world origin (three f64 numbers, precise at range).
    local px, py, pz = go:Parent():WorldPosition()
    go:LookAt(px, py, pz)
end

Venus Lua API › Core

Behavior

ScriptBehavior component — a live Lua script bound to a GameObject.

authored lua/venus_lua/src/lua_behavior.rs

Behavior

A Behavior is the handle to a ScriptBehavior component: the running instance of a
Script asset attached to a GameObject. Behavior.Attach creates one
from a loaded Script, giving the instance its own duplicated copy of the script’s function
references; the component then receives the script’s lifecycle callbacks.

Static calls use the type table (Behavior.Attach(...)); instance methods use colon syntax on a
handle (behavior:Activate()).

Types

Behavior — an opaque 8-byte handle (userdata) to a ScriptBehavior component. A handle may go
stale (its component destroyed). The two failure modes differ:

  • Calling an instance method on a value that is not a Behavior userdata — including nil
    raises a Lua type error. check_handle rejects any udata whose embedded type_id byte
    isn’t the ScriptBehavior id, so it never silently returns nil the way the GameObject getters
    do.
  • A stale but correctly-typed handle is tolerated: each method degrades as documented below
    (nil, false, or a silent no-op).

tostring(behavior) always yields the constant string Behavior — it carries no object name and
does not change when the handle goes stale.

Creation & Destruction

Behavior.Attach( GameObject object, Script script ) : Behavior or nil
Creates a new ScriptBehavior on object using the functions from the script asset (each
instance gets its own duplicated function refs). Raises a Lua type error if object is not a
GameObject or script is not a Script. Returns nil and logs otherwise: NotFound if the script
cache is unavailable or the script index is not loaded, InvalidArgument if the component cannot
be added (e.g. object is stale).

Behavior.Get( GameObject object ) : Behavior or nil
Returns the ScriptBehavior already attached to object. Raises a Lua type error if object is not
a GameObject; returns nil and logs a warning (NotFound) if the object has no ScriptBehavior.

Behavior.Exists( Behavior behavior ) : boolean
The safe test. Uses luaL_testudata, so any non-Behavior value (including nil) returns false
without raising or warning. Returns true only if the handle resolves to live component data.

behavior:Destroy( ) : void
Destroys the component if the handle is live; silently no-ops on a stale handle (the result is
discarded — no warning, unlike GameObject:Destroy). Destruction is deferred to the next safe
point.

Functions

behavior:GameObject( ) : GameObject or nil
The GameObject this behavior is attached to (pushed with its cached slot resolved, so the returned
handle’s transform getters take the fast path). Returns nil and logs a warning (NotFound) if
the owner cannot be resolved (stale handle).

behavior:Active( ) : boolean or nil
The component’s own enabled flag — what Activate()/Deactivate() write. Mirrors
GameObject:Active(). Returns nil and logs a warning (NotFound) on a stale handle, so a
destroyed behavior is distinguishable from a merely disabled one.

Note this is intent, not effect: a behavior whose own flag is enabled but whose owning
GameObject is deactivated still reports true here — and does not run. Use
ActiveInHierarchy() for the effective answer.

behavior:ActiveInHierarchy( ) : boolean or nil
true only when this behavior’s own flag is enabled and its owning GameObject is active in
hierarchy — i.e. the behavior is actually receiving OnUpdate. This is the predicate the engine
itself uses to gate dispatch. Returns nil on a stale handle.

Fixed 2026-07 (was: behavior:Active() returned handle liveness, not enabled state — it
called get_component_data, which applies no active-flag filter, so it stayed true after
behavior:Deactivate() and duplicated Behavior.Exists). Active() now reads the component’s
enabled flag and ActiveInHierarchy() was added, matching the C reference’s
IcoComponent_ActiveSelf / IcoComponent_ActiveInHierarchy split. The three predicates are now
distinct: Behavior.Exists = the handle resolves; Active = own flag; ActiveInHierarchy =
own flag AND owner active.

Methods

behavior:Activate( ) : void
Sets the component’s own enabled flag. Its enable callback runs only if the owning GameObject is
also active. Silently no-ops on a stale handle — the result is discarded, so no warning is logged.

behavior:Deactivate( ) : void
Clears the component’s own enabled flag; the disable callback runs only if the behavior was
actually active. Silently no-ops on a stale handle.

Example

-- Load a script once, then attach it as a behavior on a new object.
local script = Script.Create("spinner", "scripts/spinner.lua")

local go = GameObject.Create()
local behavior = Behavior.Attach(go, script)

-- Attach can fail (returns nil) — guard before using the handle.
if Behavior.Exists(behavior) then
    behavior:Activate()
end

-- The three predicates answer three different questions.
behavior:Deactivate()
print(Behavior.Exists(behavior))     --> true   (the handle still resolves)
print(behavior:Active())             --> false  (its own flag is off)
print(behavior:ActiveInHierarchy())  --> false  (so it is not running)

behavior:Activate()
go:Deactivate()
print(behavior:Active())             --> true   (own flag untouched by the owner)
print(behavior:ActiveInHierarchy())  --> false  (but the owner is off, so it still isn't running)

Venus Lua API › Core

Script

Cached Lua script asset — load once, attach many.

authored lua/venus_lua/src/lua_script.rs

Script

A Script is a named, cached asset holding the function references parsed from a .lua file.
Scripts are loaded once into a fixed-size cache (max 256) and retrieved by name; a
Behavior is what actually runs one on a GameObject.

Static calls use the type table (Script.Create(...)); the one instance method uses colon syntax
on a handle (script:ID()).

Types

Script — an opaque 8-byte handle (userdata) into the script cache. The handle encodes
cache_index + 1 in a Handle64 with a fixed generation of 1: scripts are append-only and never
recycled, so a handle stays valid for the life of the Lua state. tostring(script) yields
Script{name}, or the bare string Script if the entry cannot be resolved.

The safe test is Script.Exists (never raises). By contrast script:ID() runs check_handle,
which raises a Lua type error if called on a value that is not a Script userdata.

Creation & Destruction

Script.Create( string name, string filename ) : Script or nil
Loads the script at filename (resolved against the cache’s base directory) and registers it
under name. name and filename must be strings — a non-string argument raises, and non-UTF-8
bytes raise a Lua argument error. Returns nil if the cache pointer is unset (silent) or the file
fails to load (silent); if the cache is full it returns nil and prints [Script] Cache full… to
stderr.

Known issue: Script.Create is idempotent on name alone — if a script with that name is
already cached it returns the existing handle and ignores the new filename
(lua_script.rs:311). Registering two different files under one name is silently impossible; the
first load wins. There is also no Lua-side unload or replace (the cache is append-only, capacity
256); reloading from disk is a Rust-only path (ScriptCache::reload_all).

Script.Get( string name ) : Script or nil
Returns the handle for an already-loaded script name. name must be a string (non-string /
non-UTF-8 raises). Returns nil — with no warning — if the cache is unset or no script by that
name is registered.

Script.Exists( Script script ) : boolean
The safe test. Returns false for a null pointer, any userdata whose length isn’t 8 bytes, or any
whose embedded type_id isn’t the Script id; otherwise checks that the cache slot is loaded. Never
raises — safe on null / wrong-type / unresolved handles.

Functions

script:ID( ) : string or nil
The script’s registered name. Raises a Lua type error if called on a non-Script value; returns
nil if the handle’s cache entry cannot be resolved.

Example

-- Load once at startup; look it up by name later.
local script = Script.Create("enemy", "scripts/enemy.lua")

-- ...elsewhere...
local same = Script.Get("enemy")
if Script.Exists(same) then
    print(same:ID())   -- "enemy"
end

Venus Lua API › Math

Vector2

2-component vector — a native, allocation-free value type.

authored lua/venus_lua/src/lua_math.rs

Vector2

A 2-component float vector. Like Vector3, Vector2 is a native Lua value (stored inline in
the Lua fork’s vector TValue — no garbage collection), so passing and returning them is cheap.

Static math functions are called on the type table (Vector2.Dot(a, b)); the two instance methods
use colon syntax (v:magnitude()).

Types

Vector2 — a native vector value with components x, y (f32). Read them as fields: v.x,
v.y. tostring(v) yields Vec2{x,y}. Vectors participate in the fork’s native vector arithmetic
(a + b, a - b, v * scalar); for element-wise product and quotient use ComponentMul /
ComponentDiv.

Every static function below reads its vector arguments with check_vector, which requires an
actual native Vector2 value (from .new, .zero, arithmetic, or an engine getter) and
raises a Lua type error on a number or a wrong vector variant. Loose numbers are accepted only
by .new. There is no null/stale concept — these are values, not handles.

Construction

Vector2.new( number x, number y ) : Vector2
Constructs a vector. Missing — or non-numeric — arguments default to 0 (no error), so
Vector2.new() is the zero vector and Vector2.new(1) is (1, 0).

Vector2.zero( ) : Vector2
Returns (0, 0).

Functions

Static operations. Each takes native Vector2 values (see Types).

Vector2.Dot( Vector2 a, Vector2 b ) : number
Dot product.

Vector2.Distance( Vector2 a, Vector2 b ) : number
Euclidean distance between two points.

Vector2.Normalize( Vector2 v ) : Vector2
Returns v scaled to unit length.

Vector2.Lerp( Vector2 a, Vector2 b, number t ) : Vector2
Linear interpolation from a to b by t. t is a required number (raises if missing); it is
not clamped to [0, 1].

Vector2.Min( Vector2 a, Vector2 b ) : Vector2
Component-wise minimum.

Vector2.Max( Vector2 a, Vector2 b ) : Vector2
Component-wise maximum.

Vector2.Abs( Vector2 v ) : Vector2
Component-wise absolute value.

Vector2.ComponentMul( Vector2 a, Vector2 b ) : Vector2
Element-wise product (a.x*b.x, a.y*b.y).

Vector2.ComponentDiv( Vector2 a, Vector2 b ) : Vector2
Element-wise quotient.

Vector2 intentionally exposes a smaller surface than Vector3: it has no Cross,
SqrDistance, Angle, SignedAngle, Project, ProjectOnPlane, Reflect, MoveTowards, or
ClampMagnitude.

Methods

v:magnitude( ) : number
The vector’s length.

v:sqrMagnitude( ) : number
The squared length (cheaper than magnitude).

Example

-- Normalize a 2D steering direction and scale it to a fixed speed.
local dir  = Vector2.Normalize(Vector2.new(dx, dy))
local step = dir * speed          -- native scalar multiply
print(step.x, step.y)

Venus Lua API › Math

Vector3

3-component vector — a native, allocation-free value type.

authored lua/venus_lua/src/lua_math.rs

Vector3

A 3-component float vector. Unlike handle types, Vector3 is a native Lua value (stored
inline in the Lua fork’s vector TValue — no garbage collection), so passing and returning them
is cheap. Most engine APIs that take a Vector3 also accept three loose numbers.

Static math functions are called on the type table (Vector3.Dot(a, b)); the two instance
methods use colon syntax (v:magnitude()).

Types

Vector3 — a native vector value with components x, y, z (f32). Read them as fields:
v.x, v.y, v.z. tostring(v) yields Vec3{x,y,z}. Vectors participate in the fork’s native
vector arithmetic (a + b, a - b, v * scalar); for element-wise product and quotient use
ComponentMul / ComponentDiv.

Construction

Vector3.new( number x, number y, number z ) : Vector3
Constructs a vector. Missing arguments default to 0, so Vector3.new() is the zero vector and
Vector3.new(1) is (1, 0, 0).

Vector3.zero( ) : Vector3
Returns (0, 0, 0).

Functions

Static operations. Angles are in radians (the math primitives use radians; the GameObject
Euler setters, which take degrees, are the exception).

Vector3.Dot( Vector3 a, Vector3 b ) : number
Dot product.

Vector3.Cross( Vector3 a, Vector3 b ) : Vector3
Cross product a × b.

Vector3.Distance( Vector3 a, Vector3 b ) : number
Euclidean distance between two points.

Vector3.SqrDistance( Vector3 a, Vector3 b ) : number
Squared distance (cheaper — no square root).

Vector3.Normalize( Vector3 v ) : Vector3
Returns v scaled to unit length.

Vector3.Lerp( Vector3 a, Vector3 b, number t ) : Vector3
Linear interpolation from a to b by t.

Vector3.Min( Vector3 a, Vector3 b ) : Vector3
Component-wise minimum.

Vector3.Max( Vector3 a, Vector3 b ) : Vector3
Component-wise maximum.

Vector3.Abs( Vector3 v ) : Vector3
Component-wise absolute value.

Vector3.ComponentMul( Vector3 a, Vector3 b ) : Vector3
Element-wise product (a.x*b.x, a.y*b.y, a.z*b.z).

Vector3.ComponentDiv( Vector3 a, Vector3 b ) : Vector3
Element-wise quotient.

Vector3.Angle( Vector3 from, Vector3 to ) : number
Unsigned angle (radians) between two directions.

Vector3.SignedAngle( Vector3 from, Vector3 to, Vector3 axis ) : number
Signed angle (radians) about axis.

Vector3.Project( Vector3 v, Vector3 onto ) : Vector3
Projection of v onto onto.

Vector3.ProjectOnPlane( Vector3 v, Vector3 normal ) : Vector3
Component of v in the plane with the given normal.

Vector3.Reflect( Vector3 v, Vector3 normal ) : Vector3
Reflects v across the plane defined by normal.

Vector3.MoveTowards( Vector3 current, Vector3 target, number maxDistance ) : Vector3
Moves current toward target by at most maxDistance.

Vector3.ClampMagnitude( Vector3 v, number maxLength ) : Vector3
Returns v shortened to maxLength if longer.

Methods

v:magnitude( ) : number
The vector’s length. (Also computed inline in the type — call it as a method.)

v:sqrMagnitude( ) : number
The squared length (cheaper than magnitude).

Example

-- Steer toward a target at fixed speed, clamped so we never overshoot.
function Script.Update(self)
    local x, y, z = self.object:WorldPosition()
    local here = Vector3.new(x, y, z)
    local step = Vector3.MoveTowards(here, self.target, self.speed * Time.DeltaTime())
    self.object:SetWorldPosition(step)

    if Vector3.SqrDistance(step, self.target) < 0.01 then
        self.arrived = true
    end
end

Venus Lua API › Math

Vector4

4-component vector — a native, allocation-free value type.

authored lua/venus_lua/src/lua_math.rs

Vector4

A 4-component float vector. Like Vector3, Vector4 is a native Lua value (stored inline in
the Lua fork’s vector TValue — no garbage collection), so passing and returning them is cheap.

Static math functions are called on the type table (Vector4.Dot(a, b)); the two instance methods
use colon syntax (v:magnitude()).

Types

Vector4 — a native vector value with components x, y, z, w (f32). Read them as fields:
v.x, v.y, v.z, v.w. tostring(v) yields Vec4{x,y,z,w}. Vectors participate in the fork’s
native vector arithmetic (a + b, a - b, v * scalar); for element-wise product and quotient
use ComponentMul / ComponentDiv.

Every static function below reads its vector arguments with check_vector, which requires an
actual native Vector4 value (from .new, .zero, arithmetic, or an engine getter) and
raises a Lua type error on a number or a wrong vector variant. Loose numbers are accepted only
by .new. There is no null/stale concept — these are values, not handles.

Construction

Vector4.new( number x, number y, number z, number w ) : Vector4
Constructs a vector. Missing — or non-numeric — arguments default to 0 (no error), so
Vector4.new() is the zero vector and Vector4.new(1) is (1, 0, 0, 0).

Vector4.zero( ) : Vector4
Returns (0, 0, 0, 0).

Functions

Static operations. Each takes native Vector4 values (see Types); all operate over all four
components.

Vector4.Dot( Vector4 a, Vector4 b ) : number
Dot product (4-component).

Vector4.Distance( Vector4 a, Vector4 b ) : number
Euclidean distance between two 4-component points.

Vector4.Normalize( Vector4 v ) : Vector4
Returns v scaled to unit length (4-component magnitude).

Vector4.Lerp( Vector4 a, Vector4 b, number t ) : Vector4
Linear interpolation from a to b by t. t is a required number (raises if missing); it is
not clamped to [0, 1].

Vector4.Min( Vector4 a, Vector4 b ) : Vector4
Component-wise minimum.

Vector4.Max( Vector4 a, Vector4 b ) : Vector4
Component-wise maximum.

Vector4.Abs( Vector4 v ) : Vector4
Component-wise absolute value.

Vector4.ComponentMul( Vector4 a, Vector4 b ) : Vector4
Element-wise product (a.x*b.x, a.y*b.y, a.z*b.z, a.w*b.w).

Vector4.ComponentDiv( Vector4 a, Vector4 b ) : Vector4
Element-wise quotient.

Vector4 exposes only this basic surface — none of Vector3’s geometric helpers
(Cross, Angle, Project, Reflect, MoveTowards, ClampMagnitude, SqrDistance) exist for
it.

Methods

v:magnitude( ) : number
The vector’s length (over all four components).

v:sqrMagnitude( ) : number
The squared length (cheaper than magnitude).

Example

-- Pack an RGBA color as a Vector4 and read a channel back.
local c = Vector4.new(0.2, 0.6, 1.0, 1.0)
local dim = c * 0.5           -- native scalar multiply
print(dim.x, dim.y, dim.z, dim.w)

Venus Lua API › Math

Quaternion

Unit quaternion rotation — a native, allocation-free value type.

authored lua/venus_lua/src/lua_math.rs

Quaternion

A rotation stored as a 4-component quaternion. Like the vector types, Quaternion is a native
Lua value
(stored inline in the Lua fork’s vector TValue — no garbage collection), so passing
and returning them is cheap.

Static constructors and math functions are called on the type table (Quaternion.AngleAxis(...),
Quaternion.Slerp(...)); the * operator composes and applies rotations. There are no colon
instance methods.

Types

Quaternion — a native quaternion value with components x, y, z, w (f32). Read them as
fields: q.x, q.y, q.z, q.w. tostring(q) yields Quat{x,y,z,w}. Every function reads its
arguments with check_vector (or luaL_checknumber), so a wrong type raises a Lua type error;
there is no null/stale concept — these are values, not handles.

Quaternions support the * operator (the __mul metamethod), dispatched on the right-hand
operand:

  • a * b (both Quaternion) — the Hamilton product, composing the two rotations (the
    right-hand operand is applied first: a * b applies b, then a).
  • q * v (where v is a Vector3) — rotates the vector v by q, returning a Vector3.
    v must be a native Vector3.

The quaternion must be the left operand. q * x where x is neither a Quaternion nor a
Vector3 raises a type error, and v * q (vector on the left) does not rotate — it falls to
the native vector arithmetic path, not this metamethod.

Unless noted, functions do not re-normalize their inputs; Slerp normalizes its result to
compensate for SIMD rsqrt error.

Construction

Quaternion.new( number x, number y, number z, number w ) : Quaternion
Constructs from raw components — all four are required (luaL_checknumber; a missing argument
raises). No normalization is applied.

Quaternion.identity( ) : Quaternion
The identity rotation (0, 0, 0, 1).

Quaternion.RadiansAxis( number radians, Vector3 axis ) : Quaternion
Rotation of radians (radians) about axis. axis accepts a Vector3 or three loose
numbers.

Quaternion.AngleAxis( number degrees, Vector3 axis ) : Quaternion
Rotation of degrees (degrees — converted internally) about axis. axis accepts a Vector3
or three loose numbers.

Known issue: AngleAxis / RadiansAxis assume a unit-length axis and do not
normalize it (Quat::from_angle_axis, core/math/src/simd/quat.rs:42). A non-unit axis yields a
non-unit quaternion (a scaled/incorrect rotation) — this diverges from Unity’s AngleAxis, which
normalizes. Normalize the axis yourself, or use Vector3.Normalize first.

Quaternion.Euler( Vector3 degrees ) : Quaternion
Rotation from Euler angles in degrees, applied in YXZ order. Accepts a Vector3 or three
loose numbers. This is the same function as EulerYXZ (an alias), and matches the default
order used by GameObject’s EulerAngles.

Quaternion.EulerXYZ( Vector3 degrees ) : Quaternion
Euler angles in degrees, XYZ order. Accepts a Vector3 or three loose numbers.

Quaternion.EulerXZY( Vector3 degrees ) : Quaternion
Euler angles in degrees, XZY order.

Quaternion.EulerYXZ( Vector3 degrees ) : Quaternion
Euler angles in degrees, YXZ order (identical to Euler).

Quaternion.EulerYZX( Vector3 degrees ) : Quaternion
Euler angles in degrees, YZX order.

Quaternion.EulerZXY( Vector3 degrees ) : Quaternion
Euler angles in degrees, ZXY order.

Quaternion.EulerZYX( Vector3 degrees ) : Quaternion
Euler angles in degrees, ZYX order.

Functions

Static operations. Note the mixed angle units: constructors and Angle use degrees, while
RadiansAxis and Radians use radians.

Quaternion.Normalize( Quaternion q ) : Quaternion
Returns q scaled to unit length (falls back to identity for a zero-length quaternion).

Quaternion.Inverse( Quaternion q ) : Quaternion
Returns the conjugate (negates x, y, z, keeps w).

Known issue: Inverse is the conjugate, which equals the true inverse only for unit
quaternions
(Quat::inverse, core/math/src/simd/quat.rs:300). Because new and AngleAxis
don’t normalize, Inverse (and Delta, which is built on it) is wrong for a non-unit
quaternion. Normalize first if you’re unsure the quaternion is unit-length.

Quaternion.Reverse( Quaternion q ) : Quaternion
Negates all four components — the antipodal representation of the same rotation.

Quaternion.Lerp( Quaternion a, Quaternion b, number t ) : Quaternion
Component-wise linear interpolation from a to b by t. The result is not re-normalized.

Quaternion.Slerp( Quaternion a, Quaternion b, number t ) : Quaternion
Spherical linear interpolation from a to b by t. The result is normalized.

Quaternion.Angle( Quaternion a, Quaternion b ) : number
Angle between the two rotations, in degrees. Computed as 2·acos(|dot|) with the dot clamped
to 1, so it is robust to non-unit inputs.

Quaternion.Radians( Quaternion a, Quaternion b ) : number
Same angle, in radians.

Quaternion.Dot( Quaternion a, Quaternion b ) : number
Four-component dot product.

Quaternion.Delta( Quaternion a, Quaternion b ) : Quaternion
The rotation from a to b, computed as inverse(a) * b. It is a right delta:
a * Delta(a, b) == b.

Known issue: Delta uses the plain inverse(a) * b and does not take the shortest path
(Quat::delta, core/math/src/simd/quat.rs:355). When a and b are more than 180° apart in
their stored representation (negative dot), the result winds the long way around — the
shortest_delta variant that flips the sign is not the one bound to Lua. It also inherits
the unit-only limitation of Inverse above.

Example

-- Build a yaw rotation and spin a direction vector by it.
local yaw = Quaternion.AngleAxis(90, 0, 1, 0)   -- 90° about +Y (degrees; axis already unit)
local fwd = yaw * Vector3.new(0, 0, -1)          -- Quat * Vector3 rotates the vector

-- Compose two rotations, then blend halfway toward identity.
local r    = yaw * Quaternion.EulerXYZ(0, 0, 45)  -- Quat * Quat = Hamilton product
local half = Quaternion.Slerp(Quaternion.identity(), r, 0.5)

Venus Lua API › Rendering

Camera

View component — projection, clear, culling, screen/world transforms.

authored lua/venus_lua/src/lua_camera.rs

Camera

A camera component attached to a GameObject: it defines the projection (FOV, clip planes, aspect),
what it clears, which layers it draws, its render order and viewport rect, and the point/ray
transforms between world, viewport, and screen space.

Static calls use the type table (Camera.Add(go)); instance methods use colon syntax on a handle
(camera:SetVerticalFOV(...)).

Camera.Add always creates a perspective camera (Camera::default), and there is no Lua
binding to switch a camera to orthographic — so in practice every Lua-facing camera is perspective.
The FOV / aspect accessors below note their orthographic behavior for completeness only.

Types

Camera — an opaque handle (userdata) to a camera component. A handle may be null or stale
(its component destroyed); it is safe to keep and pass around.

  • Queries (getters and the projection transforms) on a null/stale handle log a warning
    (NotFound) and return nil — they never raise.
  • Setters and actions on a null/stale handle silently no-op — no warning, unlike the
    getters. (This asymmetry differs from GameObject, whose setters warn.)
  • Argument type checks still raise. The receiver must be a Camera userdata, and every vector
    argument must be a native Vector3/Vector4 (built with Vector3.new(...)): passing loose
    numbers raises a Lua type error. Unlike the GameObject transform setters, these calls do not
    accept three numbers in place of a vector.

tostring(camera) always yields the literal Camera — it does not include the object name or a
NULL marker (unlike GameObject/Shader).

Creation & Destruction

Camera.Add( GameObject gameObject ) : Camera
Adds a default (perspective, 60° FOV) Camera component to gameObject and returns its handle.
Returns nil and logs InvalidArgument if the component cannot be added.

Camera.Get( GameObject gameObject ) : Camera
Returns the Camera component on gameObject, or nil (logs NotFound) if it has none.

Camera.Exists( Camera camera ) : boolean
Returns true if the handle refers to a live Camera component. Safe on null/stale/foreign handles
(returns false) — the one call here that never warns or raises.

There is no Lua Destroy for a Camera; remove it by destroying its GameObject.

Functions

Read-only queries and projection transforms. Vector arguments must be native Vector3/Vector4
values; each call logs NotFound and returns nil on a null/stale handle.

camera:WorldToViewportPoint( Vector3 worldPoint ) : Vector3
Projects a world-space point into viewport space (x, y normalized in [0,1]; z carries view-space
depth).

camera:ViewportToWorldPoint( Vector3 viewportPoint ) : Vector3
Unprojects a viewport-space point (x, y in [0,1], z = distance from the camera) back to world space.

camera:WorldToScreenPoint( Vector3 worldPoint ) : Vector3
Projects a world-space point to pixel screen coordinates, using the current screen size (render
pixel dimensions). z carries depth.

camera:ScreenToWorldPoint( Vector3 screenPoint ) : Vector3
Unprojects a pixel screen point (z = distance from the camera) back to world space, using the
current screen size.

camera:ScreenPointToRay( Vector3 screenPoint ) : table { origin: Vector3, direction: Vector3 }
Builds a world-space ray through the given pixel screen point, using the current screen size. The
result is a table with origin and direction fields (both Vector3).

camera:ViewportPointToRay( Vector3 viewportPoint ) : table { origin: Vector3, direction: Vector3 }
Builds a world-space ray through the given viewport point (x, y in [0,1]). Unlike
ScreenPointToRay, this needs no screen size — viewport coordinates are already normalized.

camera:Active( ) : boolean or nil
The component’s own enabled flag — what Activate()/Deactivate() write. This is intent,
not effect: a camera whose own flag is enabled but whose GameObject is deactivated still reports
true. The nil on a stale handle keeps “destroyed” distinguishable from “disabled”.

camera:ActiveInHierarchy( ) : boolean or nil
true only when the own flag is enabled and the owning GameObject is active in hierarchy —
i.e. the component is actually running. This is the predicate the engine uses to gate update
dispatch.

Properties (get / set)

Getters warn (NotFound) and return nil on a null/stale handle; setters silently no-op on a
null/stale handle. Clip-plane setters (SetNearClip/SetFarClip) rebuild the projection matrix
immediately; SetVerticalFOV/SetAspect defer the rebuild to the next cull pass (see notes).

Aspect

camera:Aspect( ) : number — viewport aspect ratio (width / height). Returns 0.0 for an
orthographic camera.
camera:SetAspect( number aspect ) : void — no-op on an orthographic camera. See the known
issue below.

Known issue: on a perspective camera SetAspect does not stick. Every frame on_pre_cull
recomputes aspect = (viewportRect.width · screenWidth) / (viewportRect.height · screenHeight)
and rebuilds the projection (engine/scenegraph/src/camera.rs:507-518), overwriting whatever you
set. Aspect() therefore reports the auto-derived ratio, not your argument. To change the framed
aspect, change the window size or SetViewportRect, not SetAspect.

VerticalFOV

camera:VerticalFOV( ) : number — vertical field of view in degrees (the stored value; the
projection converts it to radians internally via (fov/2).to_radians()). Returns 0.0 for an
orthographic camera.
camera:SetVerticalFOV( number degrees ) : void — sets the vertical FOV, in degrees. No-op
on an orthographic camera. The setter mutates the stored FOV but does not rebuild the projection on
the spot (unlike SetNearClip/SetFarClip); the change is picked up by the per-frame rebuild in
on_pre_cull, so it takes visible effect on the next frame.

NearClip

camera:NearClip( ) : number — near clip-plane distance.
camera:SetNearClip( number near ) : void — rebuilds the projection immediately.

FarClip

camera:FarClip( ) : number — far clip-plane distance.
camera:SetFarClip( number far ) : void — rebuilds the projection immediately.

LayerMask

camera:LayerMask( ) : integer — bitmask of render layers this camera draws.
camera:SetLayerMask( integer mask ) : void

RenderOrder

camera:RenderOrder( ) : integer — draw order among cameras (lower renders first).
camera:SetRenderOrder( integer order ) : void

ClearColor

camera:ClearColor( ) : Vector4 — clear color as sRGB RGBA (converted back from the linear
value stored internally).
camera:SetClearColor( Vector4 srgba ) : void — accepts an sRGB RGBA Vector4 (stored as
linear). Requires a native Vector4.

ClearDepth

camera:ClearDepth( ) : number — depth clear value.
camera:SetClearDepth( number depth ) : void

ClearMode

camera:ClearMode( ) : string — one of "DontClear", "ClearColorOnly", "ClearDepthOnly",
"ClearColorAndDepth".
camera:SetClearMode( string mode ) : void — takes the same four names the getter returns, so
a:SetClearMode(b:ClearMode()) round-trips. Matched exactly — no case folding, and numbers are
rejected. An unrecognized value raises a Lua error listing the four spellings; it no longer falls
back to color + depth, which was both the most expensive mode and the one that hid the mistake
behind plausible-looking output. There is no constants table for these — pass the string literally.

CullTolerance

camera:CullTolerance( ) : number — frustum-cull tolerance expansion.
camera:SetCullTolerance( number tolerance ) : void

SkipCulling

camera:SkipCulling( ) : boolean — whether frustum culling is bypassed for this camera.
camera:SetSkipCulling( boolean skip ) : void

ViewportRect

camera:ViewportRect( ) : Vector4 — viewport rect (x, y, width, height) in [0,1] normalized
coords.
camera:SetViewportRect( Vector4 rect ) : void(x, y, width, height) in [0,1]. Values are
clamped to a valid range (x,y ≥ 0; width/height in [0.001, 1 − offset]). Requires a native
Vector4.

Methods

Actions that mutate the camera. All silently no-op on a null/stale handle.

camera:SetRenderTarget( integer gpuHandle ) : void
Directs this camera’s color output to an off-screen target. gpuHandle is a packed 4-byte
GpuHandle<GpuRenderTarget> passed as an integer — obtain it from a
RenderTexture via rt:GpuColorHandle(). The integer is reinterpreted directly
(transmute) with no validation, so an integer from any other source yields an undefined target
rather than an error.

camera:SetDepthRenderTarget( integer gpuHandle ) : void
As above for the depth target; obtain the integer from rt:GpuDepthHandle().

camera:SetReplacementShader( Shader shader ) : void
Overrides every draw for this camera with shader (e.g. a depth-only or debug pass). Pass nil to
clear the override. If a live Shader handle is given, its pipeline is resolved and
installed; if the handle cannot be resolved, logs InvalidArgument and clears the override. A
non-nil argument that is not a Shader userdata raises a type error.

camera:Activate( ) : void
Sets the component’s own enabled flag. Its enable callback runs only if the owning GameObject is
also active.

camera:Deactivate( ) : void
Clears the own enabled flag; the disable callback runs only if the component was actually active.

Example

-- Attach a perspective camera, frame the scene, and pick under the mouse.
-- (Adapted from data/scripts/session_setup.lua + editor_pick.lua.)
function Script.OnEnable(self)
    local cam = Camera.Add(self.object)
    cam:SetVerticalFOV(60)          -- degrees
    cam:SetNearClip(0.1)
    cam:SetFarClip(1000)
    cam:SetClearColor(Vector4.new(0.02, 0.02, 0.04, 1))  -- sRGB RGBA
    self.cam = cam
end

function Script.Update(self)
    if not Input.LeftMouseDown() then return end
    local mx, my = Input.MousePosition()
    -- There is no Camera:Pick — build a ray and raycast the physics world.
    local ray = self.cam:ScreenPointToRay(Vector3.new(mx, my, 0))
    local hit = Physics.Raycast(ray.origin, ray.direction, self.pickMask)
    if hit then self.hovered = hit.gameObject end
end

Venus Lua API › Rendering

Light

Light component — directional / point / spot, color, shadows, cookies.

authored lua/venus_lua/src/lua_light.rs

Light

A light component attached to a GameObject: type (directional / point / spot), color and
intensity, spot cone shape, shadow parameters, an optional projected cookie, and a layer mask.
Range is not settable — it is auto-computed from intensity and the attenuation cutoff (the TR7
lighting model, sqrt(intensity / cutoff − 0.1)).

Static calls use the type table (Light.Point(go, ...)); instance methods use colon syntax on a
handle (light:SetIntensity(...)).

Color spaces are not uniform across this API — read carefully. Color / SetColor and the
r, g, b arguments to the constructors take sRGB (converted to linear on the way in). ColorLinear
/ SetColorLinear take linear RGB with no conversion. (Scene-wide ambient on Lighting
is linear too.)

Types

Light — an opaque handle (userdata) to a light component. A handle may be null or stale
(its component destroyed); it is safe to keep and pass around. Calling a getter on a null/stale
handle logs a warning and returns nil; calling a setter silently no-ops (see the note under
Properties). tostring(light) always yields the literal Light (it does not encode the handle’s
name or liveness).

Creation & Destruction

There is no light:Destroy() — a light is removed by destroying (or removing the component from)
its owning GameObject. Every constructor adds a component to gameObject and returns the handle,
or returns nil and logs an error if the component cannot be added (a null/stale gameObject, or
the light component pool — default capacity 256 — is exhausted).

Light.Add( GameObject gameObject ) : Light
Adds a default light to gameObject. The default is a point light: white, intensity 1.0, spot
angle 45° / falloff 0.8 (unused), cutoff 0 (→ global default), no shadows, cookie −1 (none), layer
mask 0xFFFFFFFF.

Light.Point( GameObject gameObject, number r, number g, number b, number intensity ) : Light
Adds a point light with the given sRGB color and intensity.

Light.Spot( GameObject gameObject, number r, number g, number b, number intensity, number spotAngle, number spotFalloff ) : Light
Adds a spot light. spotAngle is the full cone angle in degrees; spotFalloff is the
umbra→penumbra ratio in [0,1] (0 = hard edge, 1 = the whole cone is penumbra).

Light.Directional( GameObject gameObject, number r, number g, number b, number intensity ) : Light
Adds a directional light with the given sRGB color and intensity. The direction is the owning
GameObject’s forward axis (orient it with gameObject:LookAt(...)).

Light.Get( GameObject gameObject ) : Light
Returns the Light component on gameObject, or nil (with a warning) if it has none.

Light.Exists( Light light ) : boolean
Returns true if the handle refers to a live Light component. Safe on null/stale handles (returns
false) and on non-Light values — this call never warns or raises.

Functions

Class-level attenuation default

Static calls on the Light table (a process-global, not per-light). A per-light cutoff of 0
resolves to this value.

Light.DefaultAttenuationCutoff( ) : number
The global default attenuation cutoff (initial value 0.01).

Light.SetDefaultAttenuationCutoff( number cutoff ) : void
Sets the global default attenuation cutoff.

Known issue: changing the global default after lights are already live does not re-sync
their spatial-octree extents or light-grid coverage — only their per-light SSBO range is refreshed
through the normal dirty path. Set it once at startup, before enabling lights; otherwise toggle a
light’s intensity or per-light cutoff to force a full re-sync. (engine/scenegraph/src/light.rs:380-386,
the set_attenuation_cutoff doc comment.)

Properties (get / set)

Getters return nil and log a warning on a null/stale handle. Setters no-op silently on a
null/stale handle — no warning is logged
(unlike the GameObject setters, which warn). Guard with
Light.Exists(light) if a silent no-op would hide a bug.

Type

light:Type( ) : string"Directional", "Point", or "Spot" (see
Enum names).
light:SetType( string type ) : void — takes the same three names the getter returns, so
a:SetType(b:Type()) round-trips. An unrecognized name raises. See the known issue below about
switching type on a live light.

Known issue: SetType on an already-enabled light does not re-bind its GPU slot (point/spot
use an SSBO slot, directional uses a UBO slot), so switching to or from directional at runtime
leaves the light on the wrong GPU path. Disable and re-enable the owning GameObject to switch
types reliably. (engine/scenegraph/src/light.rs:336-345, the set_type doc comment.)

Color

light:Color( ) : Vector3 — light color as sRGB RGB (converted back from the linear store).
light:SetColor( Vector3 srgb ) : void — accepts sRGB RGB (converted to linear internally).

ColorLinear

light:ColorLinear( ) : Vector3 — light color as linear RGB (the raw stored value, no conversion).
light:SetColorLinear( Vector3 linear ) : void — sets linear RGB directly (no conversion).

Intensity

light:Intensity( ) : number — HDR intensity multiplier on the color.
light:SetIntensity( number intensity ) : void — also drives Range (re-syncs grid/octree coverage).

Range

light:Range( ) : number — effective attenuation range in world units. Read-only (no
setter): auto-computed as sqrt(intensity / effective_cutoff − 0.1), clamped to 0 for
non-positive/non-finite inputs.

SpotAngle

light:SpotAngle( ) : number — full spot cone angle in degrees (spot only; default 45).
light:SetSpotAngle( number degrees ) : void — no radians conversion; the value is stored and
returned as-is.

SpotFalloff

light:SpotFalloff( ) : number — umbra→penumbra falloff ratio in [0,1] (default 0.8).
light:SetSpotFalloff( number ratio ) : void

AttenuationCutoff

light:AttenuationCutoff( ) : number — per-light attenuation cutoff (default 0 = use the global default).
light:SetAttenuationCutoff( number cutoff ) : void0 = use the global default; also drives Range.

Shadows

light:Shadows( ) : boolean — whether the light casts shadows (default false).
light:SetShadows( boolean cast ) : void

ShadowBias

light:ShadowBias( ) : number — depth bias for this light’s shadow map (default 0.005).
light:SetShadowBias( number bias ) : void

ShadowResolution

light:ShadowResolution( ) : integer — shadow-map resolution (default 0 = use engine default 2048).
light:SetShadowResolution( integer resolution ) : void0 = use default.

ShadowExtent

light:ShadowExtent( ) : number — orthographic shadow half-extent in world units, for
directional shadows (default 0 = use engine default 50).
light:SetShadowExtent( number extent ) : void0 = use default.

ShadowNearClip

light:ShadowNearClip( ) : number — near clip for the shadow projection (default 0.1).
light:SetShadowNearClip( number near ) : void

ShadowFarClip

light:ShadowFarClip( ) : number — far clip for the shadow projection (default 0 = use the
auto-computed Range).
light:SetShadowFarClip( number far ) : void0 = use the computed range.

CookieIndex

light:CookieIndex( ) : number — projected-cookie texture index (default −1). Stored as a
float, not an integer.
light:SetCookieIndex( number index ) : void — any negative value disables the cookie.

LayerMask

light:LayerMask( ) : integer — bitmask of layers this light affects (default 0xFFFFFFFF).
light:SetLayerMask( integer mask ) : void

Lifecycle

light:Active( ) : boolean or nil — the component’s own enabled flag, what Activate()/
Deactivate() write. nil plus a logged NotFound warning on a stale handle. This is intent, not
effect: a light whose own flag is enabled but whose GameObject is deactivated still reports true
here, and does not contribute to the scene.

light:ActiveInHierarchy( ) : boolean or nil — true only when the own flag is enabled and
the owning GameObject is active in hierarchy, i.e. the light is actually live. This is the predicate
the engine uses to gate update dispatch. nil on a stale handle.

light:Activate( ) : void — sets the own enabled flag. The enable callback runs only if the
owning GameObject is also active. Silently no-ops on a stale handle.

light: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.

Enum names

The light type is a string, matched exactly — one spelling each, no case folding
("point" is rejected; only "Point" is accepted). These are both the values Type() returns and
the values SetType() accepts (verified against LightType, light_types.rs:63-75):

"Directional"
"Point"
"Spot"

The Light.DIRECTIONAL / .POINT / .SPOT integer constants were deleted (2026-07), along
with the integer SetType path. They are absent from the Light table now, so Light.POINT
evaluates to nil and passing it raises. They were not reinstated as string constants: a
mistyped table field (Light.SPOTT) is nil and indistinguishable from an omitted argument,
while a present-but-wrong string can always be rejected by name.

Example

-- Warm directional "sun" plus colored point lights.
-- (Adapted from data/scripts/main_scene_setup.lua.)
function Script.OnEnable(self)
    -- Directional light: aim it by orienting its GameObject's forward axis.
    local sun_go = GameObject.Create()
    sun_go:SetID("Sun")
    sun_go:LookAt(Vector3.new(1, -2, -1))
    local sun = Light.Directional(sun_go, 1.0, 0.95, 0.85, 1.5)  -- sRGB color
    if sun then
        sun:SetShadows(true)
        sun:SetShadowExtent(60)  -- widen the orthographic shadow volume
    end

    -- A colored point light. Range is auto-computed from intensity + cutoff.
    local lamp = GameObject.Create()
    lamp:SetWorldPosition(4, 2, 0)
    local pt = Light.Point(lamp, 1.0, 0.3, 0.1, 8.0)
    pt:SetAttenuationCutoff(0)   -- 0 = use the global default (0.01)
end

Venus Lua API › Rendering

Lighting

Scene-wide lighting settings — ambient, fog, shadow params.

authored lua/venus_lua/src/lua_lighting.rs

Lighting

A module singleton (global functions only, no handles) for scene-wide lighting settings on the
engine’s LightManager: hemisphere ambient, fog, and global shadow parameters. These are not
per-light properties — those live on the Light component.

All functions act on the app’s global LightManager (always present when the engine is running);
there is no per-call null handling. Getters return the current stored value; setters return
void.

Ambient colors are linear RGB (passed through verbatim, no sRGB conversion) — unlike the
Light component’s Color/SetColor, which are sRGB.

Functions

Ambient

Lighting.AmbientSky( ) : Vector3
Sky (upper-hemisphere) ambient color, linear RGB.

Lighting.AmbientGround( ) : Vector3
Ground (lower-hemisphere) ambient color, linear RGB.

Lighting.AmbientIntensity( ) : number
Ambient intensity multiplier (default 1.0).

Lighting.SetAmbient( Vector3 skyColor, Vector3 groundColor, number intensity ) : void
Sets sky/ground ambient colors (linear RGB) and the intensity multiplier in one call. Colors are
Vector3; there is no separate per-channel or per-field setter.

Fog

Fog mode is stored internally as a float, but is only ever meaningful at the four values below;
FogMode() returns it as an integer and the FOG_* constants are integers.

Lighting.FogDensity( ) : number
Fog density (used by the exponential modes).

Lighting.FogStart( ) : number
Linear-fog start distance.

Lighting.FogEnd( ) : number
Linear-fog end distance.

Lighting.FogMode( ) : integer
Active fog mode: 0 = none, 1 = linear, 2 = exp, 3 = exp2 (see Constants).

Lighting.SetFog( number density, number start, number end, number mode ) : void
Sets all four fog fields directly. mode is a number — pass a Lighting.FOG_* constant; a
fractional value is stored verbatim and read back truncated by FogMode().

Lighting.SetFogLinear( number start, number end ) : void
Convenience for linear fog: sets start/end and mode = 1, and forces density = 0.

Lighting.SetFogExp( number density ) : void
Convenience for exponential fog: sets density and mode = 2, and forces start = end = 0.

Lighting.SetFogExp2( number density ) : void
Convenience for exp² fog: sets density and mode = 3, and forces start = end = 0.

Lighting.DisableFog( ) : void
Resets fog to defaults — mode = 0 (none) and density = start = end = 0.

Shadow parameters

Global shadow parameters shared by every shadow-casting light (per-light bias/resolution live on
the Light component).

Lighting.ShadowBias( ) : number
Global shadow depth bias (default 0.005).

Lighting.ShadowNormalBias( ) : number
Global shadow normal bias (default 0.02).

Lighting.ShadowSoftness( ) : number
Global shadow softness / PCF filter radius (default 1.0).

Lighting.ShadowStrength( ) : number
Global shadow strength (opacity of shadows), nominally [0,1] (default 1.0). Not clamped by the
binding.

Lighting.SetShadowParams( number bias, number normalBias, number softness, number strength ) : void
Sets all four global shadow parameters in one call. There is no individual setter for any of them.

Lighting.ShadowType( ) : string
Active shadow-map filtering technique, by name (see Constants).

Lighting.SetShadowType( string type ) : void
Sets the shadow-map filtering technique by name. Takes the same spellings the getter returns, so
Lighting.SetShadowType(Lighting.ShadowType()) round-trips. An unrecognized name raises a Lua error
listing the four valid spellings — it no longer falls back to Normal, which mattered because each
technique also selects a different shadow render-target format.

Constants

Fog modes (plain integers on the Lighting table) — these stay integers: FogParams.mode is an
f32 shipped straight to the GPU, not a Rust enum, so there is nothing to resolve by name:

Lighting.FOG_NONE = 0
Lighting.FOG_LINEAR = 1
Lighting.FOG_EXP = 2
Lighting.FOG_EXP2 = 3

Shadow techniques (from the ShadowType enum, light_types.rs:18-36) are names, not constants
— strings matched exactly, one spelling each, with no case folding ("vsm" is rejected; only
"VSM" is accepted). Each selects a different render-target format / shader set:

"Normal" — standard depth comparison (D32).
"VSM" — variance shadow maps (RG32F).
"ESM" — exponential shadow maps (R32F).
"CSM" — convolution/Fourier shadow maps (RGBA16F ×2).

The Lighting.SHADOW_NORMAL / _VSM / _ESM / _CSM integer constants were deleted
(2026-07)
along with the integer SetShadowType path. They are absent from the Lighting
table now, so Lighting.SHADOW_VSM evaluates to nil and passing it raises. FOG_* above is
unaffected — it was never an enum crossing.

Example

-- Warm dusk ambient with linear distance fog and soft variance shadows.
function Script.OnEnable(self)
    -- Ambient colors are LINEAR RGB (no sRGB conversion).
    Lighting.SetAmbient(
        Vector3.new(0.30, 0.34, 0.45),  -- sky
        Vector3.new(0.12, 0.10, 0.08),  -- ground
        1.0)                            -- intensity
    Lighting.SetFogLinear(40, 220)      -- mode = linear, density forced to 0
    Lighting.SetShadowType("VSM")
    Lighting.SetShadowParams(0.005, 0.02, 1.5, 0.9)  -- bias, normalBias, softness, strength
end

Venus Lua API › Rendering

Material

Shader + textures + constants — colors, UV transforms, render queue.

authored lua/venus_lua/src/lua_material.rs

Material

A material resource: a shader plus its bound textures, up to 16 color/vector slots, per-slot UV
scale/offset, and a render queue. Materials are named manager resources (not scene components) —
created by name, looked up by name, cloned, and destroyed. Colors are stored linear internally;
the Color/MainColor accessors convert to and from sRGB at the boundary, while
SetColorLinear bypasses conversion.

Static calls use the type table (Material.Create("name")); instance methods use colon syntax on a
handle (material:SetShader(...)).

Types

Material — an opaque handle (userdata) to a material resource. A handle may be null or
stale (its resource destroyed); it is safe to keep and pass around, and a method never raises on
one. Null/stale behavior is not uniform: ID() logs a warning and returns nil, but the color,
UV, and render-queue getters return a silent default (see the known issue below), and every setter
silently no-ops. Passing a non-Material value where a method expects self raises a Lua type
error. tostring(material) yields Material{name}, or Material{NULL} if the handle is dead.

Known issue — dead-handle reads fail silently. Only ID() reports a null/stale handle (logs
NotFound, returns nil). The other getters return a manager default with no warning:
MainColor()/Color() return transparent black (0,0,0,0), UVScaleOffset() returns
(1,1,0,0), and RenderQueue() returns 65535 (RENDER_QUEUE_USE_DEFAULT). Out-of-range color
indices and UV slots behave the same way, and every setter no-ops without warning
(material_manager.rs:375, :392, :518, :547). Use Material.Exists(mat) to test liveness —
do not infer it from a getter’s result.

Creation & Destruction

Material.Create( string name ) : Material
Creates a new named material resource. Returns nil and logs InvalidArgument on failure — a
duplicate name (AlreadyExists) or pool exhaustion (OutOfMemory) both surface as the same log.

Material.Get( string name ) : Material
Looks up an existing material by name. Returns nil and logs NotFound if none matches.

Material.Exists( Material material ) : boolean
Returns true if the handle refers to a live material. Safe on null, stale, or foreign (non-material
userdata / nil) arguments — returns false without raising or warning. This is the reliable
liveness check.

Material.Clone( Material material ) : Material
Duplicates material into a new anonymous resource (not registered by name; gets its own GPU
slot) and returns it. Returns nil and logs NotFound if the source is null/stale. Note this is a
static call — Material.Clone(mat), not mat:Clone().

material:Destroy( ) : boolean
Marks the material for deferred destruction. Returns true if the handle was valid and got marked,
false if it was already null/invalid. (Unlike most instance methods, this one returns a value.)

Functions

Read-only queries. See the known issue above — only ID() reports a dead handle; the rest return
silent defaults.

material:ID( ) : string
The material’s unique name. Returns nil and logs NotFound on a null/stale handle, or on an
anonymous clone (which has no registered name).

Properties (get / set)

Setters return void and silently no-op on a null/stale handle or out-of-range index (no
warning). Color indices and UV/texture slots are 0-based: color indices 0..15
(MAX_MATERIAL_COLORS = 16), UV slots 0..7 (MAX_MATERIAL_TEXTURES = 8).

RenderQueue

material:RenderQueue( ) : integer — the material’s render-queue override, or 65535
(use-shader-default) if unset or the handle is dead.
material:SetRenderQueue( integer queue ) : void — stores the raw 16-bit override (recomputes
the effective 8-bit sort key internally).

MainColor

material:MainColor( ) : Vector4 — color slot 0 as sRGB RGBA.
material:SetMainColor( Vector4 srgba ) : void — accepts sRGB RGBA (stored linear).
Alias for Color(0) / SetColor(0, …) — the same slot-0 color (material_manager.rs:365, :370).

Color

material:Color( integer index ) : Vector4 — the color at 0-based index (0..15) as sRGB
RGBA. Out-of-range or dead handle → (0,0,0,0).
material:SetColor( integer index, Vector4 srgba ) : void — sets 0-based index from sRGB
RGBA (stored linear).

UVScaleOffset

material:UVScaleOffset( integer slot ) : Vector4(scaleU, scaleV, offsetU, offsetV) for
0-based texture slot (0..7). Out-of-range or dead handle → (1,1,0,0).
material:SetUVScale( integer slot, number scaleU, number scaleV ) : void — sets scale only,
preserving the existing offset.
material:SetUVOffset( integer slot, number offsetU, number offsetV ) : void — sets offset
only, preserving the existing scale.

Methods

Actions that mutate the material. All silently no-op on a null/stale handle (no warning).

material:SetShader( Shader shader ) : void
Sets the material’s shader, pulling the pipeline handle, texture count, and default render queue from
the ShaderManager. No-ops and logs NotFound if the shader handle does not resolve.

material:SetTexture( integer slot, Texture texture ) : void
Binds texture at the given 0-based texture slot (0..7), copying its GPU image/sampler and
dimensions. No-ops and logs NotFound if the texture handle does not resolve.

material:SetColorLinear( integer index, Vector4 linearRgba ) : void
Sets the color at 0-based index to a linear-space value, verbatim (no sRGB conversion). The
counterpart write path to SetColor; there is no linear getter exposed to Lua (read back through
Color, which re-applies sRGB).

Example

-- Clone a base material and tint it, then swap its albedo texture and tile the UVs.
function Script.OnEnable(self)
    local mat = Material.Clone(Material.Get("crate"))
    mat:SetMainColor(Vector4.new(0.9, 0.2, 0.2, 1.0))   -- sRGB red tint (== SetColor(0, ...))
    mat:SetTexture(0, Texture.Load("crate_red", "textures/crate_red.dds"))
    mat:SetUVScale(0, 2.0, 2.0)                          -- slot 0, 2x2 tiling
    self.material = mat
end

function Script.OnDisable(self)
    if Material.Exists(self.material) then
        self.material:Destroy()
    end
end

Venus Lua API › Rendering

Mesh

Geometry resource — vertices, indices, bones, bounds.

authored lua/venus_lua/src/lua_mesh.rs

Mesh

A mesh resource: vertex/index/bone data and its spherical bounds. Meshes are named manager resources
(not scene components) — built from Lua vertex data, loaded from a binary .mesh file, or looked up
by name — and are bound to a MeshRenderer for drawing.

Static calls use the type table (Mesh.Load("name", "path")); instance methods use colon syntax on
a handle (mesh:VertexCount()).

Types

Mesh — an opaque handle (userdata) to a mesh resource. A handle may be null or stale (its
resource destroyed); it is safe to keep and pass around, and a method never raises on one. Every
getter returns nil and logs NotFound on a null/stale handle; the one setter (SetFileScale)
silently no-ops. Passing a non-Mesh value where a method expects self raises a Lua type error.
tostring(mesh) yields Mesh{name}, or Mesh{NULL} if the handle is dead. There is no instance
Destroy — meshes are released by the manager, not from Lua.

Creation & Destruction

Mesh.Create( string name, table vertices, table indices ) : Mesh
Creates and GPU-uploads a named mesh. vertices is a 1-based array of { posVec3, normalVec3, uvVec2 } sub-tables (position at sub-index 1, normal at 2, uv at 3; uv optional, defaults to
(0,0)). indices is a flat 1-based array of 0-based vertex indices. Returns nil and logs
InvalidArgument on failure. Lua-built vertices carry only position/normal/uv — no vertex color and
no skin weights — so Mesh.Create always produces a static (un-skinned) mesh.

Known issue — mesh indices truncate to 16-bit. Mesh.Create reads each index as
lua_tointeger(...) as u16 (lua_mesh.rs:101), so any index ≥ 65536 silently wraps with no error
or clamp. A Lua-built mesh therefore cannot reference more than 65536 vertices.

Mesh.Load( string name, string path ) : Mesh
Creates a named mesh and kicks off an asynchronous load from a binary .mesh file; the GPU
upload completes later in the frame loop (via the engine’s load poll). path is resolved relative
to the content directory. Returns nil and logs InvalidArgument if the name allocation or the
async-load kickoff fails (the coarse status does not distinguish a missing file from a name clash).

Mesh.Get( string name ) : Mesh
Looks up an existing mesh by name. Returns nil and logs NotFound if none matches.

Mesh.Exists( Mesh mesh ) : boolean
Returns true if the handle refers to a live mesh. Safe on null, stale, or foreign (non-mesh
userdata / nil) arguments — returns false without raising or warning.

Functions

Read-only queries. Each returns nil and logs NotFound on a null/stale handle.

mesh:ID( ) : string
The mesh’s unique name.

mesh:VertexCount( ) : integer
Number of vertices.

mesh:IndexCount( ) : integer
Number of indices.

mesh:BoneCount( ) : integer
Number of skeleton bones (0 for static meshes, including everything built by Mesh.Create).

mesh:Bounds( ) : Vector4
Bounding sphere packed as (centerX, centerY, centerZ, radius) — center in x, y, z, radius in
w.

Properties (get / set)

The setter returns void and silently no-ops on a null/stale handle (no warning).

FileScale

mesh:FileScale( ) : number — the import/file scale factor applied to the mesh. Returns nil
and logs NotFound on a dead handle.
mesh:SetFileScale( number scale ) : void — updates the scale; silently no-ops on a dead
handle.

Example

-- Load a mesh, adjust its import scale, and query its bounds.
function Script.OnEnable(self)
    local mesh = Mesh.Load("barrel", "meshes/barrel.mesh")
    mesh:SetFileScale(0.01)  -- centimeters -> meters
    self.mesh = mesh
end

function Script.Update(self)
    -- Load is async: guard on Exists + a populated vertex count before reading bounds.
    if Mesh.Exists(self.mesh) and self.mesh:VertexCount() > 0 then
        local b = self.mesh:Bounds()   -- (cx, cy, cz, radius)
        self.radius = b.w
    end
end

Venus Lua API › Rendering

MeshRenderer

Draws a static mesh with a material on a GameObject.

authored lua/venus_lua/src/lua_mesh_renderer.rs

MeshRenderer

A component that draws a static (non-skinned) mesh with a material. Attach one to a
GameObject; it reads the object’s transform each frame, submits the mesh to the render queue, and
carries per-instance state (tint color, render scale, cull bounds). For animated/skinned meshes
use SkinnedMeshRenderer.

Static calls use the type table (MeshRenderer.Add(...)); instance methods use colon syntax on a
component handle (mr:SetMaterial(...)).

Types

MeshRenderer — an opaque handle (userdata) to a MeshRenderer component. A handle may be null
or stale (its component/GameObject destroyed); it is safe to keep and pass around, and a method
never raises on one. On an unresolvable handle, getters log NotFound and return nil (except
HasMaterialInstance, which returns false, and GetMaterialInstance, which returns nil
silently); setters silently no-op (no warning). Passing a non-MeshRenderer value where a
method expects self raises a Lua type error. tostring(mr) yields the constant string
MeshRenderer — it does not reflect a dead handle, so never use it to test liveness (use
MeshRenderer.Exists).

Creation & Destruction

MeshRenderer.Get( GameObject object ) : MeshRenderer
Returns the MeshRenderer component on object, or nil (logs NotFound) if the object has none.

MeshRenderer.Exists( MeshRenderer mr ) : boolean
Returns true if the handle refers to a live component. Safe on null, stale, or foreign
(non-MeshRenderer userdata / nil) arguments — returns false without raising or warning.

MeshRenderer.Add( GameObject object, Mesh mesh, Material material ) : MeshRenderer
Adds a new MeshRenderer to object bound to mesh and material (both resolve lazily at first
cull). Returns the new component handle, or nil if the component could not be added. Unlike
Get, the failure path is silent — it returns nil with no log (lua_mesh_renderer.rs:139).

MeshRenderer has no instance Destroy — its lifetime follows its GameObject (destroy the object,
or use Deactivate() to stop it drawing).

Functions

Read-only queries. Each logs NotFound and returns nil on a null/stale handle unless noted.

mr:EffectiveMaterial( ) : Material
The material actually used for drawing — the per-instance override if one exists, else the shared
material. Returns nil (no log) if the renderer resolves but has no material set.

mr:HasMaterialInstance( ) : boolean
true if a per-instance material clone has been created (via GetMaterialInstance). Returns
falsenot nil — on a dead handle (though it still logs NotFound).

mr:Active( ) : boolean
The component’s own enabled flag — what Activate()/Deactivate() write. This is intent, not
effect: a renderer whose own flag is enabled but whose GameObject is deactivated still reports
true here — and is not drawing. Use ActiveInHierarchy() for the effective answer.

mr:ActiveInHierarchy( ) : boolean
true only when the own flag is enabled and the owning GameObject is active in hierarchy — i.e.
the component is actually running. This is the predicate the engine uses to gate update dispatch.

Removed 2026-07. mr:Enabled() / mr:SetEnabled(bool) are gone — they were exact duplicates
of this lifecycle pair, routing to the same activate/deactivate calls. SetEnabled(true) /
SetEnabled(false) become Activate() / Deactivate(). The honest replacement for the
Enabled() getter is ActiveInHierarchy(), not Active(): the old getter read a field
maintained by the enable/disable callbacks, which fire on effective-activity transitions.

Properties (get / set)

Setters return void and silently no-op on a null/stale handle (no warning). Getters follow the
NotFound-log-and-nil rule above.

Mesh

mr:Mesh( ) : Mesh — the bound mesh, or nil (no log) if the renderer has no mesh.
mr:SetMesh( Mesh mesh ) : void — binds a new mesh (marks the transform dirty so bounds
re-sync).

Material

mr:Material( ) : Material — the shared material handle, or nil (no log) if unset.
mr:SetMaterial( Material material ) : void — sets the shared material and destroys any
existing per-instance override (preventing a stale clone from outliving its source).

NeverCull

mr:NeverCull( ) : boolean — whether frustum culling is bypassed for this renderer.
mr:SetNeverCull( boolean value ) : void — when true, the mesh is always drawn regardless
of the camera frustum. Takes any truthy value (lua_toboolean).

RenderScale

mr:RenderScale( ) : Vector3 — the per-renderer visual scale (default (1,1,1)).
mr:SetRenderScale( Vector3 scale ) : void — sets a non-uniform render scale (marks the
transform dirty).
mr:SetRenderScaleUniform( number scale ) : void — sets all three axes to scale.

Methods

Mutating actions. Setters silently no-op on a null/stale handle (no warning).

mr:Activate( ) : void
Sets the component’s own enabled flag, driving it through the component lifecycle so on_enable
fires (re-retaining the cull entry) rather than flipping a bare flag. The enable callback runs only
if the owning GameObject is also active.

mr:Deactivate( ) : void
Clears the own enabled flag — the renderer stops drawing. The disable callback runs only if the
renderer was actually active.

mr:GetMaterialInstance( ) : Material
Returns a per-instance clone of the shared material, creating it on the first call and caching it
thereafter (subsequent calls return the same instance). Use this to set per-object colors without
affecting other renderers. Returns nil (silently — no log) if the renderer is dead or there is no
material to clone.

mr:SetColor( Vector4 color ) : void
Sets the renderer’s per-instance color tint, interpreting color as sRGB (converted to
linear). This is a push constant — free, and truly local to this renderer. It is the renderer’s only
color channel and never touches the material.

mr:SetColorLinear( Vector4 color ) : void
As SetColor, but stores color verbatim with no sRGB→linear conversion.

Setting material color slots. The renderer tint above is a single per-instance color. A
material’s color slots (1..15) live in the material’s constant buffer — shared GPU memory — so
the renderer no longer exposes an indexed color setter. Reach the slots through the material
explicitly, choosing shared vs. per-instance deliberately:

  • mr:Material():SetColor(i, …) — mutates the shared material, recoloring every renderer that uses it.
  • mr:GetMaterialInstance():SetColor(i, …) — clones the material first, so the change is local to this renderer.

mr:SetBounds( number cx, number cy, number cz, number radius ) : void
Overrides the local-space bounding sphere (center + radius) used for culling, and arms the
transform-update countdown so the cull list re-syncs.

Example

-- Give each spawned cube its own tinted material instance (NOT the shared material).
-- (Adapted from data/scripts/session_setup.lua + main_scene_setup.lua.)
local mr = MeshRenderer.Add(go, Mesh.Get("cube"), material)
if mr then
    -- The per-instance push-constant tint — always safe, never shared.
    mr:SetColor(Vector4.new(0.8, 0.2, 0.2, 1.0))  -- sRGB red

    -- To vary a *material* color slot per object, clone first so you don't
    -- recolor every cube sharing `material`.
    local inst = mr:GetMaterialInstance()
    inst:SetColorLinear(1, Vector4.new(0.05, 0.05, 0.05, 1.0))

    mr:SetBounds(0, 0, 0, 0.866)  -- unit-cube bounding sphere
end

Venus Lua API › Rendering

SkinnedMeshRenderer

Draws a skinned mesh driven by an animated skeleton.

authored lua/venus_lua/src/lua_skinned_mesh_renderer.rs

SkinnedMeshRenderer

A component that draws a skinned mesh deformed by a bone skeleton. Attach one to a GameObject,
bind a mesh + material, then drive its skeleton each frame — either by playing an
AnimController or by posing individual bones directly. For static meshes use
MeshRenderer.

Static calls use the type table (SkinnedMeshRenderer.Add(...)); instance methods use colon
syntax on a component handle (smr:Sample(...)).

Types

SkinnedMeshRenderer — an opaque handle (userdata) to a SkinnedMeshRenderer component. A handle
may be null or stale (its component destroyed); it is safe to keep and pass around.
tostring(smr) yields the constant string SkinnedMeshRenderer (it does not embed a name).

Dead-handle behavior is not uniform, so it is stated per entry below. In general: most getters
log a NotFound warning and return nil; HasMaterialInstance logs and returns false;
GetMaterialInstance and ControllerInstance return nil silently (no warning); and all
setters / animation drivers no-op silently (no warning — unlike GameObject,
whose setters warn).

Bone indices are 0-based and index the skeleton array directly (skeleton()[index], no offset);
0 is the first bone and BoneCount() - 1 the last. An out-of-range index returns nil from a
getter and is ignored by SetBoneLocal.

Creation & Destruction

SkinnedMeshRenderer.Get( GameObject object ) : SkinnedMeshRenderer
Returns the SkinnedMeshRenderer on object, or nil (logs NotFound) if it has none. Raises if
object is not a GameObject userdata.

SkinnedMeshRenderer.Exists( SkinnedMeshRenderer smr ) : boolean
Returns true if the handle refers to a live component. Safe on null / stale / foreign handles
(returns false, never warns).

SkinnedMeshRenderer.Add( GameObject object, Mesh mesh, Material material ) : SkinnedMeshRenderer
Adds a new SkinnedMeshRenderer to object bound to mesh and material (mesh, bones, and
material all resolve lazily at first cull). Returns the new component handle, or nil
silently (no log) if the scene rejects the component. Raises if any argument is not the
expected userdata.

The component has no instance Destroy — its lifetime follows its GameObject.

Functions

Read-only queries. Each logs NotFound and returns nil on a null/stale handle unless noted.

smr:EffectiveMaterial( ) : Material
The material actually used for drawing — the per-instance override if one exists, otherwise the
shared material; nil if neither is set.

smr:HasMaterialInstance( ) : boolean
true if a per-instance material clone exists. On a dead handle it logs NotFound and returns
false (not nil), since its type is boolean.

smr:BoneCount( ) : integer
Number of bones in the skeleton. Valid bone indices are 0 .. BoneCount() - 1.

smr:BoneLocalPosition( integer index ) : Vector3
The bone’s position relative to its parent bone; nil (no warning) if index is out of range.

smr:BoneLocalRotation( integer index ) : Quaternion
The bone’s rotation relative to its parent bone; nil (no warning) if out of range.

smr:BoneWorldPosition( integer index ) : Vector3
The bone’s accumulated model-space position (skeleton root frame); nil (no warning) if out of
range.

smr:BoneWorldRotation( integer index ) : Quaternion
The bone’s accumulated model-space rotation; nil (no warning) if out of range.

smr:ControllerInstance( ) : AnimController
This renderer’s private AnimController instance created by PlayController,
for advanced per-instance control (states, weights, crossfade, SetTime). Returns nil
silently if PlayController was never called (or on a dead handle). The handle is
generational — it goes stale safely when the renderer is destroyed.

smr:Active( ) : boolean
The component’s own enabled flag — what Activate()/Deactivate() write. This is intent, not
effect: a renderer whose own flag is enabled but whose GameObject is deactivated still reports
true here — and is not drawing. Use ActiveInHierarchy() for the effective answer.

smr:ActiveInHierarchy( ) : boolean
true only when the own flag is enabled and the owning GameObject is active in hierarchy — i.e.
the component is actually running. This is the predicate the engine uses to gate update dispatch.

Removed 2026-07. smr:Enabled() / smr:SetEnabled(bool) are gone — they were exact
duplicates of this lifecycle pair, routing to the same activate/deactivate calls.
SetEnabled(true) / SetEnabled(false) become Activate() / Deactivate(). The honest
replacement for the Enabled() getter is ActiveInHierarchy(), not Active(): the old
getter read a field maintained by the enable/disable callbacks, which fire on
effective-activity transitions.

Properties (get / set)

Setters return void and no-op silently (no warning) on a null/stale handle.

Mesh

smr:Mesh( ) : Mesh — the bound skinned mesh, or nil if unset.
smr:SetMesh( Mesh mesh ) : void — binds a new mesh. Raises if mesh is not a Mesh userdata.

Material

smr:Material( ) : Material — the shared material, or nil if unset.
smr:SetMaterial( Material material ) : void — sets the shared material and destroys any
per-instance override; resolves lazily at the next cull.

RenderScale

smr:RenderScale( ) : Vector3 — the per-renderer visual scale (default (1, 1, 1)).
smr:SetRenderScale( Vector3 scale ) : void — sets a non-uniform render scale (accepts a
Vector3).
smr:SetRenderScaleUniform( number scale ) : void — sets all three axes to scale.

Methods

Mutating actions. All no-op silently (no warning) on a null/stale handle.

smr:Activate( ) : void
Sets the component’s own enabled flag, driving it through the component lifecycle (re-retaining the
cull entry and re-checking the animation slot’s bone capacity) rather than flipping a bare flag. The
enable callback runs only if the owning GameObject is also active.

smr:Deactivate( ) : void
Clears the own enabled flag — the renderer stops drawing. The disable callback runs only if the
renderer was actually active.

smr:GetMaterialInstance( ) : Material
Returns a per-instance clone of the shared material, creating it on the first call and caching it
thereafter (set per-instance colors on this material). Returns nil if there is no material to
clone, or silently nil on a dead handle.

smr:SetBoneLocal( integer index, Vector3 position, Quaternion rotation ) : void
Directly poses bone index in its parent-local frame and marks the animation dirty. Out-of-range
indices are ignored (no warning).

smr:PlayController( AnimController controller ) : boolean
Clones controller (a template) into a private per-renderer instance stored on this renderer,
so it advances its own play-head; any prior instance is destroyed first. The instance is reaped
automatically when the renderer is destroyed. Returns true on success, false on failure or a
dead handle. Drive playback each frame with smr:Sample(dt). This is the per-instance replacement
for SampleController.

smr:Sample( number dt ) : void
Advances this renderer’s own controller instance (from PlayController) by dt seconds and
samples the pose into the skeleton, marking it dirty for GPU upload. No-op if PlayController was
never called. Call smr:Sample(offset) once before the per-frame loop to seed a start phase (the
play-head advances and Loop-wraps to that phase).

smr:SampleController( AnimController controller, number dt ) : void
Samples the given controller directly into this renderer’s skeleton and advances that
controller’s
single play-head by dt seconds, marking the animation dirty. Takes both the
controller and dt (unlike Sample, which takes only dt).

Known issue — deprecated (verified in source). SampleController advances the shared
controller’s single play-head, so if the same controller is sampled by N renderers in a frame
its play-head advances N times — animation runs N×-too-fast for a shared controller
(lua_skinned_mesh_renderer.rs:447-465). Prefer PlayController + Sample, which give each
renderer an independent play-head. It is kept only for the rare case of one genuinely shared
play-head.

Example

-- A herd of dogs, each with its OWN animation play-head, seeded to a staggered
-- phase so a walk-wave travels across the pack.
-- (Adapted from data/scripts/main_scene_setup.lua.)
local function spawn_dog(object, mesh, mat, ctrl, phase)
    local smr = SkinnedMeshRenderer.Add(object, mesh, mat)
    if smr and smr:PlayController(ctrl) then
        smr:Sample(phase)   -- seed a start offset (one-time)
        return smr
    end
    return nil
end

function Script.Update(self)
    -- Each dog advances its own instance by the frame dt; phase offsets are kept.
    for i = 1, #self.dogs do
        self.dogs[i]:Sample(Time.DeltaTime())
    end
end

Venus Lua API › Rendering

Texture

GPU texture handle, plus CPU-editable dynamic textures.

authored lua/venus_lua/src/lua_texture.rs

Texture

A handle to a GPU texture. Most textures are loaded assets you look up by name and read dimensions
from. Dynamic textures (created via CreateDynamic / CreateDynamic3D) add a CPU-side staging
copy you can edit pixel-by-pixel and push to the GPU with Apply.

Static calls use the type table (Texture.Get(...)); instance methods use colon syntax on a handle
(tex:SetPixel(...)).

Types

Texture — an opaque handle (userdata) to a texture. A handle may be null or stale (its
texture destroyed); it is safe to keep and pass around. tostring(tex) yields Texture{name}, or
Texture{NULL} if dead.

The pixel-editing methods only apply to dynamic textures; on a non-dynamic or dead texture they
log InvalidArgument and no-op. Pixel coordinates are 0-based texel indices (x in 0 .. Width()-1, y in 0 .. Height()-1, z in 0 .. Depth()-1); an out-of-range coordinate is
rejected by the manager (logs InvalidArgument). Color components are integers in 0–255 (see
the known issue below), not normalized floats.

Creation & Destruction

Texture.Get( string name ) : Texture
Looks up a texture by its unique name. Returns nil (logs NotFound) if none exists.

Texture.Exists( Texture texture ) : boolean
Returns true if the handle refers to a live texture. Safe on null / stale / foreign handles
(returns false, never warns).

Texture.CreateDynamic( string name, integer width, integer height, integer format ) : Texture
Creates a 2D dynamic (CPU-editable) texture. format is a Texture.PixelFormat value. Returns the
new handle, or nil (logs InvalidArgument) if the format is unknown, the name is taken, or GPU
allocation fails.

Texture.CreateDynamic3D( string name, integer width, integer height, integer depth, integer format ) : Texture
Creates a 3D dynamic texture (a 2D texture when depth <= 1). format is a Texture.PixelFormat
value. Returns the new handle, or nil (logs InvalidArgument) on failure.

tex:Destroy( ) : void
Releases the GPU image (if any) and destroys the texture. No-op on a null handle.

Functions

Read-only queries. Instance getters log NotFound and return nil on a dead handle unless noted.

Texture.IsDynamic( Texture texture ) : boolean
true if texture is a dynamic (CPU-editable) texture. Safe on null / foreign handles (returns
false).

tex:ID( ) : string
The texture’s unique name.

tex:Width( ) : integer
Width in texels.

tex:Height( ) : integer
Height in texels.

tex:Depth( ) : integer
Depth in texels (1 for 2D textures). Returns 0 (not nil) on error.

tex:AspectRatio( ) : number
Width / height. Returns nil on error.

tex:GetPixel( integer x, integer y ) : integer, integer, integer, integer
Reads a texel from the current frame’s CPU staging copy of a dynamic texture as four 0–255
components r, g, b, a. Returns 0, 0, 0, 0 and logs InvalidArgument on error (non-dynamic, dead,
or out-of-range coordinate).

tex:GetPixel3D( integer x, integer y, integer z ) : integer, integer, integer, integer
As GetPixel, for a 3D dynamic texture. Returns 0, 0, 0, 0 on error.

tex:GetPixelData( ) : string
Returns the entire CPU staging buffer as a raw byte string — the texels packed in the texture’s
pixel format (e.g. 4 bytes/texel for RGBA8), row-major. Returns an empty string (logs NotFound)
on error.

Methods

Mutating actions on dynamic textures. They log InvalidArgument on a non-dynamic or dead
texture. Edits touch the current frame’s CPU staging copy; call Apply to schedule the GPU upload.

tex:SetPixel( integer x, integer y, integer r, integer g, integer b, integer a ) : void
Writes one RGBA texel into the staging copy. Components are integers (see the known issue).

tex:SetPixel3D( integer x, integer y, integer z, integer r, integer g, integer b, integer a ) : void
Writes one RGBA texel into a 3D dynamic texture.

tex:SetPixelData( string bytes ) : void
Overwrites the whole staging buffer from a raw byte string (must match the buffer’s size and format
layout, else InvalidArgument).

tex:SetPixelDataRange( integer offset, string bytes ) : void
Writes bytes into the staging buffer starting at byte offset. A negative offset logs
InvalidArgument and is rejected; an offset + length past the buffer end is rejected by the manager.

tex:Apply( ) : void
Drains the accumulated dirty region and marks it on the GPU image so the backend can issue a
targeted subregion copy for the current frame. No-op on a null handle or a texture with no GPU image.

Known issue — components are integers cast to u8, no clamp (verified in source).
SetPixel / SetPixel3D read each of r, g, b, a with luaL_checkinteger(...) as u8
(lua_texture.rs:387-390, :433-436). Two consequences: (1) they are integers, not
normalized floats — pass 255, not 1.0 (a fractional Lua number such as 0.5 raises “number
has no integer representation”); (2) values outside 0–255 wrap modulo 256 rather than
clamping (256 -> 0, -1 -> 255). Clamp/round in Lua (e.g. math.floor into 0..255) before
calling.

Constants

Texture.PixelFormat — pixel format discriminants passed to CreateDynamic / CreateDynamic3D.
An unrecognized value fails creation with InvalidArgument.

None (0), BGRA8_sRGB (1), R8 (2), RG8 (3), R16F (4), BGRA8 (5), RG16F (6),
R32F (7), RGBA16F (8), RG32F (9), D32 (10), D32_S8 (11), BC1 (12), BC1_sRGB (13),
BC2 (14), BC2_sRGB (15), BC3 (16), BC3_sRGB (17), BC4 (18), BC5 (19), BC6 (20),
BC7 (21), BC7_sRGB (22), RGBA8 (23), RGBA8_sRGB (24).

Example

-- Walk a colored texel across a 64x64 dynamic texture each frame.
-- (Adapted from data/scripts/dyn_texture_demo.lua.)
local px, py, frame = 0, 0, 0

function Script.Update(self)
    local tex = Texture.Get("dyn_demo")
    if not tex then return end

    -- Integer 0-255 components (math.floor keeps them in range).
    local r = math.floor((px / 64) * 255)
    local g = math.floor((py / 64) * 255)
    local b = frame % 256
    tex:SetPixel(px, py, r, g, b, 255)
    tex:Apply()

    px = px + 1
    if px >= 64 then px, py = 0, (py + 1) % 64 end
    frame = frame + 1
end

Venus Lua API › Rendering

RenderTexture

Off-screen color + depth target a camera can render into.

authored lua/venus_lua/src/lua_render_texture.rs

RenderTexture

An off-screen render target — a paired color and depth texture a Camera can draw into
instead of the swapchain. Create one, hand its GPU handles to a camera’s render targets, then sample
its color Texture in a material (e.g. for a mirror, minimap, or picture-in-picture).

Static calls use the type table (RenderTexture.Create(...)); instance methods use colon syntax on
a handle (rt:ColorTexture()).

Types

RenderTexture — an opaque 8-byte handle (userdata) to a render-target pair. A handle may be
null or stale (its target destroyed); it is safe to keep and pass around. tostring(rt)
yields the constant string RenderTexture (it does not embed a name).

Creation & Destruction

RenderTexture.Create( string name, integer width, integer height, integer colorFormat, integer depthFormat, string msaa ) : RenderTexture
Creates a render target named name. colorFormat / depthFormat are still RenderTexture.PixelFormat
integers (e.g. RGBA8 color, D32 depth); msaa is a multisample-level name (see
Constants). Returns the new handle, or nil (logs InvalidArgument) if either format
is unknown or GPU allocation fails. A bad msaa name does not return nil — it raises, because
it is resolved before any allocation happens.

RenderTexture.Get( string name ) : RenderTexture
Looks up an existing render texture by name. Returns nil (logs NotFound) if none exists.

RenderTexture.Exists( RenderTexture rt ) : boolean
Returns true if the handle refers to a live render texture. Safe on null / stale / foreign handles
(returns false, never warns).

rt:Destroy( ) : void
Destroys the render texture and releases its GPU color and depth images. No-op on a null handle.

Functions

Read-only queries on a handle.

rt:GpuColorHandle( ) : integer
The color target’s GPU handle, packed into an integer for a camera’s color render target. Returns
0 (logs NotFound) on error. See “GPU handles” below.

rt:GpuDepthHandle( ) : integer
The depth target’s packed GPU handle, for a camera’s depth render target. Returns 0 (logs
NotFound) on error.

rt:ColorTexture( ) : Texture
The color attachment as a sampleable Texture handle (bind it in a material). nil if
unset, or nil (logs NotFound) on a dead handle.

rt:DepthTexture( ) : Texture
The depth attachment as a Texture handle. nil if unset, or nil (logs NotFound) on
a dead handle.

rt:Width( ) : integer
Width in pixels. Returns 0 (not nil, logs NotFound) on error.

rt:Height( ) : integer
Height in pixels. Returns 0 on error.

GPU handles

GpuColorHandle / GpuDepthHandle return a GpuHandle<GpuRenderTarget> reinterpreted as a raw
u32 integer
(a 4-byte handle, layout [gen:8 | pool_type:8 | index:16]). Treat it as opaque:
do not do arithmetic on it — just pass it straight to camera:SetRenderTarget(...) /
camera:SetDepthRenderTarget(...), which transmute the same integer back into a handle. 0 is the
null handle and is also the error return, so a failed lookup passes through harmlessly as “no
target.”

Constants

RenderTexture.PixelFormat — pixel format discriminants for colorFormat / depthFormat. An
unrecognized value fails Create with InvalidArgument.

None (0), BGRA8_sRGB (1), R8 (2), RG8 (3), R16F (4), BGRA8 (5), RG16F (6),
R32F (7), RGBA16F (8), RG32F (9), D32 (10), D32_S8 (11), BC1 (12), BC1_sRGB (13),
BC2 (14), BC2_sRGB (15), BC3 (16), BC3_sRGB (17), BC4 (18), BC5 (19), BC6 (20),
BC7 (21), BC7_sRGB (22), RGBA8 (23), RGBA8_sRGB (24).

The multisample level for msaa is a name, matched exactly with no case folding — one of
"None", "X2", "X4", "X8", "X16". An unrecognized value raises a Lua error listing all
five. The name is deliberately not the sample count: "4" is rejected, because the engine
numbers these levels 1/2/4/8/16 while the settings layer numbers the same five 0..4, and a bare
integer is ambiguous between the two.

The RenderTexture.MSAALevel sub-table was deleted (2026-07), so RenderTexture.MSAALevel
is now nil. It had only ever exposed None/X4/X8X2 and X16 were unreachable from
Lua
, and Create matched only 1/4/8, so asking for 2 or 16 samples silently produced
none. Both halves are gone; all five levels are now reachable and anything else raises.

Example

-- Render a second camera into a 1024x1024 target and show it on a UI quad.
-- (Adapted from data/scripts/main_scene_setup.lua.)
local PF = RenderTexture.PixelFormat
local rt = RenderTexture.Create("mini_camera", 1024, 1024, PF.RGBA8, PF.D32, "None")
if rt then
    -- Packed GPU handles go straight to the camera's render targets.
    cam:SetRenderTarget(rt:GpuColorHandle())
    cam:SetDepthRenderTarget(rt:GpuDepthHandle())
    -- The color attachment is a sampleable Texture for a material.
    rt_material:SetTexture(0, rt:ColorTexture())
end

Venus Lua API › Rendering

Shader

A compiled pipeline referenced by materials.

authored lua/venus_lua/src/lua_shader.rs

Shader

A handle to a compiled graphics pipeline (a vertex + fragment program plus its pipeline state).
Shaders are looked up by name and assigned to a Material (material:SetShader(...));
the geometry (3D) variant can also be created at runtime from named SPIR-V stages via CreateGeo.

Static calls use the type table (Shader.Get(...)); instance methods use colon syntax on a handle
(shader:RenderQueue()). The Lua surface is read-only — there are no property setters and no
Destroy.

Types

Shader — an opaque handle (userdata) to a compiled pipeline. A handle may be null or
stale. The instance queries on a dead handle log a warning (NotFound) and return nil; they
never raise on a live-but-empty result. Passing a non-Shader userdata (or wrong argument types)
to a method raises a Lua type error. tostring(shader) yields Shader{name}, or Shader{NULL} if
the handle is dead.

Creation & Destruction

Shader.CreateGeo( string name, string vertName, string fragName, integer renderQueue, integer textureCount ) : Shader
Creates a geometry (3D) shader named name from the named vertex and fragment SPIR-V stages, with
the given render-queue order and texture-binding count. Allocates a GPU pipeline through the render
backend, so it is only meaningful with a live backend. Returns the new handle, or nil (logs
InvalidArgument) on failure. renderQueue and textureCount are truncated to u16 (values
≥ 65536 wrap silently).

Shader.Get( string name ) : Shader
Looks up a shader by its unique name. Returns nil (logs NotFound) if none exists.

Shader.Exists( Shader shader ) : boolean
Returns true if the handle refers to a live shader. Safe on null/stale/foreign handles (returns
false) — the one call here that never warns or raises.

Shaders have no instance Destroy from Lua.

Functions

Read-only queries. Each logs NotFound and returns nil on a null/stale handle.

shader:ID( ) : string
The shader’s unique name.

shader:RenderQueue( ) : integer
The render-queue order value (a u16; controls draw sorting relative to other shaders).

shader:DepthWrite( ) : boolean
Whether the pipeline writes to the depth buffer.

shader:AlphaToCoverage( ) : boolean
Whether alpha-to-coverage is enabled on the pipeline.

Example

-- Assign a looked-up shader to a material, and inspect its pipeline state.
-- (Adapted from data/scripts/session_setup.lua.)
local mat = Material.Create("lit_material")
local geo = Shader.Get("geo_lit")
mat:SetShader(geo)
mat:SetColorLinear(0, Vector4.new(1, 1, 1, 1))

if Shader.Exists(geo) then
    -- geo:ID(), geo:RenderQueue(), geo:DepthWrite(), geo:AlphaToCoverage()
    print(geo:ID(), "queue=" .. geo:RenderQueue(), "depthWrite=" .. tostring(geo:DepthWrite()))
end

Venus Lua API › Physics

Physics

Deterministic scene queries — raycasts, shape casts, overlaps.

authored lua/venus_lua/src/lua_physics.rs

Physics

A module singleton (global functions only, no handles) for querying the deterministic physics
world: raycasts, shape sweeps, overlap tests, and the layer-collision matrix. Fixed-point
results are converted to f32 at this script boundary. Queries whose name is unsuffixed run
against rigid bodies; the engine merges voxel-terrain hits by distance where a collision world is
wired.

Vector arguments (origin, direction, center, halfExtents, …) take a Vector3.
layerMask is an integer bitmask selecting which layers the query tests.

Result tables

A single hit is a table { point: Vector3, normal: Vector3, distance: number, gameObject: GameObject }. Multi-hit queries return a 1-based array of these (or of GameObjects, as noted).

Functions

Physics.Raycast( Vector3 origin, Vector3 direction, integer layerMask ) : table or nil
Physics.Raycast( Vector3 origin, Vector3 direction, integer layerMask, number maxDistance ) : table or nil
Casts a ray and returns the closest hit as a {point, normal, distance, gameObject} table, or
nil if nothing was hit. maxDistance defaults to unbounded.

Physics.RaycastAll( Vector3 origin, Vector3 direction, integer layerMask ) : table
Physics.RaycastAll( Vector3 origin, Vector3 direction, integer layerMask, number maxDistance ) : table
Returns an array of every hit along the ray (empty table if none).

Physics.LinecastAll( Vector3 start, Vector3 end, integer layerMask ) : table
Returns an array of all hits on the segment from start to end.

Physics.SphereTestAll( Vector3 center, number radius, integer layerMask ) : table
Returns an array of the GameObjects whose colliders overlap the sphere.

Physics.SphereCast( Vector3 center, number radius, Vector3 direction, number maxDistance, integer layerMask ) : table or nil
Sweeps a sphere and returns the closest hit, or nil.

Physics.CapsuleCast( Vector3 center, Quaternion rotation, number radius, number halfHeight, Vector3 direction, number maxDistance, integer layerMask ) : table or nil
Sweeps an oriented capsule and returns the closest hit, or nil.

Physics.BoxCast( Vector3 center, Quaternion rotation, Vector3 halfExtents, Vector3 direction, number maxDistance, integer layerMask ) : table or nil
Sweeps an oriented box and returns the closest hit, or nil.

Physics.OverlapBox( Vector3 center, Quaternion rotation, Vector3 halfExtents, integer layerMask ) : table
Returns an array of the GameObjects overlapping the oriented box.

Physics.CheckSphere( Vector3 center, number radius, integer layerMask ) : boolean
true if any collider overlaps the sphere.

Physics.CheckBox( Vector3 center, Quaternion rotation, Vector3 halfExtents, integer layerMask ) : boolean
true if any collider overlaps the oriented box.

Physics.IgnoreLayerCollision( integer a, integer b, boolean ignore ) : void
Sets whether layers a and b ignore each other in the solver (Unity semantics: ignore == true clears the collide bit). Intended for startup/config — the layer table is treated as
immutable during simulation. Out-of-range indices (≥ 32) are a no-op.

Physics.GetIgnoreLayerCollision( integer a, integer b ) : boolean
true if layers a and b are set not to collide. Out-of-range or unwired returns
false.

Constants

Physics exposes no constants. Force modes are quoted strings, not a constants table — see
FixedRigidBody → AddForce.

rb:AddForce(Vector3.new(0, 12, 0), "Impulse")   -- correct

Removed (2026-07). A global ForceMode table of integers (Force=0 … Acceleration=3)
used to be registered here. It was added for an earlier integer-taking rigid-body binding that
the fixed-dynamics rewrite retired; the replacement takes mode names, so every use of the
table silently applied Force (luaL_checkstring coerces 1"1", which matched nothing).
The table is gone, and an unrecognized mode name now raises a Lua error instead of defaulting.
ForceMode is nil — reaching for it is a loud error at the call site, which is the point.

Example

-- Ground check: cast a short ray straight down from the player.
function Script.Update(self)
    local x, y, z = self.object:WorldPosition()
    local hit = Physics.Raycast(Vector3.new(x, y, z), Vector3.new(0, -1, 0), self.groundMask, 1.2)
    self.grounded = hit ~= nil
    if hit then
        -- hit.point, hit.normal, hit.distance, hit.gameObject
        self.groundNormal = hit.normal
    end
end

Venus Lua API › Physics

Collider

Collision shape on a GameObject — sphere, box, or capsule.

authored lua/venus_lua/src/lua_collider.rs

Collider

A collision shape attached to a GameObject — a sphere, an oriented or axis-aligned box, or a
capsule — plus a layer bitmask and a trigger flag. Enabling a collider registers a static
(mass-0) body
in the deterministic FixedXpbdWorld, so dynamic bodies (like
DynamicCharacter) collide with it and Physics queries can hit
it.

Static calls use the type table (Collider.Add(...)); instance methods use colon syntax on a
handle (collider:SetBox(...)).

Types

Collider — an opaque handle (userdata) to a collider component on a GameObject. It may go
stale when its component or GameObject is destroyed; a stale handle is safe to hold and pass
around. Calling a method whose receiver is not a Collider raises a type error — the one exception
is Collider.Exists, which is safe on any value. On a genuine but stale handle nothing raises:
getters log a warning and return nil, shape/config setters silently no-op, and Exists returns
false. tostring(collider) is always the literal string Collider (it never includes the
object’s name).

Known issue: A Collider is effectively static. Its collision body is placed once, at
on_enable, from the GameObject’s world transform, and is not re-posed when the GameObject
moves at runtime (engine/scenegraph/src/collider.rs:60). Moving a collider’s GameObject leaves
its collision surface behind at the enable-time pose. Re-posing happens only as a side effect of
calling a shape or filter setter — each rebuilds the body at the current transform via
sync_collider_shape. Author colliders where they belong, or re-call a setter after moving one.

Creation & Destruction

Collider.Add( GameObject gameObject ) : Collider
Attaches a new collider to gameObject and returns its handle. It starts as a degenerate
zero-extent AABB
with layer mask 0xFFFFFFFF (all layers) and is not a trigger — call a shape
setter to give it geometry. Returns nil and logs an error if the component can’t be added (the
GameObject is invalid, it already has a collider, or the pool is exhausted). Raises a type error if
gameObject is not a GameObject.

Collider.Get( GameObject gameObject ) : Collider
Returns the collider on gameObject, or nil (logging a NotFound warning) if it has none.

Collider.Exists( Collider collider ) : boolean
Returns true if the handle refers to a live collider. Safe on any value — a non-collider or
a stale handle returns false and never raises. (The one method that does not type-check its
receiver.)

There is no Destroy binding — a collider is removed with its GameObject; disabling the GameObject
unregisters its collision body, and re-enabling re-registers it.

Functions

Read-only queries.

collider:Active( ) : boolean or nil
The component’s own enabled flag — what Activate()/Deactivate() write. Logs a warning
(NotFound) and returns nil on a stale handle. This is intent, not effect: a collider whose
own flag is enabled but whose GameObject is deactivated still reports true.

collider:ActiveInHierarchy( ) : boolean or nil
true only when the own flag is enabled and the owning GameObject is active in hierarchy —
i.e. the component is actually running. This is the predicate the engine uses to gate update
dispatch. Returns nil on a stale handle.

Properties (get / set)

Setters rebuild the static fixed body so the change reaches the world (the cheap per-frame position
sync does not carry the mask or the sensor flag). Both setters silently no-op on a stale handle.

LayerMask

collider:LayerMask( ) : integer — the collider’s 32-bit layer bitmask (default 0xFFFFFFFF).
Logs a warning and returns nil on a stale handle.
collider:SetLayerMask( integer mask ) : void — sets the bitmask and rebuilds the broadphase
entry so the new mask reaches queries.

Trigger

collider:IsTrigger( ) : booleantrue if the collider is a trigger (sensor) volume. nil

  • warning on a stale handle.
    collider:SetTrigger( boolean on ) : void — marks the collider as a trigger (Unity
    isTrigger); overlaps then surface through the deterministic trigger-event pass
    (OnTriggerEnter/Stay/Exit) instead of producing solid contacts.

Methods

Shape setters — bake the collider’s local geometry, then rebuild the static body at the current
transform. Every Vector3 argument must be a native Vector3 value; three loose numbers raise
a type error (unlike the GameObject setters, which accept either). All silently no-op on a stale
handle.

collider:SetSphere( Vector3 center, number radius ) : void
Sphere of radius, centered at local-space center. Orientation-free.

collider:SetAABB( Vector3 center, Vector3 halfExtents ) : void
Axis-aligned box with half-extents halfExtents at center. The body’s orientation is pinned to
identity — it ignores the GameObject’s rotation.

collider:SetBox( Vector3 center, Vector3 halfExtents ) : void
Oriented box — same half-extents as SetAABB, but the body tracks the GameObject’s rotation.

collider:SetCapsule( Vector3 center, number radius, number halfHeight ) : void
Y-axis capsule of radius and halfHeight (center to cap-center) at center. Tracks rotation.

Mesh colliders removed (2026-07-17): SetTriangleMesh and SetConvexMesh are no longer
exposed to Lua.
They never produced triangle-mesh or convex-hull collision — both silently
registered an oriented bounding box sized to the vertices’ AABB (FixedShape::Box), ignoring the
triangles/hull entirely (and SetTriangleMesh’s indices outright). The names promised fidelity
the code did not deliver, so the bindings were commented out (lua/venus_lua/src/lua_collider.rs;
scene glue in engine/scenegraph/src/scene.rs) pending a real mesh narrowphase. For non-box
geometry, decompose it into primitive colliders (sphere / box / capsule) yourself.

Lifecycle

Both silently no-op on a stale handle.

collider:Activate( ) : void
Sets the component’s own enabled flag. Its enable callback runs only if the owning GameObject is
also active — and it is that enable callback which registers the collision body.

collider:Deactivate( ) : void
Clears the own enabled flag; the disable callback runs only if the component was actually active.

Example

-- A box trigger zone on layer 3: reports OnTriggerEnter/Stay/Exit instead of blocking.
-- Vector3 args must be native Vector3 values (loose numbers raise a type error).
function Script.OnCreate(self)
    local col = Collider.Add(self.object)
    col:SetBox(Vector3.new(0, 0, 0), Vector3.new(0.5, 0.5, 0.5))  -- a 1-unit cube
    col:SetLayerMask(1 << 3)                                       -- only layer 3
    col:SetTrigger(true)
    -- The static body is seeded here at the object's world pose and is NOT re-posed
    -- if the object later moves — re-call a setter after moving it.
end

Venus Lua API › Physics

KinematicController

Deterministic fixed-point character controller — collide-and-slide.

authored lua/venus_lua/src/lua_kinematic_controller.rs

KinematicController

A deterministic character-controller component: a capsule that sweeps through the world with
collide-and-slide (auto step-up/down, wall sliding, depenetration, walkable-slope grounding). It
mirrors a conventional controller’s API (Move, IsGrounded, CollisionFlags, GroundNormal,
Radius/SetRadius, …) and adds Rotation/SetRotation for an orientation-aware capsule.

The component owns fixed-point (deterministic) authoritative state and derives its f32
transform each frame — so it must be driven from Script.FixedUpdate at the fixed cadence
(Time.FixedDeltaTime()), not from Update. Render interpolation (stair smoothing over risers
and ledge drops) is handled internally; the authoritative state and every determinism hash stay
untouched.

Static calls use the type table (KinematicController.Add(go)); instance methods use colon syntax
on a handle (kc:Move(...)).

Types

KinematicController — an opaque handle (userdata) to the controller component on a GameObject.
A handle may be stale (its component or GameObject destroyed); it is safe to keep and pass
around. Passing a value that is not a KinematicController raises a type error, but calling a method
on a stale handle never raises — it returns the documented default (nil, false, 0, or
world-up) or no-ops.

Creation & Destruction

KinematicController.Add( GameObject object ) : KinematicController
Attaches a new controller to object and returns its handle, with default capsule tunables
(radius 0.4, half-height 0.9, step 0.3, skin 0.08, slope 46°, no ground plane). Returns
nil and logs an error if the controller can’t be added (the GameObject is invalid, already has
one, or the pool is exhausted).

KinematicController.Get( GameObject object ) : KinematicController
Returns the controller attached to object, or nil if it has none (no warning).

KinematicController.Exists( KinematicController controller ) : boolean
Returns true if the handle refers to a live controller. Safe on stale handles (returns false).

There is no Destroy binding — a controller is removed when its GameObject is destroyed.

Functions

Read-only queries. On a stale handle each returns a default (nil, or the noted fallback) without
raising.

kc:IsGrounded( ) : boolean
true if the controller is resting on a walkable surface (slope ≤ SlopeLimit). false on a
stale handle.

kc:IsWalkable( ) : boolean
Alias of IsGrounded — the slide controller only reports grounded on walkable surfaces, so the
two are identical.

kc:GroundNormal( ) : Vector3
Unit surface normal of the current ground contact ((0, 1, 0) on flat ground). Returns world up
(0, 1, 0) when airborne or on a stale handle.

kc:GroundAngle( ) : number
Slope steepness of the ground under the controller, in degrees from vertical (0 = flat, up
to SlopeLimit when grounded). Derived from the ground normal. 0 on a stale handle.

kc:CollisionFlags( ) : integer
Bitmask (OR) of the COLLISION_* values recording which sides touched during the last Move
(below / sides / above). 0 (COLLISION_NONE) if nothing was hit or on a stale handle. See
Constants.

kc:DidLand( ) : boolean
true if the controller became grounded during the last Move (the EVENT_LANDED transition —
was airborne, now grounded). false on a stale handle.

kc:DidLeaveGround( ) : boolean
true if the controller left the ground during the last Move (the EVENT_LEFT_GROUND
transition). false on a stale handle.

kc:Active( ) : boolean or nil
The component’s own enabled flag — what Activate()/Deactivate() write. nil (+ a logged
NotFound) on a stale handle, so a destroyed controller stays distinguishable from a disabled one.
This is intent, not effect: a controller whose own flag is enabled but whose GameObject is
deactivated still reports true.

kc:ActiveInHierarchy( ) : boolean or nil
true only when the own flag is enabled and the owning GameObject is active in hierarchy —
i.e. the component is actually running. This is the predicate the engine uses to gate update
dispatch. Returns nil on a stale handle.

Properties (get / set)

Getters return nil on a stale handle unless noted; setters no-op. Vector3 / Quaternion
arguments must be native math values (not three loose numbers). Config values are quantized to
fixed-point when the next Move runs.

Velocity

kc:Velocity( ) : Vector3 — current velocity (units/second).
kc:SetVelocity( Vector3 velocity ) : void — sets the velocity directly. Move overwrites it
each call, so this is mainly useful for reading (kc:Velocity().y to integrate gravity) rather
than setting.

Position

kc:Position( ) : Vector3 — current world position (the render-derived f32 value).
kc:SetPosition( Vector3 position ) : void — teleports the controller, setting the
fixed-point authoritative position and clearing stair-smoothing. Use for spawns/respawns, not
per-frame motion (use Move).

Rotation

kc:Rotation( ) : Quaternion — current orientation.
kc:SetRotation( Quaternion rotation ) : void — sets the orientation. The capsule mover is
orientation-aware, so this turns the body (e.g. to face the movement direction).

Radius

kc:Radius( ) : number — capsule radius (default 0.4).
kc:SetRadius( number radius ) : void

Height

kc:Height( ) : number — capsule half-height, center to cap-center (default 0.9). Named
Height for convention, but the value is the half-height.
kc:SetHeight( number halfHeight ) : void — sets the half-height.

SlopeLimit

kc:SlopeLimit( ) : number — max walkable slope in degrees (default 46). Steeper
surfaces are treated as walls and do not ground the controller.
kc:SetSlopeLimit( number degrees ) : void

StepHeight

kc:StepHeight( ) : number — max step height, both directions (default 0.3): auto
step-up over risers up to this height, and step-down snap when descending ledges/slopes.
kc:SetStepHeight( number height ) : void

SkinWidth

kc:SkinWidth( ) : number — contact skin gap held back from surfaces to prevent sticking
(default 0.08).
kc:SetSkinWidth( number width ) : void

GroundPlaneY

kc:GroundPlaneY( ) : number or nil — world y of the optional infinite fallback floor, or
nil if none is set (the default). nil also on a stale handle.
kc:SetGroundPlaneY( number y ) : void — enables an infinite walkable floor at world y,
layered under the voxel terrain (terrain wins where it exists). Catches the character in open
space so it can’t fall through the world. There is no un-set binding — it can only be enabled or
moved.

Methods

kc:Move( Vector3 velocity, number dt ) : void
The core per-step drive. Sets the controller’s velocity to velocity (units/second, with gravity
already folded in by the caller — the controller applies none), then advances it by dt seconds
against the deterministic collision world using collide-and-slide (sweep, step-up/down, wall slide,
depenetration). Refreshes grounded state, ground normal, collision flags, and the land /
leave-ground events. No-op on a stale handle or when no collision world is set. Call once per fixed
step from Script.FixedUpdate with dt = Time.FixedDeltaTime().

kc:Activate( ) : void
Sets the component’s own enabled flag. Its enable callback runs only if the owning GameObject is
also active. Silently no-ops on a stale handle.

kc: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.

Constants

Enum fields on the global KinematicController table (e.g. KinematicController.COLLISION_BELOW)
— not on instances.

Collision flags. CollisionFlags() returns a bitmask (bitwise OR) of these; test a side with
bitwise AND (flags & KinematicController.COLLISION_SIDES ~= 0):

Constant Value Meaning
COLLISION_NONE 0 Nothing touched during the last Move.
COLLISION_BELOW 1 Touched a walkable surface below (ground).
COLLISION_SIDES 2 Touched a wall to the side.
COLLISION_ABOVE 4 Touched a ceiling above.

Events. One-step ground transitions, surfaced by the DidLand() / DidLeaveGround() query
methods (you rarely read these constants directly):

Constant Value Meaning
EVENT_NONE 0 No transition this step.
EVENT_LANDED 1 Became grounded this step — read via DidLand().
EVENT_LEFT_GROUND 2 Left the ground this step — read via DidLeaveGround().

Example

-- A deterministic WASD character. Adapted from data/scripts/kinematic_player.lua.
-- Driven from FixedUpdate at the fixed cadence — the controller's state is
-- fixed-point and lockstep-deterministic; render smoothing is handled internally.
local Script = {}

local SPEED, GRAVITY, JUMP = 5.0, 20.0, 8.0

function Script.OnCreate(self)
    KinematicController.Add(self.object)
    local kc = KinematicController.Get(self.object)
    kc:SetStepHeight(0.6)    -- climb 0.5-unit risers
    kc:SetGroundPlaneY(0.0)  -- fallback floor so it can't fall through empty space
end

function Script.FixedUpdate(self)
    local kc = KinematicController.Get(self.object)
    if kc == nil then return end
    local dt = Time.FixedDeltaTime()
    if dt <= 0 then return end

    -- Horizontal input (world axes; forward is -Z).
    local ix, iz = 0, 0
    if Input.Key("move_forward") then iz = iz + 1 end
    if Input.Key("move_back")    then iz = iz - 1 end
    if Input.Key("move_right")   then ix = ix + 1 end
    if Input.Key("move_left")    then ix = ix - 1 end
    local move = Vector3.new(ix, 0, -iz)
    if move:sqrMagnitude() > 1 then move = Vector3.Normalize(move) end
    move = move * SPEED

    -- Script-driven gravity (the controller applies none).
    local vy
    if kc:IsGrounded() then
        vy = -2.0                                -- gentle stick to follow slopes/steps
        if Input.KeyDown("jump") then vy = JUMP end
    else
        vy = kc:Velocity().y - GRAVITY * dt
    end

    kc:Move(Vector3.new(move.x, vy, move.z), dt)

    -- Face the movement direction (orientation-aware capsule).
    if move:sqrMagnitude() > 0.01 then
        local yaw = math.deg(math.atan(move.x, move.z))
        kc:SetRotation(Quaternion.AngleAxis(yaw, Vector3.new(0, 1, 0)))
    end
end

return Script

Venus Lua API › Physics

DynamicCharacter

Two-way, massy character capsule in the deterministic physics world.

authored lua/venus_lua/src/lua_dynamic_character.rs

DynamicCharacter

A dynamic character controller: a real rigid-body capsule in the app-owned FixedXpbdWorld whose
orientation is locked (it cannot tip) and whose horizontal motion is a force-capped velocity
motor
— light crates yield to it, heavy crates block it. Gravity and contacts own vertical
motion; Jump is an impulse. It is the two-way, massy counterpart to
KinematicController (which sweeps a shape and never pushes anything).

Because its state is fixed-point and lockstep-deterministic, drive it from Script.FixedUpdate at
the fixed cadence, not from Update. Shape and mass are baked once, at attach time, via Add.
Static calls use the type table (DynamicCharacter.Add(...)); instance methods use colon syntax
(dc:Move(...)).

Types

DynamicCharacter — an opaque handle (userdata) to a dynamic-character component on a GameObject.
Passing a value that is not a DynamicCharacter to any method (including Exists) raises a type
error; a genuine stale handle never raises — it returns the documented default (nil /
false) or no-ops. The handle is safe to hold and pass around. There is no custom __tostring, so
tostring(dc) is Lua’s default userdata string.

The body is registered on on_enable, not on Add. Until it is registered, every
world-touching method — Move, Stop, Jump, IsGrounded, Velocity/SetVelocity,
Position, SetFriction, SetStepOffset — no-ops or returns its default. Guard with
IsRegistered() (the demo script does). Registration is torn down on on_disable, so deactivating
the GameObject removes the body from the simulation; reactivating re-registers it and re-applies
the cached friction and step offset.

The GameObject’s rotation is owned by gameplay: the simulation leaves it alone (the body’s
orientation is locked, and update writes position only), so animate facing/yaw freely — it never
feeds back into physics. There is no SetRotation binding (unlike KinematicController); rotate the
GameObject directly.

Creation & Destruction

DynamicCharacter.Add( GameObject gameObject, number halfHeight, number radius, number mass ) : DynamicCharacter
Attaches a dynamic-character capsule to gameObject. halfHeight, radius, and mass are
optional, defaulting to 0.5, 0.3, and 80 (kg). The push-force budget is not a parameter —
it defaults to 400 N; raise it with SetMaxForce. Shape and mass are baked when the body
registers (on_enable), which seeds it at the GameObject’s world position — so set the spawn
position before Add (as dynamic_player.lua does). Returns nil and logs an error if the
component can’t be added (invalid GameObject, already has one, or the pool is exhausted).

DynamicCharacter.Get( GameObject gameObject ) : DynamicCharacter
Returns the character on gameObject, or nil if it has none (no warning, unlike Collider.Get).

DynamicCharacter.Exists( DynamicCharacter character ) : boolean
Returns true if the handle refers to a live component. Safe on a genuine stale handle (returns
false), but raises a type error if character is not a DynamicCharacter (including nil) — see
Types.

There is no Destroy binding — the character is removed with its GameObject; disabling the
GameObject unregisters its body from the simulation.

Functions

Read-only queries. Each returns its default (false, or nil) when the body is unregistered or no
physics world is wired.

dc:IsGrounded( ) : boolean
true if the body had a ground-like contact (contact normal y ≥ 0.7, ≈ 45°) during the last
fixed step — voxel terrain, a static collider, or standing on a crate all count. false when
unregistered.

dc:Position( ) : Vector3
The body’s current world position — the authoritative fixed-point capsule center converted to
f32, not the interpolated render transform. nil when unregistered.

dc:IsRegistered( ) : boolean
true once the body exists in the world (after on_enable); false on a stale handle. Use it to
gate the other methods (see Types).

The two lifecycle getters below read the scene component, not the world body, so they answer
normally on an attached-but-unregistered character (unlike everything else in this section).

dc:Active( ) : boolean or nil
The component’s own enabled flag — what Activate()/Deactivate() write. nil (+ a logged
NotFound) on a stale handle. This is intent, not effect: a character whose own flag is enabled
but whose GameObject is deactivated still reports true.

dc:ActiveInHierarchy( ) : boolean or nil
true only when the own flag is enabled and the owning GameObject is active in hierarchy —
i.e. the component is actually running. This is the predicate the engine uses to gate update
dispatch. Returns nil on a stale handle.

Properties (get / set)

Velocity

dc:Velocity( ) : Vector3 — the body’s current linear velocity (units/second), or nil when
unregistered.
dc:SetVelocity( Vector3 velocity ) : void — sets the full 3D linear velocity directly.
velocity must be a native Vector3 (loose numbers raise). No-op when unregistered. An active
Move drive re-steers the horizontal velocity every step, so this is mainly for vertical kicks or
teleport-stops.

Methods

Locomotion and tuning.

dc:Move( number vx, number vz ) : void
Sets a persistent, force-capped horizontal velocity motor toward (vx, vz) in units/second.
Note the arguments are two loose numbers (not a Vector3) and there is no delta-time: this
is not a per-step advance — it upserts a drive that the solver re-applies every fixed step until
you change it or call Stop(), so calling it once keeps the character moving. (Contrast
KinematicController:Move, which takes a velocity Vector3 plus dt
and advances a single step.) The force cap is the push budget (SetMaxForce). Vertical motion is
untouched — gravity, contacts, and Jump own it. Idempotent; call each fixed step or on input
change. No-op when unregistered.

dc:Stop( ) : void
Removes the drive entirely — the character then slides freely under friction and contacts.
(Move(0, 0) is different: active braking toward zero under the force budget.) No-op when
unregistered.

dc:Jump( number impulse ) : void
Applies a vertical impulse of impulse = mass·Δv (e.g. mass * 6 for a ~6 units/second take-off).
Does not check grounded — gate on IsGrounded() yourself. No-op when unregistered.

dc:SetMaxForce( number newtons ) : void
Sets the locomotion/push force budget (default 400 N). Stored on the component and applied by the
next Move — it is not pushed to the world immediately. Works before registration; no-op on a
stale handle.

dc:SetFriction( number mu ) : void
Sets the body’s Coulomb friction (characters default a deliberately low 0.05: the drive owns
horizontal control, and a high μ fights it — μ·m·g rivals the force cap). Applied live when
registered and cached on the component so a re-enable keeps it. No-op on a stale handle.

dc:SetStepOffset( number height ) : void
Sets the max auto step-up (stair-climb) height in units; 0 disables (the default, and the
byte-identical off path). Terrain only — the character cannot step onto crates or other dynamic
bodies. Applied live when registered and cached for re-enable. No-op on a stale handle.

Lifecycle

Both silently no-op on a stale handle. Because registration follows the component’s enable state
(see Types), these are the component-level lever on whether the body is in the simulation
at all.

dc:Activate( ) : void
Sets the component’s own enabled flag. Its enable callback runs only if the owning GameObject is
also active — and that callback is what registers the body.

dc:Deactivate( ) : void
Clears the own enabled flag; the disable callback runs only if the component was actually active.

Example

-- A deterministic dynamic (massy) character. Adapted from data/scripts/dynamic_player.lua.
-- Driven from FixedUpdate: Move sets a persistent velocity motor the solver applies each
-- fixed step, so this must run at the fixed cadence for lockstep determinism.
local Script = {}

local SPEED      = 5.0     -- target horizontal speed (units/second)
local JUMP_SPEED = 6.0     -- take-off speed; impulse = mass * this
local MASS       = 80.0    -- must match the DynamicCharacter.Add mass
local MAX_FORCE  = 3000.0  -- push budget (N); the 400 N default is gentle

function Script.OnCreate(self)
    -- Set the spawn position BEFORE Add — the body seeds at the world position on_enable.
    self.object:SetWorldPosition(0, 7, 0)
    DynamicCharacter.Add(self.object, 0.9, 0.4, MASS)  -- half-height, radius, mass
    local dc = DynamicCharacter.Get(self.object)
    if dc then dc:SetMaxForce(MAX_FORCE) end
end

function Script.FixedUpdate(self)
    local dc = DynamicCharacter.Get(self.object)
    if dc == nil or not dc:IsRegistered() then return end

    -- WASD → world-axis direction (forward is -Z).
    local ix, iz = 0, 0
    if Input.Key("move_forward") then iz = iz + 1 end
    if Input.Key("move_back")    then iz = iz - 1 end
    if Input.Key("move_right")   then ix = ix + 1 end
    if Input.Key("move_left")    then ix = ix - 1 end
    local move = Vector3.new(ix, 0, -iz)
    if move:sqrMagnitude() > 1 then move = Vector3.Normalize(move) end
    move = move * SPEED

    -- Horizontal velocity motor (two loose numbers, no dt). (0,0) = active braking.
    -- Vertical is owned by gravity/contacts — we do NOT fold gravity into the drive.
    dc:Move(move.x, move.z)

    -- One impulse per press, only when grounded (Jump itself does not check).
    if Input.KeyDown("jump") and dc:IsGrounded() then
        dc:Jump(MASS * JUMP_SPEED)
    end
end

return Script

Venus Lua API › Physics

FixedRigidBody

Deterministic lockstep rigid body — forces, impulses, materials.

authored lua/venus_lua/src/lua_fixed_rigid_body.rs

FixedRigidBody

A rigid body in the app-owned deterministic FixedXpbdWorld (the lockstep path): box, sphere,
plane, or capsule, driven with conventional rigid-body commands (SetVelocity, AddImpulse,
AddForce, …). Shape and mass are baked once when the body registers (on_enable) — chosen at
attach time via Add / AddBox / AddSphere / AddPlane / AddCapsule, and not mutated live.
Joints between two bodies live on FixedJoint.

Static calls use the type table (FixedRigidBody.AddBox(...)); instance methods use colon syntax
on a handle (rb:AddForce(...)).

Vector arguments come in two flavours. The velocity / force / impulse commands (SetVelocity,
SetAngularVelocity, AddImpulse, AddForce, AddTorque, and the force argument of
AddForceAtPosition) require a native Vector3 — three loose numbers raise a type error. Only the
two world-position overloads, Respawn and the worldPos argument of AddForceAtPosition, accept
either a Vector3 or three loose f64 numbers; the three-number form preserves precision far
from the origin (a Vector3 is f32-backed, ~cm-lossy at range).

Types

FixedRigidBody — an opaque handle (userdata) wrapping a component that holds a generational world
body handle. A handle may be null, stale (its component destroyed), or unregistered
(attached but not yet through on_enable). Every command and query resolves the component and its
world body afresh each call; on a null / stale / unregistered handle, commands no-op and
queries return nil — they never raise. Use IsRegistered() to test readiness.

Creation & Destruction

Each Add* constructor attaches a FixedRigidBody component to gameObject and returns its
handle; the world body is created from the GameObject’s world pose at on_enable, so set the
object’s position before attaching. Returns nil and logs an error if the component cannot be
added (e.g. a null/stale GameObject or an exhausted pool).

FixedRigidBody.Add( GameObject gameObject ) : FixedRigidBody
Attaches a unit-box (half-extent 0.5, mass 1) dynamic body.

FixedRigidBody.AddBox( GameObject gameObject, number hx, number hy, number hz, number mass ) : FixedRigidBody
A box body with half-extents (hx, hy, hz) (all three required). mass is optional (default 1).

FixedRigidBody.AddSphere( GameObject gameObject, number radius, number mass ) : FixedRigidBody
A sphere body of radius (required). mass is optional (default 1).

FixedRigidBody.AddPlane( GameObject gameObject ) : FixedRigidBody
A static infinite ground plane (a half-space; normal = the GameObject’s local +Y). Always
static — it carries no mass and never moves.

FixedRigidBody.AddCapsule( GameObject gameObject, number halfHeight, number radius, number mass ) : FixedRigidBody
A capsule body (axis = local +Y); halfHeight and radius required. mass is optional
(default 1).

FixedRigidBody.Get( GameObject gameObject ) : FixedRigidBody
Returns the FixedRigidBody on gameObject, or nil if it has none. Silent — unlike the
Add* constructors, a miss logs nothing.

FixedRigidBody.Exists( FixedRigidBody body ) : boolean
true if the handle refers to a live component. Safe on any handle (null/stale → false); never
raises.

There is no explicit Destroy. The world body — with its full joint cascade — is torn down
automatically when the GameObject or component is destroyed (on_disable).

Functions

Read-only queries.

rb:Position( ) : Vector3
The body’s current world position. Returns nil if the body is unregistered or the world is
unavailable.

rb:IsRegistered( ) : boolean
true once the world body exists (after on_enable). This is the gate for every command and
query below — before registration they all no-op / return nil.

The two lifecycle getters are the exception: they read the scene component rather than the world
body, so they answer normally on an attached-but-unregistered body.

rb:Active( ) : boolean or nil
The component’s own enabled flag — what Activate()/Deactivate() write. nil (+ a logged
NotFound) on a stale handle. This is intent, not effect: a body whose own flag is enabled but
whose GameObject is deactivated still reports true.

rb:ActiveInHierarchy( ) : boolean or nil
true only when the own flag is enabled and the owning GameObject is active in hierarchy —
i.e. the component is actually running. This is the predicate the engine uses to gate update
dispatch. Returns nil on a stale handle.

Properties (get / set)

Getters return nil on an unregistered / stale handle; setters no-op. Each velocity setter takes a
native Vector3 (not loose numbers).

Velocity

rb:Velocity( ) : Vector3 — current linear velocity, or nil if unregistered.
rb:SetVelocity( Vector3 velocity ) : void

AngularVelocity

rb:AngularVelocity( ) : Vector3 — current angular velocity, or nil if unregistered.
rb:SetAngularVelocity( Vector3 velocity ) : void

Methods

Actions that mutate the body. All no-op on an unregistered / stale handle.

rb:AddImpulse( Vector3 impulse ) : void
An instantaneous linear velocity change (v += j · inv_mass). Takes a native Vector3.

rb:AddForce( Vector3 force, string mode ) : void
Applies a force (Unity Rigidbody.AddForce). force is a native Vector3. mode is the optional
force-mode name"Force" | "Impulse" | "VelocityChange" | "Acceleration" (Unity’s
names; omitted or nil"Force"). Any other value raises a Lua error, including a number:
the names are matched exactly, and there is no ForceMode constants table.

rb:AddTorque( Vector3 torque, string mode ) : void
Applies a world-space torque (Unity Rigidbody.AddTorque). torque is a native Vector3. mode
is the optional force-mode name (see AddForce; default "Force").

rb:AddForceAtPosition( Vector3 force, Vector3 worldPos, string mode ) : void
A force plus its induced torque about the application point worldPos. force is a native
Vector3; worldPos may be a Vector3 or three loose f64 numbers (the three-number form
keeps the point precise far from the origin). mode is the optional force-mode name and simply
follows worldPos in either form.

Fixed 2026-07 (was: silent wrong mode). The mode used to fall back to Force on any
unrecognized value, and the global ForceMode.* integer table — a leftover from a retired
binding — coerced through luaL_checkstring to "1"/"2"/"3" and matched nothing. Every
use of those constants silently applied Force, which is wrong by a factor of the fixed step:
Force delivers F·inv_m·dt spread over the next step, Impulse delivers J·inv_m
immediately — so a jump impulse arrived at roughly 1/120 strength, with no diagnostic. The
table is deleted and unrecognized names now raise. Names resolve through
ForceMode::from_yaml_bytes, generated beside the enum in venus-fixed-dynamics, so the
spellings cannot drift from the variants. Note the lowercase form ("impulse") was accepted-
then-ignored before and is now a hard error — spell it as Unity does.

rb:SetRestitution( number e ) : void
Sets bounciness (restitution).

rb:SetFriction( number mu ) : void
Sets the friction coefficient.

rb:SetMaterial( string name ) : void
Resolves a named physics material from the resource registry
(data/core/defs/physics_materials.yaml) and applies its friction, restitution, and combine modes
to the live body, caching it on the component so it survives respawn / re-enable. An unknown name
(or no registry loaded) logs an error and no-ops.

rb:SetCollisionEvents( boolean on ) : void
Opts into OnCollisionEnter / OnCollisionStay / OnCollisionExit on this body’s GameObject
ScriptBehavior. Persists in the authoring params (clones inherit it).

rb:Respawn( Vector3 position ) : void
Teleports to the given world position with zero velocity and identity orientation — a clean
re-drop that resets simulation state (for spawning/respawning, not in-sim motion). Accepts a
Vector3 or three loose f64 numbers; the three-number form keeps the target precise far from
the origin.

Lifecycle

Both act on the component’s enabled flag rather than the world body, so — unlike the commands above
— they are meaningful before registration. Both silently no-op on a stale handle.

rb:Activate( ) : void
Sets the component’s own enabled flag. Its enable callback runs only if the owning GameObject is
also active — and that callback is what creates the world body.

rb:Deactivate( ) : void
Clears the own enabled flag; the disable callback runs only if the component was actually active.

Example

-- A box prop that drops onto the deterministic ground, reports contacts, and
-- re-drops on a timer. (Adapted from data/scripts/fixed_physics_demo.lua.)
function Script.OnCreate(self)
    -- Pose BEFORE Add: on_enable registers the world body from this position.
    -- Three loose f64 numbers keep the drop precise far from the origin.
    self.object:SetWorldPosition(0.0, 9.0, 0.0)
    self.drop = { 0.0, 9.0, 0.0 }

    -- Unit cube → half-extents 0.5, mass 2.
    self.rb = FixedRigidBody.AddBox(self.object, 0.5, 0.5, 0.5, 2.0)
    self.rb:SetMaterial("wood")
    self.rb:SetCollisionEvents(true)   -- enable the OnCollision* callbacks
    self.timer = 0.0
end

function Script.OnCollisionEnter(self, other)
    -- Kick straight up on first contact. Mode is the quoted NAME string;
    -- there is no constants table, and an unrecognized name raises.
    self.rb:AddForce(Vector3.new(0, 5, 0), "Impulse")
end

function Script.Update(self)
    self.timer = self.timer + Time.DeltaTime()
    if self.timer >= 3.0 then
        self.timer = 0.0
        -- f64 overload: teleport back to the drop point and fall again.
        self.rb:Respawn(self.drop[1], self.drop[2], self.drop[3])
    end
end

Venus Lua API › Physics

FixedJoint

Constraint component on a FixedRigidBody — hinges, sliders, welds, limits, motors.

authored lua/venus_lua/src/lua_fixed_joint.rs

FixedJoint

A FixedJoint is a scenegraph component that constrains the FixedRigidBody
on its own GameObject — either to a static world anchor (the default) or to another
GameObject’s
FixedRigidBody (SetConnected). The constraint is solved in the
deterministic FixedXpbdWorld.

The GameObject must also carry a FixedRigidBody; the joint has nothing to constrain without one.

Static calls use the type table (FixedJoint.Add(...)); instance methods use colon syntax on the
handle (joint:SetRevolute(...)).

Types

FixedJoint — an opaque handle (userdata) to a FixedJoint component, type-id tagged and
generation-checked. The internal FixedJointHandle never crosses the Lua boundary.

The two failure modes differ:

  • Calling an instance method on a value that is not a FixedJoint userdata — including the nil
    a failed Add/Get returns — raises a Lua type error. Guard constructor results before use.
  • A stale but correctly-typed handle is tolerated, and degrades per method: the configuration
    setters and the two lifecycle verbs are silent no-ops; IsRegistered, Active and
    ActiveInHierarchy return nil and log a NotFound warning.

FixedJoint.Exists( joint ) is the safe test — it accepts any value, including nil, and returns
false rather than raising.

tostring(joint) always yields the constant string FixedJoint — it carries no object name and does
not change when the handle goes stale.

Creation & Destruction

FixedJoint.Add( GameObject object ) : FixedJoint or nil
Adds a joint component to object. The default configuration is a ball joint to a world anchor
at the body’s local origin, compliance 0. Reconfigure with the setters below before the first
fixed step. Raises a Lua type error if object is not a GameObject; returns nil and logs
InvalidArgument if the component cannot be added.

FixedJoint.Get( GameObject object ) : FixedJoint or nil
The joint already on object. Returns nil and logs NotFound if there is none.

FixedJoint.Exists( FixedJoint joint ) : boolean
true only if the handle resolves to live component data. Never raises.

There is no joint:Destroy() — a joint is removed by destroying the GameObject or removing the
component, either of which unregisters it from the world.

Registration is deferred — read this before the setters

FixedJoint.Add does not create a world constraint. Creation is deferred to the component’s
fixed_update, deliberately, so that the enable order of the body and the joint cannot matter. The
joint goes live on the first fixed step after its body is registered.

Two consequences that govern every setter on this page:

  1. The authoring setters are pure configuration writes. They take effect at the next
    (re)registration — they do not push to an already-live joint. Configure the joint in the same
    callback that creates it, before any fixed step has run.
  2. To change a live joint, cycle it. Deactivate() unregisters it from the world immediately;
    the next fixed step after Activate() rebuilds it from the current configuration. See
    Reconfiguring a live joint.

joint:IsRegistered() reports whether the constraint is live in the world right now.

Cascade death is self-healing. If either attached body is destroyed, the world’s destroy cascade
takes the joint with it; the component notices on its next fixed step, resets, and will rebuild the
joint if a body is registered again later.

Functions

joint:IsRegistered( ) : boolean or nil
Whether the constraint is currently live in the physics world. false before the first fixed step,
after Deactivate(), or after a cascade death. nil (+ logged NotFound) on a stale handle.

Methods

All setters below are configuration writes that apply at the next (re)registration. Every vector
argument must be a native Vector3 — three loose numbers raise a type error. All silently no-op on
a stale handle.

Kind

Mutually exclusive; the last call wins. Axes are in this body’s local frame and unit length is
expected.

joint:SetBall( ) : void
Point-to-point: the anchor is pinned, rotation about it is free. This is the default.

joint:SetFixed( ) : void
Weld — pins the anchor and locks the relative orientation to the construction pose.

joint:SetRevolute( Vector3 axis ) : void
Hinge about axis, leaving free spin about it.

joint:SetPrismatic( Vector3 axis ) : void
Slider along axis; perpendicular translation and relative rotation are locked.

Geometry & softness

joint:SetAnchor( Vector3 anchor ) : void
The pinned point, in this body’s local frame.

joint:SetCompliance( number compliance ) : void
Constraint softness — 0 (default) is rigid, larger yields. Compliance is 1 / stiffness.

Connection

joint:SetConnected( GameObject other ) : void
Constrain to other’s FixedRigidBody instead of the world — two dynamic bodies (the equivalent of
Unity’s connectedBody). The pin point (this body’s anchor, in world space) is automatically
expressed in the other body’s frame at creation. Raises a Lua type error if other is not a
GameObject.

joint:ClearConnected( ) : void
Revert to a static world anchor.

Limits & motors

Revolute and Prismatic only. On a Ball or Fixed joint these values are stored but never applied
— there is no error and no warning, so check the kind if a limit appears to do nothing.

Units follow the kind: radians / rad·s⁻¹ for Revolute, metres / m·s⁻¹ for Prismatic.

joint:SetLimit( number lo, number hi [, number compliance] ) : void
Clamps the free DOF to [lo, hi]. compliance defaults to 0 — a rigid stop; larger values give a
springy one.

joint:SetMotor( number targetVel, number maxForce ) : void
Velocity motor: drives the DOF rate toward targetVel, with the per-substep drive impulse capped at
±maxForce·h (a torque limit for Revolute, a force limit for Prismatic).

joint:SetPositionMotor( number target [, number compliance] ) : void
Position servo: drives the DOF toward target as a compliant constraint. compliance defaults to
0 (rigid). An alternative to the velocity motor — the last motor call wins. For a hinge, meaningful
for targets within ±π.

Lifecycle

On this component the lifecycle verbs are not merely bookkeeping — they are the supported way to
rebuild a live joint (see below).

joint:Active( ) : boolean or nil
The component’s own enabled flag — what Activate()/Deactivate() write. nil (+ a logged
NotFound) on a stale handle. This is intent, not effect: a joint whose own flag is enabled but
whose GameObject is deactivated still reports true, and is not registered.

joint:ActiveInHierarchy( ) : boolean or nil
true only when the own flag is enabled and the owning GameObject is active in hierarchy — i.e.
the component is actually running its fixed step. This is the predicate the engine uses to gate
update dispatch. nil on a stale handle.

joint:Activate( ) : void
Sets the own enabled flag. The joint re-registers on the next fixed step, using the configuration as
it stands at that moment.

joint:Deactivate( ) : void
Clears the own enabled flag and unregisters the joint from the world immediately (the disable
callback runs if the component was active).

Gaps

  • Ball and Fixed joints have no limit or motor support.
  • There is no query API for a joint’s live angle, slide distance, or reaction force. Track anything
    you need script-side.

Example

-- A hinge between two bodies, limited and motored. Both bodies register during
-- add_component (on_enable is synchronous), and the joint goes live on the first
-- fixed step after that.
function Script.OnCreate(self)
    FixedRigidBody.AddBox(self.object, 0.5, 0.5, 0.5, 1.0)
    FixedRigidBody.AddBox(self.lidObject, 0.5, 0.5, 0.5, 1.0)

    local hinge = FixedJoint.Add(self.object)
    if not hinge then return end            -- Add returns nil on failure; guard it

    hinge:SetConnected(self.lidObject)
    hinge:SetRevolute(Vector3.new(1, 0, 0)) -- hinge axis, this body's local frame
    hinge:SetAnchor(Vector3.new(0, 0.5, 0)) -- pin point, this body's local frame
    hinge:SetLimit(0.0, 1.57)               -- 0..~90 degrees, rigid stops
    hinge:SetMotor(2.0, 50.0)               -- 2 rad/s, torque cap 50

    self.hinge = hinge
end

Reconfiguring a live joint

Setters do not affect an already-registered joint. Cycle the component to rebuild it:

-- Turn the hinge into a slider at runtime.
self.hinge:Deactivate()                        -- unregisters from the world now
self.hinge:SetPrismatic(Vector3.new(0, 1, 0))  -- reconfigure while it is down
self.hinge:SetLimit(0.0, 2.0)                  -- metres, for a prismatic joint
self.hinge:Activate()                          -- rebuilds on the next fixed step

-- IsRegistered() is false until that step runs.

A world-anchored joint is the same pattern with no SetConnectedFixedJoint.Add(go) plus
SetAnchor is a complete, working joint.

Venus Lua API › Animation

AnimationClip

A named animation clip resource — length, frame rate, loop mode.

authored lua/venus_lua/src/lua_anim_clip.rs

AnimationClip

A named animation-clip resource: compressed keyframe data with a length, frame rate, and loop
mode. A clip is never played on its own — it is bound to an AnimState on an
AnimController (via state:SetClip), and the controller samples it into a
skinned mesh.

Static calls use the type table (AnimationClip.Load(...)); instance methods use colon syntax on
a handle (clip:Length()).

Types

AnimationClip — an opaque handle (userdata) to a clip in the clip manager. A handle may be
stale (its clip destroyed); it is safe to keep and pass around. A method on a stale handle
logs an error and returns nil (getters) or is a silent no-op (setters/Destroy) — it does not
raise. Passing a value that is not an AnimationClip userdata where one is expected raises a
Lua type error. tostring(clip) always yields the literal AnimationClip (no name/id).

Creation & Destruction

AnimationClip.Create( string name ) : AnimationClip
Creates an empty clip registered under name. Returns nil and logs an error on a duplicate name
or pool exhaustion (8192 clips max).

AnimationClip.Get( string name ) : AnimationClip
Looks up a clip by name. Returns nil and logs an error if none exists.

AnimationClip.Load( string name, string path ) : AnimationClip
Queues an async load of a binary .anim file (path is relative to the content directory)
registered under name. Returns a valid handle immediately; the file is read and parsed on a
background thread and the data installs on a later frame. Until then the clip samples as identity
and reports Length 0 / FrameCount 0 / FrameRate 30 (the header default) — binding it to
states meanwhile is safe. A read/parse failure surfaces later (logged by the zone pump) and leaves
the clip empty. Returns nil only when the handle itself cannot be created (duplicate name, pool
full).

AnimationClip.Exists( AnimationClip clip ) : boolean
Returns true if the handle refers to a live clip. Safe on stale handles (returns false) and
the one call that never logs; returns false for any non-AnimationClip argument.

clip:Destroy( ) : void
Destroys the clip and reclaims its slot and name. Silent no-op on a stale handle.

Functions

Read-only queries. Each returns nil and logs an error on a stale handle. On a freshly Created
or not-yet-loaded clip the header is at its defaults (FrameRate 30, Length 0, FrameCount 0,
LoopMode "Default").

clip:ID( ) : string
The clip’s unique name.

clip:Length( ) : number
Clip duration in seconds, computed as (keyframe_count − 1) / frameRate. 0 for an empty or
single-key clip, or when the frame rate is 0.

clip:FrameRate( ) : integer
Playback frame rate (frames per second). Defaults to 30 on a clip with no data yet.

clip:FrameCount( ) : integer
Number of playable frames, which is keyframe_count − 1 (a clip stores one extra boundary
key). 0 for an empty clip — not the raw keyframe count.

Properties (get / set)

LoopMode

clip:LoopMode( ) : string — the clip’s loop mode by name: "Default", "Once", "Loop", or
"Clamp".
clip:SetLoopMode( string mode ) : void — takes the same four names the getter returns, so
a:SetLoopMode(b:LoopMode()) round-trips. Matched exactly — no case folding, and numbers are
rejected. An unrecognized value raises a Lua error listing the four spellings. Silent no-op on a
stale handle (a stale handle is not an argument error, so it does not raise).

A clip’s loop mode is only a default: when a state binds the clip, the state inherits it only
if the state’s own loop mode is "Default"
, and the state resolves this once, at
SetClip/SetLoopMode time. "Default" at the clip level means “unspecified” — a bound state
that also leaves its mode at "Default" plays once and stops (the sampler treats a resolved
Default as Once).

Fixed (2026-07). SetLoopMode used to accept a raw integer routed through a total
AnimLoopMode::from_u32 that mapped any value outside 0..=3 to Default, so
clip:SetLoopMode(7) silently set Default with no diagnostic, and no named constants were
exposed for the four values. It now resolves the name through AnimLoopMode::from_yaml_bytes
(generated by yaml_enum! beside the enum, anim_loop_mode.rs:18) and raises on anything it does
not recognize — the same conversion ForceMode got. from_u32 survives for the remaining
integer callers but returns Option now, so it reports the miss instead of swallowing it.

Known issue (verified in source). Changing a clip’s loop mode after a state has bound it
does not propagate. A state copies the clip’s mode into its effective (combined_loop_mode) only
inside state:SetClip / state:SetLoopMode (anim_controller.rs:251, 843); clip:SetLoopMode
mutates only the clip (lua_anim_clip.rs:286). Set the clip’s loop mode before binding it, or
re-bind (state:SetClip(clip)) afterward.

Example

-- Load a run clip and mark it as looping *before* any state binds it.
function Script.OnCreate(self)
    local clip = AnimationClip.Load("dog_run", "animations/dog_run.anim")
    clip:SetLoopMode("Loop")
    self.runClip = clip
    -- Safe even though the async data has not landed yet:
    -- Length()/FrameCount() read 0 until it does.
    print("run frames:", clip:FrameCount())
end

Venus Lua API › Animation

AnimController

Layered animation state machine — states, layers, masks.

authored lua/venus_lua/src/lua_anim_controller.rs

AnimController

A layered animation controller: a set of named AnimStates spread across up to
16 blend layers (indices 0..15), each layer optionally masked (AnimationMask)
and either additive or blended. States bind AnimationClips and are played,
weighted, and cross-faded to drive a skinned mesh.

Static calls use the type table (AnimController.Create(...)); instance methods use colon syntax
on a handle (controller:CreateState(...)).

Types

AnimController — an opaque handle (userdata) to a controller in the controller manager. A handle
may be stale (its controller destroyed); a method on a stale handle logs an error and returns
nil, or is a silent no-op for setters/Destroy — it does not raise. Passing a non-AnimController
userdata where one is expected raises a Lua type error. tostring(controller) always yields the
literal AnimController. CreateState / GetState return AnimState sub-handles — see
AnimState.

Creation & Destruction

AnimController.Create( string name ) : AnimController
Creates a controller registered under name. Returns nil and logs an error on a duplicate name
or pool exhaustion (2048 controllers max).

AnimController.Get( string name ) : AnimController
Looks up a controller by name. Returns nil and logs an error if none exists.

AnimController.Clone( AnimController source ) : AnimController
Deep-clones source (its states, layer masks, additive flags, and time layers) into a new
controller. Cloned states start disabled (call state:Play() to run them). Returns nil and
logs an error if source is stale.

AnimController.Exists( AnimController controller ) : boolean
Returns true if the handle refers to a live controller. Safe on stale handles (returns false,
never logs); false for any non-AnimController argument.

controller:Destroy( ) : void
Destroys the controller (and its states). Silent no-op on a stale handle.

Known issue (verified in source). Clone allocates an unnamed controller
(clone_internal uses pool.alloc(), not alloc_namedanim_controller.rs:717). The clone is
therefore not findable by AnimController.Get, and clone:ID() returns an empty string. Keep
the returned handle yourself; you cannot look a clone up by name.

Functions

controller:ID( ) : string
The controller’s unique name. Returns nil and logs an error on a stale handle. (Empty string for
a controller produced by Clone — see above.)

Methods

Actions on the controller. Each is a silent no-op on a stale handle unless noted. Layer arguments
are 0..15; an out-of-range layer is silently ignored.

controller:CreateState( string name ) : AnimState
Appends a new animation state named name and returns its AnimState sub-handle.
Returns nil and logs an error only if the controller handle is stale — creation itself never
fails on a live controller (no per-controller state cap).

controller:GetState( string name ) : AnimState
Returns the existing state named name (read-only lookup), or nil (logging an error) if no such
state exists or the controller is stale.

controller:SetLayerMask( integer layer, AnimationMask mask ) : void
Assigns a per-bone blend mask to layer (see AnimationMask). Passing nil
clears the layer’s mask. A stale or fabricated mask handle is rejected — it logs an error and
no-ops rather than silently blending unmasked.

controller:SetLayerAdditive( integer layer ) : void
Sets layer to additive blending (the layer’s pose is added on top of lower layers).

controller:SetLayerBlended( integer layer ) : void
Sets layer to normal (weight-normalized override) blending.

controller:StopAll( integer layer ) : void
Stops every state on layer (each is disabled and its time rewound to 0).

Known issue (verified in source). CreateState does not check for a duplicate state name —
it always appends (anim_controller.rs:770). If two states share a name, GetState returns the
first one (find_state_id_by_name returns the first match — anim_controller.rs:422), so the
later duplicate is unreachable by name. Keep state names unique, or retain the handle
CreateState returns.

Example

-- Build a dog controller: a full-body idle plus a masked upper-body layer.
function Script.OnCreate(self)
    local ctrl = AnimController.Create("dog_ctrl")

    local idle = ctrl:CreateState("idle")   -- returns an AnimState
    idle:SetClip(AnimationClip.Get("dog_idle"))
    idle:Play()

    -- Layer 1 blends an upper-body wag additively, restricted by a mask.
    local wag = ctrl:CreateState("tail_wag")
    wag:SetLayer(1)
    wag:SetClip(AnimationClip.Get("dog_wag"))
    ctrl:SetLayerAdditive(1)
    ctrl:SetLayerMask(1, AnimationMask.Get("upper_body"))

    self.ctrl = ctrl
end

Venus Lua API › Animation

AnimState

A single animation state within an AnimController — clip, weight, speed, time.

authored lua/venus_lua/src/lua_anim_controller.rs

AnimState

A single animation state inside an AnimController: it binds an
AnimationClip, plays on a blend layer, and carries a weight, target weight,
speed, playback time, and loop mode. States are cross-faded via their target weights to blend
animations.

All calls use colon syntax on a handle (state:Play()).

Types

AnimState — an opaque handle (userdata); a sub-handle of an AnimController (it stores the
parent controller handle plus the state’s index). It has no own constructor — obtain one only
from AnimController:CreateState( name ) or AnimController:GetState( name ) (see
AnimController). A handle may be stale (its controller destroyed); a
method on a stale handle logs an error and returns nil (getters) or is a silent no-op
(setters/Play/Stop/Pause) — it does not raise. Passing a non-AnimState userdata raises a Lua
type error. tostring(state) always yields the literal AnimState.

A newly created state defaults to: layer 0, disabled, no clip, loop mode "Default", weight
1.0, target weight 1.0, speed 1.0, time 0.

Functions

state:ID( ) : string
The state’s name. Returns nil and logs an error on a stale handle.

Properties (get / set)

Setters return void and are a silent no-op on a stale handle unless noted.

Layer

state:Layer( ) : integer — the blend layer (0..15) this state plays on.
state:SetLayer( integer layer ) : void — moves the state to layer. Moving disables the
state
(call Play() again afterward). An out-of-range layer (≥ 16) is a silent no-op.

Clip

state:Clip( ) : AnimationClip — the bound clip, or nil if none is bound (no log) or the
state is stale (logs an error).
state:SetClip( AnimationClip clip ) : void — binds a clip; passing nil clears it. A
non-null clip handle that does not resolve to a live clip (stale/fabricated) is rejected and
logs an error rather than silently freezing the state.

Enabled

state:Enabled( ) : boolean — whether the state is currently active in its layer.
state:SetEnabled( boolean enabled ) : void — enables/disables without touching time (unlike
Stop, which also rewinds).

LoopMode

state:LoopMode( ) : string — the state’s own loop-mode override by name: "Default",
"Once", "Loop", or "Clamp". "Default" means “defer to the bound clip’s loop mode”.
state:SetLoopMode( string mode ) : void — sets the override and re-resolves the effective mode
from the bound clip. Takes the same four names the getter returns, so
a:SetLoopMode(b:LoopMode()) round-trips. Matched exactly — no case folding, and numbers are
rejected. An unrecognized value raises a Lua error listing the four spellings.

Weight

state:Weight( ) : number — the current blend weight (typically 0..1).
state:SetWeight( number weight ) : void — sets the weight directly (snaps). This also sets
TargetWeight
to the same value, so it holds instead of blending away on the next frame.

TargetWeight

state:TargetWeight( ) : number — the weight the state is blending toward.
state:SetTargetWeight( number weight ) : void — sets only the target; the weight eases toward
it each frame (this is the cross-fade knob).

Speed

state:Speed( ) : number — the playback speed multiplier (unitless; 1.0 = normal).
state:SetSpeed( number speed ) : void — a negative speed plays the clip backward.

Time

state:Time( ) : number — the current playback time, in seconds.
state:SetTime( number time ) : void — sets the play-head directly (wrapping/clamping to the
clip length happens during sampling, not at set time).

Methods

state:Play( ) : void
Enables the state; playback resumes from the current time (time is left as-is, so a paused
state continues where it stopped).

state:Stop( ) : void
Disables the state and rewinds its time to 0.

state:Pause( ) : void
Disables the state but keeps the current time (resume with Play).

Known issue (verified in source). LoopMode() returns the state’s own override
(self.loop_mode, anim_controller.rs:148), not the effective mode actually used for playback
(combined_loop_mode, resolved from state + clip in resolve_loop_mode,
anim_controller.rs:251). So a state left at "Default" that plays a looping clip still reports
"Default", and the effective mode is not readable from Lua. Note also that a resolved Default
(neither the state nor the clip set a mode) is treated as Once by the sampler
(increment_time, anim_controller.rs:282) — set SetLoopMode("Loop") explicitly if you want
looping.

Known issue (verified in source). SetLoopMode is a silent no-op if the state’s bound clip
has gone stale (state_set_loop_mode returns an error that the binding ignores —
anim_controller.rs:876); clear the clip first if you need it to take effect. The other half of
this note is fixed as of 2026-07: the mode used to be a raw integer routed through a total
AnimLoopMode::from_u32 that silently mapped anything outside 0..=3 to Default, with no named
constants exposed. It is now a name resolved through AnimLoopMode::from_yaml_bytes
(anim_loop_mode.rs:18), and an unrecognized name raises.

Example

-- Cross-fade between idle and run using target weights (adapted from the dog
-- animation setup). SetTargetWeight fades; the controller's per-frame sample
-- eases each weight toward its target.
function Script.Update(self)
    local run  = self.ctrl:GetState("run")
    local idle = self.ctrl:GetState("idle")
    run:SetTargetWeight(self.moving and 1.0 or 0.0)
    idle:SetTargetWeight(self.moving and 0.0 or 1.0)
    run:SetLoopMode("Loop")   -- "Default" would play once
    run:Play()
    idle:Play()
end

Venus Lua API › Animation

AnimationMask

Per-bone blend weights for animation layers.

authored lua/venus_lua/src/lua_anim_mask.rs

AnimationMask

A per-bone weight table used to mask an animation blend layer: each bone gets a weight in [0, 1]
(0 = excluded, 1 = fully included), so a layer can affect only part of the skeleton (e.g. an
upper-body action over a full-body locomotion base). Assign a mask to a controller layer with
AnimController:SetLayerMask (see AnimController).

A mask holds one weight per bone, up to 128 bones (MAX_BONE_CURVES). Bone indices are
0-based and match the skeleton’s bone order — they are not 1-based Lua indices.

Static calls use the type table (AnimationMask.Create(...)); instance methods use colon syntax on
a handle (mask:SetValue(...)).

Types

AnimationMask — an opaque handle (userdata) to a mask in the mask manager. A handle may be
stale (its mask destroyed); a method on a stale handle logs an error and returns nil
(getters) or is a silent no-op (setters/Destroy) — it does not raise. Passing a non-AnimationMask
userdata where one is expected raises a Lua type error. tostring(mask) always yields the literal
AnimationMask.

Creation & Destruction

AnimationMask.Create( string name ) : AnimationMask
Creates a mask registered under name, with every bone weight initialized to 0.0 (i.e. the
whole skeleton excluded — set the bones you want to 1.0). Returns nil and logs an error on a
duplicate name or pool exhaustion (1024 masks max).

AnimationMask.Get( string name ) : AnimationMask
Looks up a mask by name. Returns nil and logs an error if none exists.

AnimationMask.Exists( AnimationMask mask ) : boolean
Returns true if the handle refers to a live mask. Safe on stale handles (returns false, never
logs); false for any non-AnimationMask argument.

mask:Destroy( ) : void
Destroys the mask. Silent no-op on a stale handle.

Functions

mask:ID( ) : string
The mask’s unique name. Returns nil and logs an error on a stale handle.

Methods

Silent no-op on a stale handle.

mask:SetValue( integer index, number value ) : void
Sets the blend weight for bone index (0-based). value is clamped to [0, 1] (0 =
excluded, 1 = fully included; intermediate values blend partially). An out-of-range index
(≥ 128) is silently ignored.

mask:SetAllValues( number value ) : void
Sets every bone’s weight to value (clamped to [0, 1]). Useful to reset a mask to all-excluded
(0) or all-included (1) before poking individual bones.

Example

-- Build an upper-body mask and assign it to layer 1 of a controller.
function Script.OnCreate(self)
    local mask = AnimationMask.Create("upper_body")
    -- A new mask is already all-zero; this line is just explicit intent.
    mask:SetAllValues(0.0)
    mask:SetValue(3, 1.0)   -- spine   (0-based bone index)
    mask:SetValue(5, 1.0)   -- left arm
    self.ctrl:SetLayerMask(1, mask)
end

Venus Lua API › Audio

AudioSource

Per-object sound emitter — clip playback and channel parameters.

authored lua/venus_lua/src/lua_audio_source.rs

AudioSource

A component that plays an AudioClip from a GameObject’s world position, driving a
mixer channel with per-source parameters (volume, pitch, pan, spatialization, spread, min
distance, priority, category). Obtain one from a GameObject with AudioSource.Get; instance
methods use colon syntax (source:Play()).

Audible output requires the engine to be built with the fmod feature. Without it, every playback
call still succeeds (no error), but nothing is heard and IsPlaying stays false — the audio
engine’s playback methods are no-ops when no backend is present.

Types

AudioSource — an opaque handle (userdata) to an audio-source component. Unlike a
GameObject handle, the instance methods raise a Lua type error
(luaL_typeerror) if the receiver is nil, a non-userdata value, or a foreign userdata (the
shared handle check gates on a type-id byte and rejects mismatches). A stale handle — a real
AudioSource whose component was destroyed — is tolerated: getters log a warning and return a
default (0 / nil), setters and actions no-op silently. AudioSource.Exists is the exception —
it never raises and never warns. tostring(source) always yields the constant string
AudioSource (no name or index, even when stale).

Creation & Destruction

AudioSource.Get( GameObject gameObject ) : AudioSource
Returns the AudioSource component attached to gameObject, or nil (logs a warning) if it has
none.

AudioSource.Exists( AudioSource source ) : boolean
Returns true if the handle refers to a live component. Safe on null / stale / foreign userdata
(returns false, never warns).

Known issue (no Lua constructor). AUDIO_SOURCE_LIB exposes only Get and Exists
(lua_audio_source.rs:605) — there is no Add and no Destroy. Unlike
Light and Collider, which register a Add, an AudioSource cannot be
attached to or removed from a GameObject through Lua. Attach it via scene / YAML data (or Rust);
Get only retrieves one that already exists.

Functions

source:IsPlaying( ) : boolean
true if the source currently holds a live, playing channel. Returns false (never raises) when
there is no channel, the backend is absent, or the handle is stale.

source:Active( ) : boolean or nil
The component’s own enabled flag — what Activate()/Deactivate() write. Logs a warning
(NotFound) and returns nil on a stale handle. This is intent, not effect: a source whose own
flag is enabled but whose GameObject is deactivated still reports true.

source:ActiveInHierarchy( ) : boolean or nil
true only when the own flag is enabled and the owning GameObject is active in hierarchy —
i.e. the component is actually running. This is the predicate the engine uses to gate update
dispatch. Returns nil on a stale handle.

Properties (get / set)

Getters return the stored value; on a stale handle they log a warning and return the listed
default. Setters return void, write the field, and propagate to the live channel if one is
playing; they no-op silently on a stale handle. Ranges below are the component’s documented
defaults/intent (engine/scenegraph/src/audio_source.rs:49) — the per-source setters do not
clamp, so out-of-range values are passed straight to the mixer.

Clip

source:Clip( ) : AudioClip — the assigned clip, or nil if none (or stale).
source:SetClip( AudioClip clip ) : void — assigns the clip; pass nil to clear. Stops any
active playback first.

Volume

source:Volume( ) : number — linear gain, nominal [0, 1], default 1.0.
source:SetVolume( number volume ) : void — the live channel’s effective volume is this value
times the master and per-category volumes; not clamped here.

Pitch

source:Pitch( ) : number — playback-rate multiplier, default 1.0 (0.5 = one octave down,
2.0 = one octave up).
source:SetPitch( number pitch ) : void

Priority

source:Priority( ) : integer — voice-stealing priority [0, 256], lower = higher
priority
, default 128.
source:SetPriority( integer priority ) : void

Pan

source:Pan( ) : number — 2D stereo pan, -1.0 (left) .. 1.0 (right), default 0.0.
source:SetPan( number pan ) : void

Spatialization

source:Spatialization( ) : number — 2D↔3D blend, 0.0 = pure 2D, 1.0 = fully 3D,
default 1.0.
source:SetSpatialization( number level ) : void

Spread

source:Spread( ) : number — 3D speaker spread angle in degrees (0..360), default
0.0.
source:SetSpread( number angle ) : void

MinDistance

source:MinDistance( ) : number — 3D distance below which the sound stays at full volume,
default 1.0.
source:SetMinDistance( number distance ) : void

PlayOnAwake

source:PlayOnAwake( ) : boolean — whether the source auto-plays when its GameObject is
enabled, default false.
source:SetPlayOnAwake( boolean value ) : void — takes effect on the next enable cycle.

Category

source:Category( ) : string — the mixer category by name: "SFX", "Music", or "Voice"
(default "SFX"). See Constants and the known issue below.
source:SetCategory( string category ) : void — takes the same three names the getter returns,
so a:SetCategory(b:Category()) round-trips. Matched exactly — no case folding, and numbers are
rejected. An unrecognized value raises a Lua error listing the three spellings.

Methods

source:Play( ) : void
Stops any current playback, then plays the assigned clip from the source’s world position, applying
every configured channel parameter (volume, pitch, position, then pan, priority,
spatialization, spread, min distance). No-op if there is no clip, no backend, or the handle is
stale.

source:Stop( ) : void
Stops playback and releases the channel.

source:Pause( ) : void
Pauses the active channel (no-op if not playing or no backend).

source:Unpause( ) : void
Resumes a paused channel.

source:Activate( ) : void
Sets the component’s own enabled flag. Its enable callback runs only if the owning GameObject is
also active. Silently no-ops on a stale handle.

source: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.

Constants

The mixer category is a name, not a constant. Pass the string literally — "SFX", "Music",
or "Voice" — matched exactly, with no case folding.

Fixed (2026-07). Two defects here were closed in the same pass that documented them.
(1) A sub-table of the old mixer-category integers (AudioSource.Category.SFX = 0, etc.)
outlived the conversion by a commit. Since SetCategory rejects numbers, the idiomatic-looking
source:SetCategory(AudioSource.Category.SFX) resolved to 0 and raised — the broken call
was the one this page used to show. The table is deleted, matching Light, Lighting, UI, and
RenderTexture; AudioSource.Category is now nil. Pinned by
lua_audio_source::category_tests::category_integer_subtable_is_removed.
(2) The stale-handle path of Category() pushed the integer 0 while every live path pushed
a name, so source:Category() == "SFX" was false on a stale handle and the return type
depended on liveness. It now pushes "SFX" — the type-correct analogue of this file’s
convention (Volume0.0, Priority0).

Known issue (category routing lag). Changing the category on an already-playing source does
not immediately re-route its live channel: set_category only stores the field
(engine/scenegraph/src/audio_source.rs:122). The new routing is picked up the next time the
volume FFI fires (SetVolume) or on the next Play.

Example

-- A footstep emitter. The AudioSource is attached in scene data; Lua only
-- configures and triggers it. (Get returns nil if the component is missing,
-- so guard before use.)
function Script.OnEnable(self)
    self.source = AudioSource.Get(self.object)
    if self.source then
        self.source:SetCategory("SFX")
        self.source:SetVolume(0.8)
        self.source:SetSpatialization(1.0)   -- fully 3D-positioned
        self.source:SetMinDistance(2.0)
    end
end

-- Called by game logic (e.g. an animation event), not a built-in callback.
function Script.OnFootstep(self)
    if self.source and not self.source:IsPlaying() then
        self.source:Play()
    end
end

Venus Lua API › Audio

AudioClip

A named sound asset — load audio data, query duration and import flags.

authored lua/venus_lua/src/lua_audio_clip.rs

AudioClip

A named handle to a sound asset in the audio clip manager. An AudioSource
references a clip to play it. Clips are looked up or created by unique name, then loaded from a
file path; the 2D/loop import flags control how the data is decoded.

Actual decoding requires the engine to be built with the fmod feature. Without it, Load
succeeds but stores no audio data (and Length stays 0).

Types

AudioClip — an opaque handle (userdata) to a clip in the manager. The instance methods raise
a Lua type error (luaL_typeerror) if the receiver is nil, a non-userdata value, or a foreign
userdata. A stale handle (its clip destroyed) is tolerated: every query logs a warning and
returns a default (0 / false / nil). AudioClip.Exists never raises and never warns.
tostring(clip) yields AudioClip{name}, or AudioClip{NULL} if the handle is stale.

Creation & Destruction

AudioClip.Get( string name ) : AudioClip
Finds a clip by its unique name. Returns nil and logs a warning if no match exists.

AudioClip.Create( string name ) : AudioClip
Creates a new, empty clip registered under name. Returns nil and logs an error if creation
fails — including when a clip with that name already exists (create_resource returns
AlreadyExists, audio_clip_manager.rs:33) or the clip pool is exhausted. Use Get to fetch an
existing one.

AudioClip.Exists( AudioClip clip ) : boolean
Returns true if the handle refers to a live clip. Safe on null / stale / foreign userdata
(returns false, never warns).

Known issue (no Lua destroy). AUDIO_CLIP_MEMBERS_LIB has no Destroy
(lua_audio_clip.rs:230). The engine can destroy and recycle a clip slot
(AudioEngine::destroy_clip), but that is not bound to Lua: a script can Unload a clip’s audio
data, yet the handle and its name stay registered for the session. Reusing the name later fails
the Create duplicate-name check above.

Functions

clip:ID( ) : string
The clip’s unique name. Returns nil (logs a warning) on a stale handle.

clip:Length( ) : number
Duration in seconds. Returns 0 if the clip is not loaded — and, because the duration is filled in
by the FMOD backend at load time (audio_engine.rs:171), it stays 0 in builds without the
fmod feature even after a successful Load.

clip:Is2D( ) : boolean
The 2D import flag (true = non-spatialized). Default true.

clip:IsLoop( ) : boolean
The loop import flag. Default false.

Methods

clip:Load( string path ) : void
Loads audio data for the clip from the file at path, decoding per the current 2D/loop import
flags. Logs a warning (InvalidArgument) on failure — including calling Load on a clip that is
already loaded: the clip must be Unloaded first (audio_engine.rs:156).

clip:Unload( ) : void
Releases the clip’s loaded audio data, returning it to the empty state (so it can be Loaded
again). Logs a warning if the handle is stale.

clip:SetImportAs2D( boolean value ) : void
Sets the 2D import flag (see Is2D). The flag is consumed by Load, so set it before loading;
changing it on an already-loaded clip has no effect until the next Unload + Load.

clip:SetImportAsLoop( boolean value ) : void
Sets the loop import flag (see IsLoop). Same load-time ordering as SetImportAs2D.

Example

-- Create and load a spatialized clip once, then hand it to a source.
local clip = AudioClip.Get("footstep") or AudioClip.Create("footstep")
if clip then
    clip:SetImportAs2D(false)   -- 3D playback — set before Load
    clip:Load("audio/footstep.wav")
end

function Script.OnEnable(self)
    local source = AudioSource.Get(self.object)
    if source and clip then
        source:SetClip(clip)
    end
end

Venus Lua API › Audio

AudioBuffer

Streaming PCM buffer — push float samples for real-time playback.

authored lua/venus_lua/src/lua_audio_buffer.rs

AudioBuffer

A streaming audio buffer: a lock-free ring of PCM f32 samples that Lua fills over time and the
mixer drains. Use it for procedurally generated or externally streamed audio. Create one with
AudioBuffer.Create; instance methods use colon syntax (buf:WriteSamples(...)).

Playback (Play / Stop) requires the engine to be built with the fmod feature. Without it,
Create, WriteSamples, and AvailableWrite still work against the ring buffer, but Play and
Stop are no-ops. At most MAX_AUDIO_STREAMS (16) streams can exist at once.

Types

AudioBuffer — an opaque handle (userdata) to a streaming buffer. The instance methods raise a
Lua type error (luaL_typeerror) if the receiver is nil, a non-userdata value, or a foreign
userdata. A stale handle (its stream destroyed) is tolerated: WriteSamples returns the count
written so far, the other queries log a warning and return 0, and Destroy is a silent no-op.
tostring(buf) yields AudioBuffer{index} (the pool slot index, 1..16).

The buffer owns an underlying stream that is freed automatically when the handle is
garbage-collected
— its __gc metamethod calls destroy_stream (lua_audio_buffer.rs:179), so
dropping the last Lua reference releases the ring and stops playback. You do not have to call
Destroy, but you must keep a reference for as long as you want the buffer alive.

Gotcha (GC frees the stream). Because __gc destroys the stream, a buffer held only in a
local that goes out of scope can be collected mid-playback, silently stopping the sound and
freeing its ring. Store the handle somewhere durable (e.g. on self) for the lifetime of the
audio.

Creation & Destruction

AudioBuffer.Create( integer sampleRate, integer channels, integer bufferSize ) : AudioBuffer
Creates a streaming buffer. All three arguments are integers (a non-integer raises a standard Lua
argument error). bufferSize is a sample count (individual f32 slots — for multi-channel
audio, samples are interleaved), and is rounded up to the next power of two internally
(SpscRingBuffer::newnext_power_of_two(), spsc_ring_buffer.rs:39). Returns nil and logs an
error on failure: bufferSize of 0, or exceeding the 16-stream limit
(StreamLimitReached).

buf:Destroy( ) : void
Destroys the underlying stream now (stops playback, frees the ring). Safe on a stale handle — the
generational pool rejects it. Also happens automatically on garbage collection.

Functions

buf:AvailableWrite( ) : integer
Number of samples the ring can accept right now before it is full. Immediately after Create this
equals the rounded (power-of-two) capacity. Returns 0 (logs a warning) on a stale handle.

Methods

buf:WriteSamples( table samples ) : integer
Writes float samples from a 1-based array into the ring and returns the count actually written. The
whole table is consumed when the ring has room — it is drained in internal 8192-sample batches
through a fixed 32 KiB stack buffer, so there is no per-call cap (the 8192 is not a limit). A result
below #samples means the ring filled up (back-pressure) — call again (e.g. next frame) to write
the remainder. An empty table returns 0.

Known issue (silent coercion). Each element is read with lua_tonumberx
(lua_audio_buffer.rs:82): a non-numeric table entry (string, nil, table) is silently written
as 0.0 rather than rejected. Ensure the array holds only numbers.

buf:Play( number volume ) : void
Starts playback of the stream. volume is optional and defaults to 1.0. The volume is applied
raw to the channel — unlike AudioSource, a stream is not scaled by the
master/category mixer volumes (audio_engine.rs:619). Logs a warning on a stale handle.

buf:Stop( ) : void
Stops playback of the stream. Logs a warning on a stale handle.

Example

-- Procedurally stream a sine tone. `self.buf` keeps the handle alive so GC
-- doesn't collect (and silence) the stream between frames.
function Script.OnEnable(self)
    self.buf = AudioBuffer.Create(44100, 1, 4096)   -- rounded to 4096 (already a pow2)
    self.phase = 0.0
    if self.buf then
        self.buf:Play(0.5)
    end
end

function Script.Update(self)
    if not self.buf then return end
    -- Fill exactly the room the ring reports; a full ring writes nothing.
    local room = self.buf:AvailableWrite()
    if room > 0 then
        local samples = {}
        for i = 1, room do
            self.phase = self.phase + 0.05
            samples[i] = math.sin(self.phase) * 0.5
        end
        self.buf:WriteSamples(samples)
    end
end

Venus Lua API › UI

UI / UIObject

2D UI node — hierarchy, pixel/relative transform, layout, interaction, animation.

authored lua/venus_lua/src/lua_ui.rs

UI / UIObject

A node in the 2D UI scene graph: a transform expressed in both absolute pixels and
parent-relative fractions, an anchor, a place in the parent → child hierarchy, a lifecycle
(active / inactive / destroyed), responder/interaction state, and a native per-property animation
system. Components — UIText, UITexture, UIButton — attach to a UIObject and render inside its
computed rectangle.

Types

The global is named UI, but the handle it hands back is the metatable type UIObject.
You create and fetch through the global (UI.Create(), UI.Get(name) — dot calls), and you call
instance methods on the returned handle with colon syntax (uiObject:SetPixelSize(...)). There is
no global named UIObject; the name only appears as the userdata’s type and in tostring.

UIObject — an opaque handle (userdata) to a UI node. A handle may be null or stale (its
object destroyed); it is safe to keep and pass around. Calling a method on a null/stale handle
returns nil (getters) or warns and no-ops (mutators) — it never raises. Passing a value that is
not a UIObject at all (a number, a foreign handle) does raise a type error.
tostring(uiObject) yields the object’s ID (its name), or the literal "UIObject" if the handle is
dead or unnamed.

Screen space is Y-down (origin top-left): +x is right, +y is down. All pixel positions,
sizes, and readbacks use this convention. Positions and sizes come in two flavors that add together:
pixel units (absolute px) and relative units (a fraction of the parent’s unscaled size).
Rotations are in radians.

Creation & Destruction

UI.Create( ) : UIObject
Creates a new, active UIObject as a UI root at the origin. Returns nil and logs an error if the UI
scene pool is exhausted. New objects are recorded in the ambient zone’s spawn ledger, so they unload
with the zone.

UI.CreateInactive( ) : UIObject
Same as Create, but the object starts disabled (its components do not run until activated).

UI.Instantiate( UIObject source ) : UIObject
Recursively clones source and its children. source must be a UIObject handle (raises otherwise);
returns nil and logs a warning if the clone fails.

UI.Get( string name ) : UIObject
Finds a UIObject by its unique name (ID). Returns nil and logs a warning if no match exists.

UI.Exists( UIObject object ) : boolean
Returns true if the handle refers to a live object. Safe on any value — a null/stale handle or a
non-UIObject returns false — and it never warns.

uiObject:Destroy( ) : void
Destroys the object (and its children). Destruction is deferred to the next safe point, so calling
it on self from a callback is safe.

Functions

Read-only queries. Every getter returns nil and (for the delegated ones) logs a warning on a
null/stale handle unless noted.

Hierarchy

Same shape as GameObject — the tree is navigated identically.

uiObject:Active( ) : boolean
The object’s own enabled flag, ignoring ancestors.

uiObject:ActiveInHierarchy( ) : boolean
true only if this object and every ancestor are active.

uiObject:Parent( ) : UIObject
The parent, or nil if the object is a UI root (a root is not an error — no warning).

uiObject:Root( ) : UIObject
The topmost ancestor (itself if already a root).

uiObject:Children( ) : table
An array (1-based) of the direct children.

uiObject:ChildCount( ) : integer
Number of direct children (returns 0 on a dead handle).

uiObject:GetChild( integer index ) : UIObject
The child at index (1-based); nil if out of range.

uiObject:FindChild( string name ) : UIObject
Search for a descendant named name; nil if none.

uiObject:IsChildOf( UIObject parent ) : boolean
true if parent is this object’s ancestor (returns false on a dead handle).

uiObject:IsRoot( ) : boolean
true if the object has no parent.

uiObject:FirstChild( ) : UIObject
First direct child, or nil.

uiObject:NextSibling( ) : UIObject
Next sibling under the shared parent, or nil.

uiObject:PreviousSibling( ) : UIObject
Previous sibling, or nil.

World-space readbacks

The final values after the layout, anchor, and hierarchy passes resolve. Read-only — there are no
setters (you set the local inputs above; the engine computes these each frame).

uiObject:WorldPosition( ) : Vector2
Final computed screen position, in pixels (Y-down from top-left).

uiObject:WorldSize( ) : Vector2
Final computed size in pixels (unscaled_size × world scale).

uiObject:WorldRotation( ) : number
Accumulated world rotation, in radians.

uiObject:WorldScale( ) : number
Accumulated world scale.

uiObject:WorldOpacity( ) : number
Accumulated world opacity, [0, 1].

uiObject:WorldDepth( ) : integer
Accumulated world depth (sort key).

uiObject:LayoutOffset( ) : Vector2
The offset applied to this child by the parent’s auto-layout pass; zero when the parent has no
layout. Read-only (recomputed each frame by the layout post-pass).

Responder state

Engine-driven interaction state, updated as the pointer moves. Read-only.

uiObject:HoverTime( ) : number
Seconds the pointer has been hovering this object (0 if not hovered).

uiObject:PressTime( ) : number
Seconds since the current press began.

uiObject:DownPosition( ) : Vector2
Screen position where the active press started.

uiObject:PressDelta( ) : Vector2
Pointer movement since the press started.

Properties (get / set)

Paired accessors. Setters return void and no-op with a warning on a null/stale handle. Setters
that take a position or size accept a Vector2 or two numbers.

ID

uiObject:ID( ) : string — the object’s unique name.
uiObject:SetID( string name ) : void — renames the object.

PixelPosition

uiObject:PixelPosition( ) : Vector2 — pixel offset from the anchor-derived local position.
uiObject:SetPixelPosition( Vector2 position ) : void

RelativePosition

uiObject:RelativePosition( ) : Vector2 — offset as a fraction of the parent’s unscaled size.
uiObject:SetRelativePosition( Vector2 position ) : void

PixelSize

uiObject:PixelSize( ) : Vector2 — absolute pixel contribution to size.
uiObject:SetPixelSize( Vector2 size ) : void

RelativeSize

uiObject:RelativeSize( ) : Vector2 — size as a fraction of the parent’s unscaled size.
uiObject:SetRelativeSize( Vector2 size ) : void

Anchor

uiObject:Anchor( ) : string — the anchor setting, by name (e.g. "MiddleCenter").
uiObject:SetAnchor( string anchor ) : void — one of the nine anchor names (see
Enum names). An unrecognized name raises.

Rotation

uiObject:Rotation( ) : number — local rotation, in radians.
uiObject:SetRotation( number radians ) : void

LocalScale

uiObject:LocalScale( ) : number — uniform local scale (default 1).
uiObject:SetLocalScale( number scale ) : void

Opacity

uiObject:Opacity( ) : number — local opacity, [0, 1] (default 1).
uiObject:SetOpacity( number opacity ) : void

Depth

uiObject:Depth( ) : integer — local depth (a sort key; combined up the hierarchy into
WorldDepth).
uiObject:SetDepth( integer depth ) : void

Padding

uiObject:Padding( ) : number, number, number, number — inner content inset
left, top, right, bottom. Children’s relative position/size reference this padded rect.
uiObject:SetPadding( number left, number top, number right, number bottom ) : void
uiObject:SetPaddingUniform( number value ) : void — convenience: sets all four sides to
value (no matching getter — read back with Padding).

Margin

uiObject:Margin( ) : number, number, number, number — outer spacing
left, top, right, bottom consumed by the parent’s layout pass.
uiObject:SetMargin( number left, number top, number right, number bottom ) : void

LayoutMode

uiObject:LayoutMode( ) : string — auto-layout mode for this container’s direct children, by
name: "None", "Vertical", or "Horizontal".
uiObject:SetLayoutMode( string mode ) : void — an unrecognized name raises.

LayoutSpacing

uiObject:LayoutSpacing( ) : number — gap between children in the layout.
uiObject:SetLayoutSpacing( number spacing ) : void

LayoutAlignment

uiObject:LayoutAlignment( ) : string — child alignment along the layout axis, by name:
"Center", "Start", or "End".
uiObject:SetLayoutAlignment( string alignment ) : void — an unrecognized name raises.

Layer

uiObject:Layer( ) : integer — the UI layer index.
uiObject:SetLayer( integer layer ) : void

PseudoState

uiObject:PseudoState( ) : string — the responder pseudo-state used for styling, by name:
"None", "NonInteractive", "Hover", "Activated", "Held", "Released", "Focus".
uiObject:SetPseudoState( string state ) : void — an unrecognized name raises. Getter and
setter use the same spellings, so a:SetPseudoState(b:PseudoState()) round-trips.

Interactive

uiObject:Interactive( ) : booleantrue only for the three responding types
("Button", "Drag", "TextField"). The three hit-testing-only types — "HitTarget", "Block",
"Pass" — report false here even though they do take part in hit-testing.
uiObject:SetInteractive( string responseType ) : void — sets the responder response type by
name (see Enum names); an unrecognized name raises. Asymmetric: the getter
returns a boolean, so unlike the other enum properties this pair does not round-trip — there is no
getter that returns the response-type name.

ScriptTarget

uiObject:GetScriptTarget( ) : string — the name of the script object that receives this
element’s UI callbacks, or nil if unset.
uiObject:SetScriptTarget( string name ) : void

Methods

Actions that mutate the object. All no-op with a warning on a null/stale handle.

uiObject:Activate( ) : void
Enables the object and recursively updates children.

uiObject:Deactivate( ) : void
Disables the object and recursively updates children.

uiObject:SetParent( UIObject parent ) : void
Reparents the object under parent. parent must be a UIObject handle — unlike
GameObject:SetParent, there is no string-name overload.

uiObject:MakeRoot( ) : void
Detaches from the current parent, making the object a UI root. Re-homes zone ownership so the fresh
root unloads with the ambient zone instead of leaking.

uiObject:DetachChildren( ) : void
Unparents every direct child, making each a root.

Property animation

Every animatable property has a tripleAnimate<P>, Cancel<P>Anim, Is<P>Animating — plus
one global CancelAllAnimations(). Animate<P> tweens the property to a target over duration
seconds using an interp easing-curve name (see Enum names); the optional
smoothTime (default 0) adds critically-damped smoothing on top. interp is required and an
unrecognized name raises. Vec2-valued targets accept a Vector2 or two numbers; scalar targets take
one number.

  • Vec2: uiObject:Animate<P>( Vector2 target, number duration, string interp, number smoothTime = 0 ) : void
  • Scalar: uiObject:Animate<P>( number target, number duration, string interp, number smoothTime = 0 ) : void
  • uiObject:Cancel<P>Anim( ) : void — stop the animation on that property, leaving it at its current value.
  • uiObject:Is<P>Animating( ) : boolean — whether that property is currently animating.
Property Value Animate Cancel Query
PixelPosition Vector2 AnimatePixelPosition CancelPixelPositionAnim IsPixelPositionAnimating
RelativePosition Vector2 AnimateRelativePosition CancelRelativePositionAnim IsRelativePositionAnimating
PixelSize Vector2 AnimatePixelSize CancelPixelSizeAnim IsPixelSizeAnimating
RelativeSize Vector2 AnimateRelativeSize CancelRelativeSizeAnim IsRelativeSizeAnimating
Opacity number AnimateOpacity CancelOpacityAnim IsOpacityAnimating
Rotation number AnimateRotation CancelRotationAnim IsRotationAnimating
Scale number AnimateScale CancelScaleAnim IsScaleAnimating

uiObject:CancelAllAnimations( ) : void
Cancels every active property animation on the object at once.

Enum names

Every enum argument above is a string, matched exactly against the spellings below —
one spelling each, no case folding and no aliases ("vertical" and "VERTICAL" are both rejected;
only "Vertical" is accepted). An unrecognized value raises a Lua error listing the valid
spellings, and numbers are rejected the same way. The getters return these same strings, so
a:SetAnchor(b:Anchor()) round-trips.

The UI.* integer constants were deleted (2026-07) — all 39 of UI.LAYOUT_*, UI.ALIGN_*,
UI.ANCHOR_*, UI.INTERP_*, and UI.RESPONSE_*. They are simply absent from the UI table
now, so a stale call like panel:SetAnchor(UI.ANCHOR_MIDDLE_CENTER) passes nil and raises.
They were not reinstated as string constants on purpose: a mistyped table field
(UI.ANCHOR_LOWER_RIGTH) is nil and indistinguishable from an omitted argument, while a
present-but-wrong string can always be rejected by name.

Layout mode (SetLayoutMode / LayoutMode): "None", "Vertical", "Horizontal".

Layout alignment (SetLayoutAlignment / LayoutAlignment): "Center", "Start", "End".

Anchor (SetAnchor / Anchor) — top-to-bottom / left-to-right:

  Left Center Right
Upper "UpperLeft" "UpperCenter" "UpperRight"
Middle "MiddleLeft" "MiddleCenter" "MiddleRight"
Lower "LowerLeft" "LowerCenter" "LowerRight"

Pseudo-state (SetPseudoState / PseudoState): "None", "NonInteractive", "Hover",
"Activated", "Held", "Released", "Focus".

Response type (SetInteractive):

Name Behavior
"None" Not interactive; does not participate in hit-testing.
"Button" Responds to press/release.
"Drag" Responds to drag (movement while pressed).
"TextField" Editable text field (focuses on click, receives text events).
"HitTarget" Hit-tests; propagates the hit up to the first interactive ancestor.
"Block" Hit-tests; absorbs the hit with no response or propagation.
"Pass" Hit-tests; masks lower-depth objects but does not consume input.

Interpolation (the interp argument of every Animate* call):

Name   Name   Name
"None"   "CubicIn"   "QuinticIn"
"Linear"   "CubicOut"   "QuinticOut"
"QuadraticIn"   "CubicInOut"   "QuinticInOut"
"QuadraticOut"   "QuarticIn"   "SinIn"
"QuadraticInOut"   "QuarticOut"   "SinOut"
    "QuarticInOut"   "SinInOut"

Example

-- Build a full-screen panel, then fade + slide it in.
-- (Adapted from data/scripts/ui_setup.lua.)
local panel = UI.CreateInactive()
panel:SetID("main_menu_panel")
panel:SetAnchor("MiddleCenter")
panel:SetPixelSize(Screen.UIPixelWidth(), Screen.UIPixelHeight())
panel:SetInteractive("Block")   -- swallow clicks behind the menu

-- Attach a background texture (see the UITexture page).
local tex = UITexture.Create(panel)
tex:SetMaterial(Material.Get("ui_default"))

-- Start hidden and offset upward, then animate to rest.
panel:SetOpacity(0)
panel:SetPixelPosition(0, -40)
panel:Activate()

panel:AnimateOpacity(1.0, 0.25, "CubicOut")          -- fade in
panel:AnimatePixelPosition(0, 0, 0.25, "CubicOut")   -- slide down (x, y, dur, interp)

-- Later, in an update, react to hover:
if panel:HoverTime() > 0 and not panel:IsOpacityAnimating() then
    panel:AnimateScale(1.05, 0.1, "QuadraticOut")
end

Venus Lua API › UI

UIText

Text label component — string, font, layout, color, shader params.

authored lua/venus_lua/src/lua_ui_text.rs

UIText

A component that renders a text string inside a UIObject’s computed rectangle: string, font,
size, line height, alignment, wrapping, packed color, and character spacing, plus per-object
shader/instance parameters for custom text materials. Attach one with UIText.Create; instance
methods use colon syntax (text:SetText(...)).

Static calls use the global table (UIText.Create(obj) — dot). Instance methods take colon syntax
on the returned handle (text:SetFontSize(24)).

Types

UIText — an opaque handle (userdata) to a UIText component. A handle may go stale when its
component or owning UIObject is destroyed. Calling a getter on a stale handle logs a warning and
returns a default (nil for most; false for HasMaterialInstance; "" for GetText; 0 / a
zero Vector4 for the shader/instance getters); calling a setter silently no-ops (no warning).
Passing a receiver that is not a UIText userdata (a number, nil, a foreign handle) raises a Lua
type error — only UIText.Exists tolerates any value. tostring(text) always yields the literal
"UIText" (it does not reflect liveness or content).

Creation & Destruction

UIText.Create( UIObject uiobject ) : UIText
Adds a UIText component to uiobject and returns its handle. uiobject must be a UIObject (raises a
type error otherwise). Returns nil and logs a warning if the component cannot be added (e.g. the UI
object already has one).

UIText.Get( UIObject uiobject ) : UIText
Returns the UIText component already on uiobject, or nil (logs a warning) if it has none.
uiobject must be a UIObject (raises otherwise).

UIText.Exists( UIText text ) : boolean
Returns true if the handle refers to a live component. Safe on any value — a stale handle or a
non-UIText userdata returns false, and it never warns.

There is no explicit Destroy on UIText — the component is removed when its owning UIObject is
destroyed.

Functions

Read-only queries. Each warns and returns its default on a stale handle (see Types).

text:CharCount( ) : integer
Number of characters in the laid-out text. nil on a stale handle.

text:LineCount( ) : integer
Number of lines after wrapping. nil on a stale handle.

text:EffectiveMaterial( ) : Material
The material actually used to draw the text — a per-object Material instance if one
exists, otherwise the shared material; nil if none is set (or on a stale handle).

text:HasMaterialInstance( ) : boolean
true if this text owns a per-object material instance (rather than sharing one). Returns false on
a stale handle.

text:Active( ) : boolean
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 UIObject is deactivated,
in which case the component still is not running. Use ActiveInHierarchy() for the effective
answer.

text:ActiveInHierarchy( ) : boolean
true only when the own flag is enabled and the owning UIObject 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.

Properties (get / set)

Paired accessors. Getters warn and return a default on a stale handle; setters no-op silently
(no warning) on a stale handle — an asymmetry with the warning getters.

Text

text:GetText( ) : string — the current text, decoded from the internal u32 codepoints to
UTF-8. The getter is named GetText, not Text (the lone break from the bare-noun getter
convention). Returns "" on a stale handle.
text:SetText( string text ) : void — replaces the string. Non-string args are coerced by
luaL_checklstring (numbers accepted; other types raise).

FontSize

text:FontSize( ) : number
text:SetFontSize( number size ) : void

LineHeight

text:LineHeight( ) : number
text:SetLineHeight( number height ) : void

Align

text:Align( ) : string"Left", "Center", or "Right".
text:SetAlign( string align ) : void — takes the same three names the getter returns, so
a:SetAlign(b:Align()) round-trips. Matched exactly — no case folding, and numbers are
rejected. An unrecognized value raises a Lua error listing the three spellings. The name is
resolved before the component is touched, so a raise cannot leave a half-applied write.

Wrap

text:Wrap( ) : boolean
text:SetWrap( boolean wrap ) : void

Color

text:Color( ) : integer — the packed color as 0xRRGGBBAA (default 0xFFFFFFFF).
text:SetColor( integer packedRGBA ) : void — accepts a packed 0xRRGGBBAA integer or a
Vector4 of r, g, b, a in 0..1. The Vector4 form clamps each component to [0,1], scales by
255 with rounding, and packs to 0xRRGGBBAA.

Known issue: the Vector4 overload of SetColor packs components straight to bytes with no
sRGB→linear conversion
(lua_ui_text.rs:430-440), unlike the engine-wide convention that
“all color setters accept sRGB and convert to linear on input” (see CLAUDE.md Coordinate
Conventions). This packed color field is also separate from the component’s tint (the linear
state-color used by UITexture-style tinting), so text color set here won’t match colors set through
sRGB APIs elsewhere unless you account for the space difference.

CharacterSpacing

text:CharacterSpacing( ) : number
text:SetCharacterSpacing( number spacing ) : void

Font

text:Font( ) : Font — the assigned Font handle, or nil if none is set (default
font) or on a stale handle.
text:SetFont( Font font ) : voidfont must be a Font userdata (raises a type error
otherwise; see Font).
text:ClearFont( ) : void — reverts to the default font (no getter pair).

PerCharacterWrap

text:PerCharacterWrap( ) : boolean
text:SetPerCharacterWrap( boolean value ) : void

Overflow

text:Overflow( ) : boolean
text:SetOverflow( boolean value ) : void

Shader & instance parameters

Per-object payloads written into the text material’s push constants. Shader params are 24
f16 lanes
(indices 0..23, packed into the uiData region — lane meaning is shader-defined,
see venus_common.hlsl); instance params are 4 f32 lanes (indices 0..3, read by HLSL as
instanceParams.x/y/z/w). Instance params are the full-precision sibling — prefer them for time
accumulators or values that must survive an f32 round-trip. Lane and quad indices are 0-based.
Out-of-range indices are silent no-ops on setters and return 0 / a zero Vector4 on getters
(no warning — the only warning here is the stale-handle one on a getter’s slow path).

text:SetShaderParam( integer lane, number value ) : void
text:ShaderParam( integer lane ) : number
Set / get a single f16 shader lane (lane 0..23).

text:SetShaderParamsVec4( integer quadIndex, Vector4 value ) : void
text:ShaderParamsVec4( integer quadIndex ) : Vector4
Set / get four contiguous shader lanes at once: quadIndex q maps value.x/y/z/w to lanes
q*4 .. q*4+3. Valid quadIndex is 0..5 (covering all 24 lanes); 6+ writes past the end and is
a no-op / zero-vector. value must be a Vector4 (raises otherwise).

text:SetInstanceParam( integer lane, number value ) : void
text:InstanceParam( integer lane ) : number
Set / get a single f32 instance lane (lane 0..3).

text:SetInstanceParams( Vector4 value ) : void
text:InstanceParams( ) : Vector4
Set / get all four f32 instance lanes at once (value.x/y/z/w → lanes 0..3).

Methods

text:Activate( ) : void
Sets the component’s own enabled flag. The enable callback runs only if the owning UIObject is also
active. Silently no-ops on a stale handle.

text: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

-- Score readout: create-or-fetch a label, style it, and update it each frame.
function Script.OnEnable(self)
    local text = UIText.Get(self.object) or UIText.Create(self.object)
    local font = Font.Get("hud")            -- see the Font page; nil if unloaded
    if font then text:SetFont(font) end
    text:SetFontSize(24)
    text:SetAlign("Center")
    text:SetColor(Vector4.new(1, 1, 1, 1))  -- packed to 0xFFFFFFFF
    text:SetText("Score: 0")
    self.score = 0
end

function Script.Update(self)
    local text = UIText.Get(self.object)
    if text then
        text:SetText("Score: " .. tostring(self.score))
    end
end

Venus Lua API › UI

UITexture

Textured UI quad — material, mesh, instancing, and state colors.

authored lua/venus_lua/src/lua_ui_texture.rs

UITexture

A component that draws a textured quad (or a custom mesh) inside a UIObject’s rectangle: it
binds a material, optionally an instanced mesh, supports interaction-driven state colors, and carries
per-object shader/instance parameters. Attach one with UITexture.Create; instance methods use colon
syntax (uitex:SetMaterial(...)).

Types

UITexture — an opaque handle (userdata) to a UITexture component. A handle may go stale when its
component or owning UIObject is destroyed. Calling a getter on a stale handle logs a warning and
returns a default (nil for the material/StateColor/InstanceCount getters; false for the
Has* booleans; 0 / a zero Vector4 for the shader/instance getters); the property setters
(SetInstanceCount, shader/instance setters, Enable/DisableStateColors) no-op silently.
SetMaterial and SetMesh are the exception — they route through the scene and do log a warning
on a stale handle. Passing a receiver that is not a UITexture userdata raises a Lua type error;
only UITexture.Exists tolerates any value. tostring(uitex) always yields the literal
"UITexture".

Creation & Destruction

UITexture.Create( UIObject uiobject ) : UITexture
Adds a UITexture component to uiobject and returns its handle. uiobject must be a UIObject (raises
otherwise). Returns nil and logs a warning if the component cannot be added.

UITexture.Get( UIObject uiobject ) : UITexture
Returns the UITexture component already on uiobject, or nil (logs a warning) if it has none.
uiobject must be a UIObject (raises otherwise).

UITexture.Exists( UITexture uitex ) : boolean
Returns true if the handle refers to a live component. Safe on any value — a stale handle or a
non-UITexture userdata returns false, and it never warns.

There is no explicit Destroy — the component is removed when its owning UIObject is destroyed.

Functions

Read-only queries. Each warns and returns its default on a stale handle (see Types).

uitex:Material( ) : Material
The shared (non-instance) Material assigned to the quad, or nil if none is set (or on
a stale handle).

uitex:EffectiveMaterial( ) : Material
The material actually used to draw — a per-object instance if one exists, otherwise the shared
material; nil if neither is set.

uitex:HasMaterialInstance( ) : boolean
true if this texture owns a per-object material instance. false on a stale handle.

uitex:HasStateColors( ) : boolean
true if interaction-driven state colors are enabled. false on a stale handle.

uitex:StateColor( ) : Vector4
The current spring-animated state color as a linear RGBA Vector4 (the smoothed color the quad is
currently tinted with). nil on a stale handle.

uitex:Active( ) : boolean
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 UIObject is deactivated,
in which case the component still is not running. Use ActiveInHierarchy() for the effective
answer.

uitex:ActiveInHierarchy( ) : boolean
true only when the own flag is enabled and the owning UIObject 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.

Properties (get / set)

Getter warns and returns its default on a stale handle; the setter no-ops silently.

InstanceCount

uitex:InstanceCount( ) : integer — number of instances drawn when a custom instanced mesh is
bound. nil on a stale handle.
uitex:SetInstanceCount( integer count ) : void

Shader & instance parameters

Per-object payloads written into the material’s push constants. Shader params are 24 f16
lanes
(indices 0..23, packed into the uiData region — lane meaning is shader-defined, see
venus_common.hlsl); instance params are 4 f32 lanes (indices 0..3, read by HLSL as
instanceParams.x/y/z/w). Instance params are the full-precision sibling — prefer them for time
accumulators, pixel coordinates above ~2048, or anything needing f32 round-trip stability. Lane and
quad indices are 0-based. Out-of-range indices are silent no-ops on setters and return 0 /
a zero Vector4 on getters.

uitex:SetShaderParam( integer lane, number value ) : void
uitex:ShaderParam( integer lane ) : number
Set / get a single f16 shader lane (lane 0..23).

uitex:SetShaderParamsVec4( integer quadIndex, Vector4 value ) : void
uitex:ShaderParamsVec4( integer quadIndex ) : Vector4
Set / get four contiguous shader lanes at once: quadIndex q maps value.x/y/z/w to lanes
q*4 .. q*4+3. Valid quadIndex is 0..5 (covering all 24 lanes); 6+ is a no-op / zero-vector.
value must be a Vector4 (raises otherwise).

uitex:SetInstanceParam( integer lane, number value ) : void
uitex:InstanceParam( integer lane ) : number
Set / get a single f32 instance lane (lane 0..3).

uitex:SetInstanceParams( Vector4 value ) : void
uitex:InstanceParams( ) : Vector4
Set / get all four f32 instance lanes at once (value.x/y/z/w → lanes 0..3).

Methods

uitex:Activate( ) : void
Sets the component’s own enabled flag. The enable callback runs only if the owning UIObject is also
active. No-ops silently on a stale handle.

uitex:Deactivate( ) : void
Clears the own enabled flag; the disable callback runs only if the component was actually active.
No-ops silently on a stale handle.

uitex:SetMaterial( Material material ) : void
Assigns the shared material used to draw the quad. material must be a Material handle (raises
otherwise). Warns (NotFound) and no-ops on a stale UITexture handle.

uitex:SetMesh( Mesh mesh ) : void
Binds a custom Mesh to draw instead of the default quad; the mesh’s rendering info is read
from an already-loaded mesh. A null/invalid mesh resets the component to the default (empty) mesh
info silently. Warns (NotFound) and no-ops on a stale UITexture handle.

uitex:EnableStateColors( ) : void
Enables interaction-driven state colors (hover / press / etc. tint the quad). No-ops silently on a
stale handle.

uitex:DisableStateColors( ) : void
Disables state colors. No-ops silently on a stale handle.

Known issue: the Lua MeshContext used by SetMesh hard-codes upload_mesh to
Err(GpuUploadFailed) (lua_ui_texture.rs:44-46). It is inert today — UITexture::set_mesh only
reads rendering_info and never calls upload_mesh — but it means this path can only bind meshes
that are already GPU-resident; it can never trigger an upload of new mesh data from Lua.

Example

-- Build a background panel with a material, then enable hover/press tinting.
-- (Adapted from data/scripts/ui_setup.lua.)
function Script.OnEnable(self)
    local obj = self.object
    local uitex = UITexture.Get(obj) or UITexture.Create(obj)

    local mat = Material.Get("ui_default")
    if mat then
        uitex:SetMaterial(mat)
    end

    uitex:EnableStateColors()               -- quad now tints on hover/press
end

function Script.Update(self)
    local uitex = UITexture.Get(self.object)
    if uitex and uitex:HasStateColors() then
        local c = uitex:StateColor()        -- current spring-animated RGBA
        -- ... drive something off the live tint ...
    end
end

Venus Lua API › UI

UIButton

Interactive button component — enabled, focus, and click state.

authored lua/venus_lua/src/lua_ui_button.rs

UIButton

A component that makes a UIObject behave as a clickable button. It is a pure state machine:
it tracks an enabled/disabled flag, keyboard/gamepad focus, an interaction pseudo-state, and a
one-frame “was clicked” edge, and it writes the pseudo-state onto the owning UIObject so visual
components (like UITexture state colors) can react. Attach one with
UIButton.Create; instance methods use colon syntax (btn:WasClicked()).

Types

UIButton — an opaque handle (userdata) to a UIButton component. A handle may go stale when its
component or owning UIObject is destroyed. On a stale handle: the boolean getters
(IsEnabled/IsFocused/WasClicked) log a warning and return false, State logs a warning and
returns 0, and the setters (SetEnabled/SetFocused) log a warning and no-op. Passing a receiver
that is not a UIButton userdata raises a Lua type error; only UIButton.Exists tolerates any
value. tostring(btn) always yields the literal "UIButton".

Creation & Destruction

UIButton.Create( UIObject uiobject ) : UIButton
Adds a UIButton component to uiobject and returns its handle. uiobject must be a UIObject (raises
otherwise). Returns nil and logs a warning if the component cannot be added.

UIButton.Get( UIObject uiobject ) : UIButton
Returns the UIButton component already on uiobject, or nil (logs a warning) if it has none.
uiobject must be a UIObject (raises otherwise).

UIButton.Exists( UIButton btn ) : boolean
Returns true if the handle refers to a live component. Safe on any value — a stale handle or a
non-UIButton userdata returns false, and it never warns.

There is no explicit Destroy — the component is removed when its owning UIObject is destroyed.

Functions

btn:WasClicked( ) : boolean
true if the button was clicked this frame — a one-frame edge, so poll it every frame (e.g. from
Script.Update). Returns false (with a warning) on a stale handle.

btn:State( ) : integer
The button’s current interaction pseudo-state (UIPseudoState) as an integer. Returns 0 (with a
warning) on a stale handle. The same enum backs UIObject:PseudoState:

Value State Meaning
0 None idle / not interacting
1 NonInteractive disabled (not accepting input)
2 Hover pointer over the button
3 Activated press just began
4 Held press sustained
5 Released press just released
6 Focus holds keyboard/gamepad focus

(These are raw integers — no named UIButton.* constants are exposed.)

btn:Active( ) : boolean
The component’s own enabled flag — what Activate()/Deactivate() write. Returns nil (and
logs NotFound) on a stale handle. This is intent, not effect: the flag can be on while the
owning UIObject is deactivated, in which case the component still is not running.

btn:ActiveInHierarchy( ) : boolean
true only when the own flag is enabled and the owning UIObject is active in hierarchy — i.e.
the component is actually running. This is the predicate the engine uses to gate update dispatch.
Returns nil on a stale handle.

Component lifecycle is a separate axis from the button’s IsEnabled()/SetEnabled() flag below:
Active/Activate/Deactivate govern whether the component runs at all, while IsEnabled gates
whether an already-running button accepts input.

Properties (get / set)

Getters warn and return false on a stale handle; setters warn (NotFound) and no-op on a stale
handle. Note the Is-prefixed getter names (there is no bare-noun Enabled()/Focused()).

Enabled

btn:IsEnabled( ) : boolean — whether the button accepts input.
btn:SetEnabled( boolean enabled ) : void

Focused

btn:IsFocused( ) : boolean — whether the button currently holds focus.
btn:SetFocused( boolean focused ) : void

Methods

btn:Activate( ) : void
Sets the component’s own enabled flag. The enable callback runs only if the owning UIObject is also
active. Silently no-ops on a stale handle (no warning).

btn: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 (no warning).

Example

-- Poll a button every frame and act on the click edge.
function Script.Update(self)
    local btn = UIButton.Get(self.object)
    if btn and btn:IsEnabled() and btn:WasClicked() then
        -- handle the click (e.g. advance a menu)
    end

    -- React to interaction state (2 = Hover, 4 = Held).
    if btn and btn:State() == 2 then
        -- pointer is hovering
    end
end

Venus Lua API › UI

Font

Opaque font handles for UIText.

authored lua/venus_lua/src/lua_ui_text.rs

Font

A one-function module for looking up loaded fonts. Font.Get returns an opaque Font handle that
you hand to UIText via text:SetFont(font) (and read back with text:Font()). The
handle itself has no instance methods — it is only a typed reference into the font manager.
(Registered from the same binding file as UIText; see FONT_LIB and push_font_handle in
lua_ui_text.rs.)

Types

Font — an opaque handle (userdata, 16 bytes) to a font in the font manager, carrying the font’s
FontHandle bits (type id 69). Its metatable is sealed and has no __index, so it exposes no
methods, fields, or custom tostring: any font:Something() or font.field access yields nil
(and calling it errors with “attempt to call a nil value”). Pass it only to UIText:SetFont; get it
back from UIText:Font. A handle may be stale if the underlying font is evicted — SetFont
consumes the raw handle regardless, so a stale font simply resolves to nothing downstream.

Functions

Font.Get( string name ) : Font
Looks up a loaded font by its registered name (via the font manager’s get_resource) and returns
its handle. Returns nil and logs a warning (NotFound) if no font with that name is registered.
name must be a string (raises a type error otherwise).

Example

-- Resolve a named font once, then apply it to a label.
function Script.OnEnable(self)
    local text = UIText.Get(self.object) or UIText.Create(self.object)
    local font = Font.Get("title")   -- nil + warning if "title" isn't loaded
    if font then
        text:SetFont(font)
    end
end

Venus Lua API › Input & Time

Input

Poll keyboard, mouse, and action-map bindings.

authored lua/venus_lua/src/lua_input.rs

Input

A module singleton (global functions only, no handle) for polling this frame’s input. Every query
reads the committed input snapshot for the current frame (the double-buffered state the engine
commits once per frame), so results are stable for the whole frame.

There are two families:

  • Raw keyboard & mouse — read the committed device state directly. Note the raw keyboard
    surface is deliberately narrow: only Escape, Return, and Backspace have dedicated
    per-key getters (plus the any-key aggregates), and the mouse exposes the left and right
    buttons only. Any other physical key must be reached through a named action.
  • Named actionsKey / KeyDown / KeyUp / Axis resolve an action name through the
    binding maps. The string argument is an action name (e.g. "jump", "move_forward"), not
    a physical key name and not a key code. An unknown or empty name resolves to the null action
    and returns false / 0 — it never errors. (Names are matched via the engine’s 30-char Name
    type, so an action name is effectively truncated to 30 characters.)

Bind is the one call that takes a raw integer key code; EnableMap / DisableMap take an
action-map name string.

Functions

Raw keyboard & mouse

Input.AnyKey( ) : boolean
true while any key is held this frame.

Input.AnyKeyDown( ) : boolean
true on the frame any key was first pressed.

Input.AnyKeyUp( ) : boolean
true on the frame any key was released.

Input.MousePosition( ) : number, number
The cursor position as two return values x, y (in the input system’s coordinate space).

Input.MouseDelta( ) : number, number
Cursor movement since the previous frame as two return values dx, dy.

Input.MouseScrollX( ) : number
Horizontal scroll-wheel delta this frame (0 when not scrolling).

Input.MouseScrollY( ) : number
Vertical scroll-wheel delta this frame (0 when not scrolling).

Input.LeftMouse( ) : boolean
true while the left mouse button is held.

Input.LeftMouseDown( ) : boolean
true on the frame the left button was pressed.

Input.LeftMouseUp( ) : boolean
true on the frame the left button was released.

Input.RightMouse( ) : boolean
true while the right mouse button is held.

Input.RightMouseDown( ) : boolean
true on the frame the right button was pressed.

Input.RightMouseUp( ) : boolean
true on the frame the right button was released.

Input.Escape( ) : boolean
true while the Escape key is held (KeyCode::Escape).

Input.Return( ) : boolean
true while the Return/Enter key is held (KeyCode::Enter).

Input.Backspace( ) : boolean
true while the Backspace key is held (KeyCode::Backspace).

Named actions

Input.Key( string action ) : boolean
true while the named action is held (any of its bindings is down and its map is enabled). Unknown
or empty action → false.

Input.KeyDown( string action ) : boolean
true on the frame the named action was triggered (a rising edge). Action edges are consumed once
per frame, so this fires only on the first fixed step of a frame — a jump read from
Script.FixedUpdate will not double-fire when one render frame runs several fixed steps. Unknown
action → false.

Input.KeyUp( string action ) : boolean
true on the frame the named action was released. Unknown action → false.

Input.Axis( string action ) : number
The analog value of the named axis action (an action bound to an axis source such as a mouse
delta or gamepad stick). Returns 0 if the action is unknown or has no axis contribution.

Binding control

Input.Bind( integer keyCode, string action ) : boolean
Adds a keyboard binding: presses of keyCode now trigger the existing action action.
Returns true on success; false if keyCode is not a valid key code, if no action by that name
exists, or if that action’s binding list is full. It does not create actions — the action must
already be defined in a loaded binding map. See the known issue below for the key-code encoding.

Input.EnableMap( string mapName ) : boolean
Enables an action map by name. Returns true on success, false if no map by that name exists.

Input.DisableMap( string mapName ) : boolean
Disables an action map by name (its actions stop responding). Returns true on success, false
if no map by that name exists.

Known issue: Bind takes a bare integer key code and no key-code enum is exposed to Lua.
Input.Bind reads its first argument with luaL_checkinteger and casts it to a u8
KeyCode (lua_input.rs:174-188); valid codes are the raw KeyCode discriminants
0x000x74 (e.g. KeyA = 0x00, KeyW = 0x16 = 22, Space = 0x58 = 88 — see
engine/input/src/key_code.rs). The Input table registers only functions and no
constants (load_input, lua_input.rs:253-265), so a script has no symbolic names and must
hard-code these magic integers — a real usability gap. Two sharp edges follow from the
as u8 cast (lua_input.rs:176): a value ≥ 256 silently wraps mod 256 instead of failing
(e.g. 30044F9), and only the missing/unmapped high codes above 0x74 return
false. Passing a valid-but-wrong wrapped value binds the wrong key with no error.

Example

-- Adapted from data/scripts/kinematic_player.lua — read WASD (named actions) plus a
-- raw mouse-look delta each fixed step.
function Script.FixedUpdate(self)
    local ix, iz = 0, 0
    if Input.Key("move_forward") then iz = iz + 1 end
    if Input.Key("move_back")    then iz = iz - 1 end
    if Input.Key("move_right")   then ix = ix + 1 end
    if Input.Key("move_left")    then ix = ix - 1 end
    self.moveX, self.moveZ = ix, iz

    -- Edge-triggered jump (fires once even when a frame runs several fixed steps).
    if Input.KeyDown("jump") then
        self.vy = 8.0
    end

    -- Raw mouse-look: MouseDelta returns two numbers.
    local dx, dy = Input.MouseDelta()
    self.yaw = self.yaw + dx * self.sensitivity
end

Venus Lua API › Input & Time

Time

Frame and fixed-step timing — delta, scale, FPS.

authored lua/venus_lua/src/lua_time.rs

Time

A module singleton (global functions only, no handle) for reading frame and fixed-step timing.
Delta values come in scaled (multiplied by TimeScale) and unscaled forms; a separate
group describes the deterministic fixed-step physics loop. All values are read-only queries except
the three Set* setters, which clamp their inputs (see below).

Functions

Time.DeltaTime( ) : number
Scaled seconds elapsed since the previous frame (unscaled × TimeScale), pre-clamped to
MaximumDeltaTime.

Time.UnscaledDeltaTime( ) : number
Frame delta ignoring TimeScale (still clamped to MaximumDeltaTime).

Time.SmoothDeltaTime( ) : number
An exponentially-smoothed (EMA) frame delta — steadier than DeltaTime for animation.

Time.Time( ) : number
Scaled seconds since startup (accumulated DeltaTime).

Time.UnscaledTime( ) : number
Unscaled seconds since startup (accumulated UnscaledDeltaTime).

Time.RealtimeSinceStartup( ) : number
Drift-free wall-clock seconds since startup, read from an Instant. Never scaled, smoothed, or
clamped — it keeps advancing even when TimeScale is 0.

Time.FrameCount( ) : integer
Number of frames rendered since startup.

Time.FPS( ) : number
The smoothed frames-per-second display value, recomputed on roughly half-second windows (so it is
stable to read every frame, not a raw 1/DeltaTime).

Time.FixedStepCount( ) : integer
Number of fixed steps the accumulator will run this framenot a running total. It is 2
at 60 fps with the default 1/120 step, 0 or 1 at 144 fps, 4 at 30 fps, and is capped by
the spiral-of-death guard (≤ 8 with the default settings) after a stall.

Time.CurrentFixedStep( ) : integer
The 0-based index of the fixed step currently executing within this frame’s batch (ranges
0 .. FixedStepCount-1); set by the app frame loop before each Script.FixedUpdate.

Time.IsFixedUpdate( ) : boolean
true while the engine is dispatching fixed-update callbacks (i.e. inside a fixed step).

Properties (get / set)

Setters return void and silently clamp — they never raise a Lua error. Non-finite inputs (NaN,
±inf) are rejected outright, leaving the previous value unchanged (a NaN would otherwise poison the
physics accumulator).

TimeScale

Time.TimeScale( ) : number — the global time-scale multiplier (1.0 = real time, 0.0 = paused).
Time.SetTimeScale( number scale ) : void — scales DeltaTime/Time. Clamped to [0.0, 100.0] (lib.rs:316-327), so negatives become 0; 0 pauses scaled time while
RealtimeSinceStartup keeps running.

MaximumDeltaTime

Time.MaximumDeltaTime( ) : number — the frame-delta clamp ceiling, in seconds.
Time.SetMaximumDeltaTime( number seconds ) : void — caps DeltaTime/UnscaledDeltaTime to
avoid a spiral-of-death after a stall. Clamped to [0.001, 1.0] (lib.rs:361-372).

FixedDeltaTime

Time.FixedDeltaTime( ) : number — the fixed-step timestep, in seconds (default 1/120).
Time.SetFixedDeltaTime( number seconds ) : void — sets the deterministic physics timestep.
Clamped to [1/480, 1/30] (≈ 0.002080.03333 s, i.e. 30–480 Hz) (lib.rs:333-344).
Because the fixed-point physics runs lockstep off this cadence, treat it as a startup setting —
changing it mid-session changes the simulation (and its determinism hash).

Example

function Script.Update(self)
    -- Toggle pause without stopping wall-clock time (RealtimeSinceStartup keeps counting).
    if Input.KeyDown("pause") then
        Time.SetTimeScale(Time.TimeScale() > 0.0 and 0.0 or 1.0)
    end
    self.label:SetText(string.format("%.1fs  (%.0f fps)", Time.Time(), Time.FPS()))
end

Venus Lua API › Input & Time

Screen

Window and render-target dimensions and scale factors.

authored lua/venus_lua/src/lua_screen.rs

Screen

A module singleton (global functions only, no handle) reporting window dimensions, fullscreen
state, and the UI/render scale factors. Window dimensions are in logical points; the pixel
dimensions multiply those points by the corresponding scale factor. All queries read a per-frame
ScreenState snapshot, so they are stable for the whole frame.

Functions

Screen.WindowWidth( ) : integer
Viewport width in logical points.

Screen.WindowHeight( ) : integer
Viewport height in logical points.

Screen.WindowSize( ) : Vector2
Width and height (logical points) packed into a single Vector2 — one native value, size.x /
size.y. (Contrast Input.MousePosition, which returns two separate numbers.)

Screen.WindowAspect( ) : number
Viewport aspect ratio (width / height).

Screen.IsFullscreen( ) : boolean
true if the window is in fullscreen mode.

Screen.UIScaleFactor( ) : number
The UI (points → pixels) scale factor.

Screen.RenderPixelWidth( ) : integer
Render-target width in pixels — floor(WindowWidth × RenderScaleFactor).

Screen.RenderPixelHeight( ) : integer
Render-target height in pixels — floor(WindowHeight × RenderScaleFactor).

Screen.UIPixelWidth( ) : integer
UI surface width in pixels — floor(WindowWidth × UIScaleFactor).

Screen.UIPixelHeight( ) : integer
UI surface height in pixels — floor(WindowHeight × UIScaleFactor).

Properties (get / set)

RenderScaleFactor

Screen.RenderScaleFactor( ) : number — the render-resolution scale factor.
Screen.SetRenderScaleFactor( number scale ) : void — resizes the render target relative to
the window (e.g. 0.5 = half-res upscaled to the window, 2.0 = supersampling). Only strictly
positive values take effect
: scale <= 0 and non-finite (NaN) inputs are silently ignored — no
error, no change (engine/platform/src/screen.rs:233-241). There is no upper bound, so large
values will allocate a proportionally large render target.

Note: SetRenderScaleFactor is visible one frame later. The setter writes the global
platform state immediately, but the Screen.* getters read a per-frame ScreenState snapshot
that the app refreshes once at the top of each frame (app/src/lib.rs:491-498). So calling
Screen.SetRenderScaleFactor(x) and then Screen.RenderScaleFactor() in the same frame
returns the old value; the new scale (and the derived RenderPixelWidth/RenderPixelHeight)
appears next frame. This is intentional, not a bug — just don’t round-trip it synchronously.

Example

function Script.Start(self)
    local size = Screen.WindowSize()          -- one Vector2
    self.hud:SetPosition(size.x - 10, 10)      -- anchor near the top-right corner

    -- Drop to half-resolution rendering on low-end hardware.
    if self.lowSpec then
        Screen.SetRenderScaleFactor(0.5)       -- observed via RenderScaleFactor() next frame
    end
end

Venus Lua API › Systems

Network

Lobby and LAN discovery state machine — host, join, ready.

authored lua/venus_lua/src/lua_network.rs

Network

A module singleton (global functions only, no handle) wrapping the background networking thread’s
lobby and discovery state machine. There are no callbacks — Lua polls. Every getter reads a
main-thread snapshot the manager refreshes once per frame from the network thread, so values
reflect the last poll rather than the instant the network thread changed them. SetTarget requests
a desired state (idle / host / join); GetState reports the actual current state as a string.

GetState returns one of "idle", "hosting", "connecting", "lobby", "in_game",
"error".

Functions

Network.SetTarget( string mode, ... ) : void
Requests a target state. The extra arguments depend on mode, which is required:

  • SetTarget("idle") — go idle (no extra arguments).
  • SetTarget("host", integer port, string gameName, string hostName, integer maxPlayers) — host a
    lobby. gameName is truncated to 32 bytes, hostName to 16 bytes, and the local player name
    (from SetPlayerName) to 32 bytes.
  • SetTarget("join", string address) — join address ("ip:port"), truncated to 48 bytes.

Any other mode raises an argument error. port and maxPlayers are read as integers and wrapped
to u16/u8. Each accepted call bumps an epoch so the network thread re-latches the request.

Network.SetPlayerName( string name ) : void
Sets the local player name (truncated to 32 bytes) sent when hosting or joining. It is stored on
the manager and copied into the host/join params at the next SetTarget, so call it first.

Network.GetState( ) : string
The current network state: "idle", "hosting", "connecting", "lobby", "in_game", or
"error" (last-polled snapshot).

Network.GetError( ) : string or nil
The last error message from the network thread, or nil when there is none.

Network.IsHost( ) : boolean
true if this peer is the lobby host (last-polled snapshot).

Network.GetPlayers( ) : table
A 1-based array of the occupied lobby slots, each { slot: integer, name: string, ready: boolean, isLocal: boolean }. slot is the 0-based slot index (0–7); unoccupied slots are skipped, so
the returned array is dense even when the underlying slots are not.

Network.GetPlayerCount( ) : integer
Number of players currently in the lobby (the snapshot’s player count).

Network.AllReady( ) : boolean
true if every occupied slot is marked ready. An empty lobby returns true (there is no
un-ready slot), so gate a game start on GetPlayerCount() as well.

Network.SetReady( boolean ready ) : void
Sets the local player’s ready flag. The argument is read leniently: nil/false → not ready, any
other value → ready.

Network.RequestStartGame( ) : void
Raises the shared start-game flag that the host’s lobby watches to begin the match. The binding
itself performs no host check — a non-host may set the flag (the network thread decides whether
to honor it), so guard the call with IsHost() in script.

Network.SetBrowsing( boolean active ) : void
Enables or disables LAN server discovery (mDNS browsing). The argument is read leniently, like
SetReady.

Network.GetServers( ) : table
A 1-based array of discovered servers, each { gameName: string, hostName: string, address: string, playerCount: integer, maxPlayers: integer }, where address is "ip:port".

Network.GetServerCount( ) : integer
Number of discovered servers in the snapshot.

Example

-- Host a game, then poll the lobby each frame.
Network.SetPlayerName("Ada")
Network.SetTarget("host", 7777, "My Game", "Ada's Server", 8)

function Script.Update(self)
    if Network.GetState() == "hosting" then
        for _, p in ipairs(Network.GetPlayers()) do
            -- p.slot (0-based), p.name, p.ready, p.isLocal
        end
        -- AllReady() is vacuously true for an empty lobby, so require players too.
        if Network.IsHost() and Network.GetPlayerCount() > 0 and Network.AllReady() then
            Network.RequestStartGame()
        end
    end
end

Venus Lua API › Systems

Mod

Mod registry queries — loaded state, version, load order.

authored lua/venus_lua/src/lua_mod.rs

Mod

A module singleton (global functions only, no handle) for querying the mod registry: whether a mod
is loaded, its version, and the resolved load order. Mods are identified by their string id. All
three functions are read-only queries against the engine’s resource registry.

The modId argument is read with luaL_checkstring (a missing or non-string/-number id raises an
argument error); a non-UTF-8 id is treated as the empty string, which matches no mod.

Functions

Mod.IsLoaded( string modId ) : boolean
true if a mod with the given id is loaded, false otherwise (including unknown ids).

Mod.Version( string modId ) : integer or nil
The loaded mod’s version number, or nil if no mod with that id is loaded.

Mod.LoadOrder( ) : table
A 1-based array of mod-id strings in resolved (topologically-sorted) load order — the sequence the
registry initialized the mods in, dependencies first. Empty table when no mods are loaded.

Example

if Mod.IsLoaded("core") then
    print("core mod version", Mod.Version("core"))
end

for i, id in ipairs(Mod.LoadOrder()) do
    print(i, id)  -- dependencies appear before the mods that require them
end

Venus Lua API › Systems

Gizmo

Wireframe debug gizmos on the editor layer.

authored lua/venus_lua/src/lua_gizmo.rs

Gizmo

A module singleton (global functions only) that spawns wireframe debug gizmos as scene
GameObjects on the editor layer (LAYER_EDITOR) — they render only to editor cameras, never to
game views. Each constructor creates a child GameObject with a MeshRenderer (the named gizmo mesh

  • the gizmo_wire material), parents it under the passed-in parent, and returns that child’s own
    handle. The return value is a plain GameObject — ordinary transform/parenting
    methods work on it; Gizmo.SetColor and Gizmo.Destroy are module-level conveniences that take
    such a handle.

The gizmo meshes must be registered first (the app’s setup_gizmo_meshes); a missing mesh or the
gizmo_wire material raises an error. Color arguments are a Vector4 RGBA in 0..1 (accepted as
sRGB).

Creation & Destruction

Gizmo.CreateWireSphere( GameObject parent, Vector4 color ) : GameObject
Creates a wireframe sphere gizmo parented under parent. Returns the new gizmo GameObject.

Gizmo.CreateWireCube( GameObject parent, Vector4 color ) : GameObject
Creates a wireframe cube gizmo. Returns the new gizmo GameObject.

Gizmo.CreateWireCone( GameObject parent, Vector4 color ) : GameObject
Creates a wireframe cone gizmo. Returns the new gizmo GameObject.

Gizmo.CreateArrow( GameObject parent, Vector4 color ) : GameObject
Creates an arrow gizmo. Returns the new gizmo GameObject.

Gizmo.CreateCircle( GameObject parent, Vector4 color ) : GameObject
Creates a wireframe circle gizmo. Returns the new gizmo GameObject.

Gizmo.Destroy( GameObject gizmo ) : void
Destroys a gizmo GameObject and its children. No-op if the handle is null.

Methods

Gizmo.SetColor( GameObject gizmo, Vector4 color ) : void
Sets the instance color tint on the gizmo’s mesh renderer. No-op if the gizmo has no MeshRenderer.

Example

function Script.Start(self)
    -- Attach a red wire sphere to this object (editor-only visualization).
    self.gizmo = Gizmo.CreateWireSphere(self.object, Vector4.new(1, 0, 0, 1))
end

function Script.Update(self)
    if self.selected then
        Gizmo.SetColor(self.gizmo, Vector4.new(1, 1, 0, 1))  -- highlight yellow
    end
end

function Script.OnDestroy(self)
    Gizmo.Destroy(self.gizmo)
end

Venus Lua API › Systems

FSM

Attach YAML-defined state machines to UI objects.

authored lua/venus_lua/src/lua_fsm.rs

FSM

A module singleton (global functions only, no handle) for wiring a hierarchical FSM — a YAML
template plus a unified Lua script of per-state callbacks — onto a UI object. The script’s
state sub-tables become the machine’s states; module-level lifecycle/event functions (on_create,
on_enable, on_disable, on_destroy, OnClickRelease, update, late_update) run alongside.
Each state sub-table may define any of the per-state callbacks update, entering_update,
exiting_update, enter_start, enter_complete, exit_start, exit_complete, child_start,
child_complete, child_update, and can_transition. For the 3D/scene-graph equivalent driven
off the fixed step, see FixedUpdateFSM.

The global FSM table exposes one function: AttachToObject. Transitions are requested on the
FSM component itself — see UIFsm.

Per-state callbacks are dispatched by the FSM, so it hands itself in as argument 2:
function(self, fsm, data, time, dt, ...). It is pre-resolved because these run every frame.

Module-level UI events (OnClickRelease, OnHoverBegin, …) are dispatched by
UIScriptBehavior, the general UI scripting component, which knows nothing about state machines
and injects nothing: function(self, source_name). A handler that needs the FSM reaches it with
UIFsm.Get(self.object).

self is the per-instance context table, shared by both. The engine populates object and
behavior on it and reads nothing else — the rest is yours.

Functions

FSM.AttachToObject( UIObject uiObject, string yamlPath, string scriptPath ) : boolean or nil, string
FSM.AttachToObject( UIObject uiObject, string yamlPath, string scriptPath, string componentName ) : boolean or nil, string
Loads the FSM template from yamlPath (parsed once, then cached by path) and the unified Lua
script from scriptPath (both resolved relative to the FSM manager’s base directory), then
attaches a UIScriptBehavior + FsmComponent pair to uiObject. The optional componentName
names the behavior component. Returns true (a single value) on success, or nil plus an error
string (two values) on failure — a null uiObject, a resolved path that is too long, an unreadable
or malformed YAML file, a template with no states, a full template/script cache, a script that
fails to load, or a component add that fails. yamlPath/scriptPath are read with lua_tolstring,
so a missing or non-string path produces the nil, "…missing…" error rather than raising.

Removed: FSM.RequestTransition no longer exists. It had always been a registered no-op —
it resolved the component, then discarded the work under a // TODO: Direct transition API and
returned no values, so a caller’s request vanished with no error and no log. The mechanism that
actually worked was assigning a magic string field, self._pending = "state_name", on the shared
context table; that is gone too. Both are replaced by fsm:RequestTransition("state_name"),
which returns a real result.

Example

-- Attach the menu state machine to a UI panel.
local ok, err = FSM.AttachToObject(panel, "ui/menu_fsm.yaml", "scripts/menu_fsm.lua", "MenuFSM")
if not ok then
    print("FSM attach failed:", err)
end

-- Inside scripts/menu_fsm.lua, a state requests the next state like this:
--   M.main_menu = {
--       update = function(self, fsm, data, time, dt)
--           if start_clicked then fsm:RequestTransition("game") end
--       end,
--   }

Venus Lua API › Systems

FixedUpdateFSM

Attach a deterministic, fixed-step state machine to a GameObject.

authored lua/venus_lua/src/lua_fixed_update_fsm.rs

FixedUpdateFSM

A module singleton (global functions only, no handle) that attaches a Lua-driven finite state
machine to a scene-graph GameObject, ticked on the fixed-step update. The state machine’s
template is inferred from the script’s top-level state sub-tables: any key whose value is a table
and that is not a known lifecycle/module key (on_create, on_enable, on_disable,
on_destroy, OnClickRelease, update, late_update) becomes a state. For the UI-object variant,
see FSM.

Inside state handlers, self.object is the attached GameObject. The Lua Attach binding injects
no other context fields — the per-instance self.<param> fields that the FSM system can carry are
populated only by the YAML scene-installer path, which this call does not reach.

Functions

FixedUpdateFSM.Attach( GameObject object, string scriptPath, string initialState ) : boolean or nil
Loads the FSM module script at scriptPath (resolved relative to the script base directory),
builds the template from its state-table keys, and attaches a FixedUpdateFSM component to
object starting in initialState. Returns true on success, or nil on failure — an
empty/non-UTF-8 script path or initial-state string, a script that fails to load or does not return
a table, a template with no states, an initialState that is not one of those states, a full
script/template cache, or a failed component add. (Prerequisites not yet ready — the Lua VM or
script cache uninitialized — also return nil.)

FixedUpdateFSM.Detach( GameObject object ) : void
Destroys the FixedUpdateFSM component on object, firing its on_disableon_destroy
callbacks. No-op if the object has no such component.

FixedUpdateFSM.State( GameObject object ) : string or nil
Returns the current state’s name, or nil if the object has no FixedUpdateFSM component, the
component has no data or active instance, its template pointer is null, or the current state is
null.

Example

-- Attach a patrol/chase/attack FSM to an enemy and query its current state.
-- The script must define patrol/chase/attack as state sub-tables and return the module.
local ok = FixedUpdateFSM.Attach(enemy, "scripts/enemy_fsm.lua", "patrol")
if not ok then
    print("enemy FSM attach failed")
end

local state = FixedUpdateFSM.State(enemy)
if state == "chase" then
    -- ...
end

-- Later, when the enemy is removed:
FixedUpdateFSM.Detach(enemy)

Venus Lua API › Systems

UIFsm

The UI state machine component — request transitions, read current state.

authored 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 (and a logged error) if it has none.

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

Venus Lua API › Systems

ItemDB

Read-only queries against the loaded item database.

authored lua/venus_lua/src/lua_inventory.rs

ItemDB

A module singleton (global functions only, no handle) for querying the item-definition database
shared by Inventory and Equipment. An item definition carries an
id, a name (≤30 chars), an item_type tag, an equip_slot tag (0 = not equippable), a
stack_max limit, a flags bitfield, an icon index, and an optional set of numeric properties
keyed by a small integer. The engine stores and serves this data without interpreting the type,
slot, flag, or property meanings — those are game-defined. LoadYaml populates the database from
a YAML document.

Every function takes its itemId (and key) as an integer; passing a non-number raises a Lua
type error (luaL_checkinteger). Absent items and properties are reported as nil / 0 /
false, never as an error.

Functions

ItemDB.Get( integer itemId ) : table { id, name, type, equip_slot, stack_max, flags, icon } or nil
Returns the item definition as a table, or nil if itemId is not in the database. name is a
string; id, type, equip_slot, stack_max, flags, and icon are all integers. (The type
field is the item’s item_type tag; icon is the icon_index.)

ItemDB.GetProperty( integer itemId, integer key ) : number or nil
Returns the numeric (f32) property stored under key for itemId, or nil if either the item or
the property is absent. The two absent cases are indistinguishable — both yield nil.

ItemDB.PropertyCount( integer itemId ) : integer
Number of numeric properties defined on itemId; 0 if the item does not exist.

ItemDB.Exists( integer itemId ) : boolean
true if itemId has a definition in the database, false otherwise.

ItemDB.Count( ) : integer
Total number of item definitions currently loaded.

ItemDB.LoadYaml( string yaml ) : boolean
Parses yaml and loads item definitions from it (mutating the shared database). Returns true on
parse success, false on parse failure — in which case the database is left unchanged — or for
a null / empty / non-UTF-8 string.

Known issue: GetProperty’s key argument is silently narrowed to a u8
(lua_inventory.rs:91, let key = luaL_checkinteger(L, 2) as u8). A key of 256 wraps to 0
and returns property 0’s value rather than being rejected; keys must stay in 0..=255.
itemId is likewise narrowed to u32 across these functions, so negative or out-of-range ids
wrap silently.

Example

-- Inspect an item before handing it to the player.
if ItemDB.Exists(1001) then
    local def = ItemDB.Get(1001)
    print(def.name, "stacks to", def.stack_max)

    if def.equip_slot ~= 0 then
        print(def.name, "is equippable in slot", def.equip_slot)
    end

    -- Property key 0 is game-defined (e.g. "attack_power").
    local attack = ItemDB.GetProperty(1001, 0)
    if attack then
        print("attack power:", attack)
    end
end

Venus Lua API › Systems

Inventory

Per-entity slot-based item storage.

authored lua/venus_lua/src/lua_inventory.rs

Inventory

A module singleton (global functions only, no handle) that manages a slot-based
InventoryComponent on a GameObject. Each function takes the owning entity as its first
argument. Item stacking, capacity, and equippability are resolved against the shared
ItemDB. Slot indices, itemId, count, and instanceData are passed as integers.

Slot indices are 0-based. A component with n slots addresses slots 0 .. n-1; Find and
GetSlot/SetSlot/RemoveAt/Swap all use this range.

Argument and null behavior

The entity is validated with check_handle: passing a value that is not a GameObject (a
number, string, table, …) raises a Lua type error. A null or stale GameObject handle is not
an error — it resolves to “no component”, so each function falls through to its no-component
default (nil / 0 / 0,0,0 / false / no-op). Because of this, a 0 / 0,0,0 / false
result cannot be distinguished from an empty slot or a genuinely-missing item — use
Inventory.Get(entity) when you need to know whether the component itself exists.

Creation & Destruction

Inventory.Add( GameObject entity ) : void
Inventory.Add( GameObject entity, integer slotCount ) : void
Adds an InventoryComponent to entity with slotCount slots (default 16; narrowed to a
u8). The add result is ignored, so calling it on an entity that already has a component, or on a
dead handle, silently does nothing.

Inventory.Get( GameObject entity ) : boolean
true if entity currently has an InventoryComponent. This is the only presence check.

Inventory.Remove( GameObject entity ) : void
Destroys the entity’s InventoryComponent if present; a no-op otherwise.

Functions

Read-only queries. Each returns its no-component default (noted per entry) when the entity lacks
an InventoryComponent.

Inventory.GetSlot( GameObject entity, integer slot ) : integer, integer, integer
Returns itemId, count, instanceData for the 0-based slot. Returns 0, 0, 0 if the entity has
no component or the slot index is out of range or the slot is empty — the three cases are
indistinguishable.

Inventory.CountOf( GameObject entity, integer itemId ) : integer
Total quantity of itemId summed across all slots (0 if none / no component).

Inventory.HasRoom( GameObject entity, integer itemId, integer count ) : boolean
true if count more of itemId would fit, accounting for existing partial stacks (up to the
item’s stack_max, clamped to ≥1) and empty slots. false if not, or if there is no component.

Inventory.Find( GameObject entity, integer itemId ) : integer or nil
The 0-based index of the first slot holding itemId, or nil if none is found / no component.

Inventory.SlotCount( GameObject entity ) : integer
Number of active slots in the inventory (0 if no component).

Methods

Mutating actions. Slot arguments are 0-based and narrowed to a u8.

Inventory.AddItem( GameObject entity, integer itemId, integer count ) : integer or nil
Adds up to count of itemId, filling existing stacks then empty slots per the item’s
stack_max. Returns the number actually added (may be less than count, or 0 if full), or
nil if the entity has no InventoryComponent.

Inventory.RemoveAt( GameObject entity, integer slot, integer count ) : integer
Removes up to count items from the 0-based slot. Returns the number removed (0 if the slot
is empty, out of range, or there is no component).

Inventory.RemoveItem( GameObject entity, integer itemId, integer count ) : integer
Removes up to count of itemId across all slots. Returns the number removed (0 if none / no
component).

Inventory.Swap( GameObject entity, integer slotA, integer slotB ) : void
Swaps the contents of two slots. A no-op if either index is out of range, the indices are equal,
or there is no component.

Inventory.SetSlot( GameObject entity, integer slot, integer itemId, integer count, integer instanceData ) : void
Overwrites slot with the given stack (no stacking or validation against stack_max). A no-op if
the slot index is out of range or there is no component.

Inventory.SetSlotCount( GameObject entity, integer count ) : void
Resizes the inventory’s active slot count (e.g. unlocking bag slots). A no-op if there is no
component.

Known issue: every out-of-range slot index is a silent no-op or default rather than an
error — RemoveAt/Swap/SetSlot do nothing (inventory.rs:113, :149, :166) and GetSlot
returns InventorySlot::default() = 0,0,0 (inventory.rs:158). Combined with the
no-component fall-through, an out-of-range or empty read is indistinguishable from a missing
component. instanceData is carried as a u64 but pushed through lua_Integer (i64), so values
≥ 2^63 surface in Lua as negative integers.

Example

-- Give the player a 24-slot bag and add a stack of potions.
Inventory.Add(player, 24)

local added = Inventory.AddItem(player, 1001, 5)
print("added", added, "potions")

-- Slots are 0-based: read the first slot that holds item 1001.
local slot = Inventory.Find(player, 1001)
if slot then
    local id, count, instance = Inventory.GetSlot(player, slot)
    print("slot", slot, "holds", count, "of item", id)
end

if Inventory.HasRoom(player, 1001, 10) then
    Inventory.AddItem(player, 1001, 10)
end

Venus Lua API › Systems

Equipment

Per-entity named-slot equipment.

authored lua/venus_lua/src/lua_inventory.rs

Equipment

A module singleton (global functions only, no handle) that manages an EquipmentComponent — a set
of named equip slots — on a GameObject. Each function takes the owning entity as its first
argument. Unlike Inventory’s numeric slots, an equipment slot is identified by a
game-defined slotId (an EquipSlotId, u8). When you equip an item, the target slot is chosen
automatically: it is the slot whose id equals the item’s equip_slot field in the shared
ItemDB — you do not pass a slot to Equip.

Argument and null behavior

The entity is validated with check_handle: a non-GameObject first argument raises a Lua type
error, and Equipment.Add’s second argument must be a table (luaL_checktype) or it raises. A
null or stale GameObject handle is not an error — it resolves to “no component”, so each
function returns its no-component default (nil / false / no-op). Equipment.Get(entity) is the
only way to distinguish a missing component from an empty/absent slot.

Creation & Destruction

Equipment.Add( GameObject entity, table slotIds ) : void
Adds an EquipmentComponent to entity whose slot layout is the given array of slotId integers,
e.g. {1, 2, 3, 7, 8} for head/chest/legs/mainhand/offhand. At most 16 ids are read (extras
are silently dropped) and each is narrowed to a u8. The add result is ignored, so re-adding to an
entity that already has a component silently does nothing.

Equipment.Get( GameObject entity ) : boolean
true if entity currently has an EquipmentComponent. This is the only presence check.

Equipment.Remove( GameObject entity ) : void
Destroys the entity’s EquipmentComponent if present; a no-op otherwise.

Functions

Equipment.GetSlot( GameObject entity, integer slotId ) : integer, integer, integer or nil
Returns the itemId, count, instanceData currently equipped in slotId, or nil if the slot is
empty, no such slot exists in the layout, or the entity has no component (the cases are
indistinguishable).

Equipment.HasSlot( GameObject entity, integer slotId ) : boolean
true if entity’s equipment layout defines a slot with the given slotId. false if not or if
there is no component.

Methods

Equipment.Equip( GameObject entity, integer itemId, integer count ) : boolean or …
Equipment.Equip( GameObject entity, integer itemId, integer count, integer instanceData ) : boolean or …
Equips itemId into the slot matching its equip_slot (instanceData defaults to 0). The
return encodes the outcome:

  • true — equipped into a previously empty slot.
  • prevItemId, prevCount — two integers; the slot was occupied, so the incoming item replaced the
    previous one, which is returned to you (a swap).
  • false, errorString — rejected with no mutation; errorString is one of "unknown_item"
    (item not in ItemDB), "not_equippable" (the item’s equip_slot is 0), or
    "no_matching_slot" (the entity’s layout has no slot for that equip_slot).
  • nil — the entity has no EquipmentComponent.

Equipment.Unequip( GameObject entity, integer slotId ) : integer, integer, integer or nil
Clears slotId, returning the removed itemId, count, instanceData. Returns nil if the slot was
empty, no such slot exists, or the entity has no component.

Known issue: the four success/failure shapes of Equip are disambiguated only by Lua type
and arity (boolean vs. two integers vs. false, string vs. nil) — lua_inventory.rs:401.
Callers must test result == true / result == false / result == nil explicitly rather than
treating the first return as a plain boolean, since a successful swap returns a number as the
first value. slotId and each slotIds entry are silently narrowed to u8.

Example

-- Lay out equip slots, then equip a sword (item 2001, whose equip_slot is 7).
Equipment.Add(player, {1, 2, 3, 7, 8})

local a, b = Equipment.Equip(player, 2001, 1)
if a == true then
    print("equipped into an empty slot")
elseif a == false then
    print("rejected:", b)          -- b is the error string
elseif a ~= nil then
    print("swapped out item", a, "x" .. b)   -- a, b = prev itemId, count
end

-- Read it back by the item's equip slot id.
if Equipment.HasSlot(player, 7) then
    local id, count = Equipment.GetSlot(player, 7)
    print("mainhand holds item", id)
end

Venus Lua API › Systems

Save

Schema-driven save/load — records, slots, quick/autosave.

authored lua/venus_lua/src/lua_save.rs

Save

A module singleton (global functions only, no handle) for the schema-driven save system. Game state
is stored as named SaveRecords (each bound to a registered schema) inside an
in-memory save-data blob, which is written to and read from numbered slots plus dedicated
quicksave and autosave slots. OnSave / OnLoad callbacks let Lua serialize and reconstruct state
around a slot write/read.

There are 10 numbered slots, addressed 0..9 (MAX_SLOTS). Slot arguments outside that range
are rejected without touching disk. String arguments are read with luaL_checklstring (a
non-string raises); non-UTF-8 bytes decode to an empty string. Record names longer than 30 bytes
are truncated (they become a Name).

Functions

Records (in-memory save data)

Save.NewRecord( string schemaName ) : SaveRecord or nil
Creates a fresh SaveRecord for the named schema, fields at their schema defaults.
Returns nil if no schema with that name is registered.

Save.GetRecord( string name ) : SaveRecord or nil
Returns a copy of the record stored under name in the current save data (as a Lua-owned
SaveRecord), or nil if absent. Because it is a copy, mutating the returned record
does not change the stored data until you write it back with SetRecord.

Save.SetRecord( string name, SaveRecord record ) : void
Stores a copy of record under name in the current in-memory save data. record must be a
SaveRecord userdata (validated with luaL_checkudata) — passing anything else raises a Lua error.

Save.RemoveRecord( string name ) : boolean
Removes the record named name from the current save data. Returns true if one was removed,
false if there was no such record.

Save.HasRecord( string name ) : boolean
true if a record named name exists in the current save data.

Save.RecordCount( ) : integer
Number of records in the current save data.

Save.ListRecords( ) : table
A 1-based array of the record-name strings in the current save data.

Save.Clear( ) : void
Clears all in-memory save data (does not touch on-disk slots).

Slots & persistence

Save.Save( integer slot ) : boolean or boolean, string
Runs the OnSave callback, stamps the header timestamp, then writes the current save data to
slot. Returns true on success; bare false on an out-of-range slot or a failed disk write;
or false, errorString only if the OnSave callback raised — in which case the on-disk slot is
left untouched.

Save.Load( integer slot ) : boolean or boolean, string
Reads slot into memory, then runs the OnLoad callback. Returns true on success; bare false
on an out-of-range slot or a failed disk read (the callback does not run); or false, errorString if the read succeeded but OnLoad raised — leaving records loaded but reconstruction
incomplete.

Save.SlotExists( integer slot ) : boolean
true if a save file exists at slot. false for an out-of-range slot.

Save.Delete( integer slot ) : boolean
Deletes the save file at slot. Returns true on success, false on an out-of-range slot or a
failed delete.

Save.GetSlotInfo( integer slot ) : table { name, timestamp, play_time } or nil
Returns slot metadata — name (string), timestamp (integer, Unix seconds), play_time (number)
— or nil for an empty or out-of-range slot.

Quicksave & autosave

Save.Quicksave( ) : boolean or boolean, string
Like Save.Save but to the dedicated quicksave slot (no slot argument). Runs OnSave first; a
raising callback aborts the write and returns false, errorString.

Save.Autosave( ) : boolean or boolean, string
Like Save.Quicksave but to the dedicated autosave slot.

Save.LoadQuicksave( ) : boolean or boolean, string
Loads the quicksave slot, then runs OnLoad. Bare false on a failed read; false, errorString
if OnLoad raised after a successful read.

Save.LoadAutosave( ) : boolean or boolean, string
Like Save.LoadQuicksave but from the autosave slot.

Save.QuicksaveExists( ) : boolean
true if a quicksave file exists.

Save.AutosaveExists( ) : boolean
true if an autosave file exists.

Callbacks

Exactly one OnSave and one OnLoad callback are held at a time (as a registry ref on the Lua
state). OnSave fires before every write (Save/Quicksave/Autosave); OnLoad fires
after a successful read (Load/LoadQuicksave/LoadAutosave). Each is invoked with no
arguments and its return values ignored
, under a pcall guarded by a traceback handler; if it
raises, the message is mirrored to stderr and surfaced to the caller as the errorString second
return.

Save.SetOnSave( function callback ) : function or nil
Registers callback as the pre-save hook, replacing (and returning) the previous one, or nil if
none was set. callback must be a function (luaL_checktype) — see the known issue.

Save.SetOnLoad( function callback ) : function or nil
Registers callback as the post-load hook, replacing (and returning) the previous one, or nil.

Header metadata

Save.SetSaveName( string name ) : void
Sets the display name written into the save header.

Save.SetPlayTime( number seconds ) : void
Sets the play-time value (seconds, stored as f32) written into the save header.

Known issue: SetOnSave / SetOnLoad call luaL_checktype(L, 1, LUA_TFUNCTION)
(lua_save.rs:646, :662), so there is no way to clear a callback once set — passing nil
raises instead of unregistering. The only reset is re-running load_save (engine re-init).
Also note the false, errorString two-value form is emitted only on a callback raise: an
out-of-range slot or a plain disk-write/read failure returns a bare false with no reason
(lua_save.rs:534, :550, :571).

Example

-- Serialize player state into a record just before each save.
Save.SetOnSave(function()
    local r = Save.NewRecord("player")
    r:SetF32("health", Player.health)
    r:SetVec3("position", Player.position)
    Save.SetRecord("player", r)
end)

-- Rebuild it after each successful load.
Save.SetOnLoad(function()
    local r = Save.GetRecord("player")
    if r then
        Player.health   = r:GetF32("health")
        Player.position = r:GetVec3("position")
    end
end)

Save.SetSaveName("Autumn Fields")
local ok, err = Save.Save(0)
if not ok then
    print("save failed:", err or "disk error")
end

Venus Lua API › Systems

SaveRecord

A schema-bound bag of typed fields for the save system.

authored lua/venus_lua/src/lua_save.rs

SaveRecord

A single schema-bound record used by the Save system: a set of named, typed fields
(f32 / i32 / u32 / bool / Vector2 / Vector3 / Vector4 / Quaternion / string) whose layout and
defaults come from a registered schema. Every accessor is keyed by a field-name string and is
validated against the record’s schema on each call (the schema is re-resolved from the save
manager’s registry every time). Instance methods use colon syntax
(record:GetF32("health")).

Types

SaveRecord — userdata wrapping an owned record (heap-boxed in Rust). Calling a method on a value
that is not a SaveRecord raises a Lua type error (luaL_checkudata). tostring(record) yields
SaveRecord(schema), where schema is the record’s schema name. The underlying record is freed by
the __gc metamethod when the userdata is garbage-collected — there is no explicit destroy and no
stale-handle hazard.

Creation & Destruction

SaveRecord has no constructor global. Records are produced by
Save.NewRecord (a fresh record at schema defaults) and Save.GetRecord (a
copy of a stored record), and written back with Save.SetRecord. Each Lua record owns
its own heap record independently of the manager’s stored data: a record from GetRecord is a
snapshot, so later SetRecord calls elsewhere do not change it, and mutating it does not change the
stored copy until you SetRecord it back. Records are reclaimed by Lua garbage collection.

Functions

record:SchemaName( ) : string
The name of the schema this record is bound to. Always succeeds.

Properties (get / set)

Typed field accessors, each keyed by a field-name string (a non-string field name raises;
non-UTF-8 decodes to "").

Getters return the field value as a single value on success. On failure they return two
values — nil plus an error string: "field not found or type mismatch" when the schema exists but
the field is absent or is a different type, or "schema not found" when the record’s schema is not
registered.

Setters take the field name plus a value and return a single boolean: true on success,
false if the field is absent, the type does not match, or the schema is not registered. Setters
never return an error string, so the failure cause is not reported (see the known issue).

F32

record:GetF32( string field ) : number or nil, string
record:SetF32( string field, number value ) : boolean

I32

record:GetI32( string field ) : integer or nil, string
record:SetI32( string field, integer value ) : boolean

U32

record:GetU32( string field ) : integer or nil, string
record:SetU32( string field, integer value ) : boolean

Bool

record:GetBool( string field ) : boolean or nil, string
record:SetBool( string field, boolean value ) : boolean — the value is read with
lua_toboolean, so any Lua value is accepted and coerced by Lua truthiness (only nil/false are
false).

Vec2

record:GetVec2( string field ) : Vector2 or nil, string
record:SetVec2( string field, Vector2 value ) : boolean

Vec3

record:GetVec3( string field ) : Vector3 or nil, string
record:SetVec3( string field, Vector3 value ) : boolean

Vec4

record:GetVec4( string field ) : Vector4 or nil, string
record:SetVec4( string field, Vector4 value ) : boolean

Quat

record:GetQuat( string field ) : Quaternion or nil, string
record:SetQuat( string field, Quaternion value ) : boolean

Str

record:GetStr( string field ) : string or nil, string
record:SetStr( string field, string value ) : boolean — the field is stored as a Name, so
value is truncated to 30 bytes.

Known issue: the getter/setter error contracts are asymmetric. Getters distinguish
"schema not found" from "field not found or type mismatch" but still fold field-absent and
type-mismatch into that single string (lua_save.rs:157, :162). Setters collapse all
failures — missing schema, missing field, wrong type — into a bare false with no error string
(lua_save.rs:317324), so a caller cannot tell why a set failed. A get/set failure is not an
error to Lua; nothing raises.

Example

local r = Save.NewRecord("player")
r:SetF32("health", 100.0)
r:SetVec3("position", Vector3.new(0, 1, 0))

-- Getters may return (nil, errorString) on a bad field or type.
local hp, err = r:GetF32("health")
if hp then
    print(r:SchemaName(), "health =", hp)
else
    print("read failed:", err)
end

-- Setters return a plain boolean; false means the field/type/schema was invalid.
if not r:SetI32("level", 5) then
    print("no i32 field 'level' in this schema")
end