Files
skate-3-rust-engine/sdk/GENERAL_API.md
T

12 KiB
Raw Blame History

General engine API

Manifest API remains 2. Feature discovery uses compiled sdk.capabilities: engine_access=1, command_results=1, native_bodies=1, input_override=1, player_physics=2, player_overlap=1, landed_details=1. The host has no injury, vehicle or challenge rules. Those live in Lua.

deformation=1 adds optional impact-driven mesh/collider deformation. See DEFORMATION.md for generic body parameters, scene-node bindings, performance limits and multiplayer behavior.

Reading systems

sdk.engine.systems() lists available system names. sdk.engine.read(name) reads the latest snapshot, without executing a new simulation step:

Name Data Write interface
player Mode, bail, stance, pose, velocity, scoring and transition counters Existing player teleport, participation, attachment and gesture controls
rig Native bodies, anatomical joints, solver contacts and joint loads sdk.rig, typed sdk.bodies impulses
bodies This mod's Rapier bodies and contacts sdk.physics creation, geometry, forces, velocities, transforms, joints and motors
input Raw pad, keys and 18 mapped action values sdk.input.override_action
graphs Live action/motion controller states, state times and active behavior IDs sdk.graphs.set_enabled
animation Tick, pose generation and bone count No arbitrary animation replacement or pose injection
scoring Current player scoring observations No arbitrary native score/collector mutation
world Current map observation Existing mod geometry and volume APIs; no rewriting map assets
camera Current camera observation Existing follow, watch, mirror, rig, set, capture and release controls
network Session identity, peers and mod state Existing state publication, entity replication and authority-controlled operations
commands This mod's most recent command receipts sdk.commands.request

The existing graphics, audio, UI, settings, assets and timer interfaces remain available. Menus can create a named pause-menu section and nested pages using sdk.ui.menu(key, {section=..., title=..., items=...}).

Catalogs are larger and requested explicitly:

sdk.engine.inspect("catalog", "graphs") -- or "scoring"
-- On a later callback:
local result = sdk.commands.result("catalog")
if result and result.ok then
    local action = result.value.action
end

Graph catalogs expose authored names, hierarchy, transitions, operations and runtime behavior IDs. states[].enabled, transitions[].enabled and behaviors[].enabled reflect runtime gates. authored_enabled and operation metadata describe the loaded asset. Binding states[].behaviors uses operation IDs; use behaviors[].id for behavior gates. Scoring catalogs expose loaded definition IDs, labels, categories and authored points, not a substitute scoring implementation.

Commands and results

local token = sdk.commands.request("release_joint", {
    kind="player_joint", joint=joint.index,
    options={free_swing=true, free_twist=true, drive_enabled=false,
             possession_enabled=false, descendants=true}
})
-- A later snapshot, after the host executes the command:
local result = sdk.commands.result("release_joint")
if result then
    if result.ok then sdk.log("Applied") else sdk.log(result.error) end
end

A request wraps any validated native command table. The kind and payload are the same as the public wrappers in crates/skate-mods/src/api.lua; the full schema is Command in crates/skate-mods/src/vm.rs. Results contain token, ok, error, value, and simulation tick. Reusing a key replaces its slot; the Lua accessor ignores an older token. Maximum 64 result keys per mod. Receipts remain readable until replacement or mod/world cleanup. Nested requests are rejected. Invalid Lua/schema arguments still fail callback validation.

ok means the host accepted and applied that command. It does not mean a remote peer acknowledged a network message or a multi-tick operation finished. Host execution failures become receipts instead of disabling the mod. Commands are not rollback transactions. Ordinary wrappers remain fire-and-forget, with their existing error behavior. Skyline uses a receipt for chassis creation before binding graphics, audio and driving state to that body.

Native bodies and constraints

All native IDs are zero-based; Lua arrays are one-based. Discover IDs from observations instead of assuming animation bone IDs equal physical part IDs.

Use sdk.rig.read({"tick", "ragdoll", "contacts"}) to request only those top-level fields. sdk.rig.contacts(), parts() and joints() also convert only their requested section into Lua. Calling sdk.rig.read() without arguments still returns the complete rig, including joint solver data.

local rig = sdk.rig.read()
local part = rig.parts[1]
local body = sdk.bodies.read({kind="skater", index=part.index})
sdk.bodies.impulse(body, {0,10,0}, body.position)
sdk.bodies.angular_impulse({kind="board", index=0}, {0,1,0})
sdk.bodies.impulse({kind="mod", key="crate"}, {0,10,0})

Native skater/board reads expose position, XYZW quaternion, world linear/angular velocities, inverse mass/inertia and state flags. Skater parts also expose the mapped bone name, joint index, effective collision/material/drive settings and the direct part override. Impulse vectors use world coordinates: N s for linear, N m s for angular; an optional world point adds the corresponding torque. Native impulses are local-player only and rejected while attached or suspended. Commands apply between simulation steps. Subsequent native controls and constraints still participate. Bodies with no dynamic mass reject impulses.

sdk.rig.configure_joint(index, options) replaces a mod-owned override:

  • swing_limit, twist_limit: radians in [0.01, pi].
  • free_swing, free_twist: release the corresponding angular limit.
  • enabled=false: omit the entire joint constraint, including its linear anchor.
  • drive_enabled=false: omit the child's animation bone drives.
  • possession_enabled=false: omit board-holding drives involving the child.
  • descendants=true: extend those drive suppressions down the physical joint hierarchy. Joint angle/enable changes still affect the specified joint.

Free angular limits retain linear attachment. This is what Broken Bones uses. Native parameters and packed joint frames are readable but are not writable raw memory. Limits apply to a temporary joint copy, so native defaults remain intact. Drive-enable observations include ancestor and part suppression; true permits native drives but does not create a drive that the current native state omits.

sdk.rig.configure_part(index, options) replaces a physical part override: motion="dynamic"|"frozen"|"static" (native flags 4/2/1), collision=bool, friction=0..10 (both material coefficients), animation_drives=bool, and possession_drives=bool. These affect the native solve. They do not replace the whole player's movement state, mass preset, rendering or animation output. Freezing a physical part does not freeze its animation target.

reset_joint(index) and reset_part(index) release owned overrides. sdk.rig.reset() releases all this mod's joint and part overrides. Direct joint/part slots, action slots and graph gates reject a different owner's write. Omitted fields follow native behavior. Mod failure, disable/unload and world changes restore owned controls. Released impulses are physical changes and are not undone. Another mod's ancestor drive suppression can still affect a child.

Contacts and joint loads

sdk.rig.contacts() reads the most recent completed native solve. rig.tick is the cursor; do not count the same snapshot again in multiple callbacks. Contacts include:

  • id, phase: stable body-pair identity and begin, stay, end. Multiple manifold points share the pair lifecycle.
  • a, b: board/skater body index, static world, external solid or remote actor. External solids include solid_id and, when locally known, mod owner/key. Remote actors include peer ID and board/skater part identity.
  • World point and normal; normal/friction/total forces; impulse (force * dt); combined static/dynamic friction and material tags.
  • relative_velocity_before_solve: A minus B velocity at the contact point, including angular velocity, sampled from the bodies before this solve. closing_speed=max(0,-dot(relative_velocity,normal)).

These are solver samples, not exact continuous time-of-impact measurements. Resting support can have substantial force, so injury rules should also require closing speed. end reports have zero force/impulse/velocity. At most 128 active contact reports plus enough end reports to reach 256 are published per tick; contacts_truncated signals omitted reports. Pair tracking still examines all contacts; truncation does not create false contact ends.

Each joint's load is absent if no eligible row was solved. Otherwise it exposes world linear/angular impulse and force/torque, plus the packed solver_words. The host projects native accumulators (bytes 32/48) using axes at 192/240 and divides displacement-producing corrections by dt for impulses, then dt again for forces. Signs are for the child/A body. Angular values describe the direct angular constraint couple, excluding the lever-arm torque from the linear anchor. These are constraint reactions, not a bone-damage model.

Rapier sdk.physics.read(key).player_overlapping tests enabled native player shapes against the body's actual collider shapes. Sensors use intersection; solids include a 0.025 m contact tolerance. Suspended/attached players are excluded. This differs from sdk.volumes, which checks observed player root positions.

Input and graph control

sdk.input.override_action(id, value) overrides mapped gameplay actions 64–81 with a value in [-1,1]; nil releases the slot. It applies at the next input publication and lasts until released or cleaned up. It leaves raw pad/key observations intact and respects menu/debug input suppression.

sdk.graphs.read("action"|"motion") reads the live controller. sdk.graphs.set_enabled(graph, "state"|"transition"|"behavior", id, bool|nil) sets or restores an eligibility gate. It does not force a state jump, bypass native conditions, inject callbacks, or guarantee an immediate exit from an already-active state. Use commands.request with kind="graph_gate" when the mod needs an explicit rejection result. Native graph execution still owns state entry/exit and behavior lifecycle.

Bundled examples and remaining boundaries

Broken Bones selects physical limb joints using observed names/topology and requires both closing speed and impulse. It releases angular limits and limb drives, keeps linear anchors, reports success only after a receipt, and provides a Heal action. Injury state is published through generic mod network state; the existing native pose stream carries the resulting physical pose. Peers must run the mod to display injury summaries. It does not simulate fractures medically, detach meshes, or add blood rendering.

Simon Says uses confirmed settlement metadata (landed_trick_base, landed_spin_degrees, landed_clean, landed_sketchy) and explicit round baselines. Wipeout uses persistent kinematic solids and actual collider overlaps.

The API now provides these concrete lower-level interfaces, not arbitrary access to every Rust resource. Arbitrary animation blending/pose injection, scoring collector replacement, raw graph operation replacement, native topology creation, and direct mutation of another peer's native joints remain unexposed. New needs should extend these engine interfaces rather than adding mod-specific functions.

Contact lifecycle refers to force-bearing native solver reports: an end means no positive normal response was reported for that pair. It is not proof that the collider shapes have geometrically separated.