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

215 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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](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:
```lua
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
```lua
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.
```lua
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.