mirror of
https://github.com/SK8-ENGINE/skate-3-rust-engine.git
synced 2026-10-26 19:47:44 +00:00
Add generalized engine access and challenge mods, repair SKATE turn handling, and improve collision prediction and physics transitions. Reduce mod snapshot and material update overhead, preserve authored GLB materials with lighting and shadows, and improve CPU profiling.
263 lines
13 KiB
Markdown
263 lines
13 KiB
Markdown
# Engine observations and controls (API 2)
|
||
|
||
The SDK exposes engine primitives. Game rules, turn order, trick matching, scoring
|
||
policies, and win conditions belong in Lua mods. No host API depends on a mod ID
|
||
or a particular game mode.
|
||
|
||
## Player and scoring observations
|
||
|
||
`sdk.player.read()` reads the local skater. `sdk.player.skaters()` returns a table
|
||
keyed by peer ID; `sdk.player.skater(id)` reads one entry. Entries include names,
|
||
pose, velocity, angular velocity, movement mode, grind, stance, score, and named
|
||
input intents. `sdk.net.players()` lists connected peers; their first skater
|
||
observation may arrive later.
|
||
|
||
- `trick` / `trick_seq`: latest trick **announcement**, including midair tricks.
|
||
- `landed_trick` / `landing_seq`: latest confirmed, banked on-board scoring
|
||
sequence. The counter advances only after successful native scoring settlement,
|
||
not on announcement, bail, offboard completion, or teleport. The label remains
|
||
available until the next confirmed result.
|
||
- `bail_seq`: counter incremented once when entering a native wipeout.
|
||
|
||
Counters are observation cursors, not one-frame pulses. Remember the previous
|
||
counter, establish a baseline when a peer appears, and handle a lower counter as
|
||
a reset. Remote snapshots carry the latest state, not an unbounded event history;
|
||
intermediate events may be coalesced if several occur between received updates.
|
||
Trick labels are display strings, not a structured trick taxonomy. A mod chooses
|
||
its own matching policy. `sdk.capabilities.skater >= 4` includes these counters.
|
||
|
||
```lua
|
||
local previous = {}
|
||
return {on_fixed_update = function()
|
||
for id, skater in pairs(sdk.player.skaters()) do
|
||
local before = previous[id]
|
||
if before and skater.landing_seq > before then
|
||
sdk.log(skater.name .. " landed " .. skater.landed_trick)
|
||
end
|
||
previous[id] = skater.landing_seq
|
||
end
|
||
end}
|
||
```
|
||
|
||
## Native player movement and session authority
|
||
|
||
`sdk.player.teleport({position={x,y,z}, heading=radians, velocity={x,y,z}})`
|
||
uses native player travel. Heading defaults to zero; velocity is optional. This
|
||
only moves the local skater. Coordinates are bounded to +/-100000 and velocity
|
||
components to +/-200. Invalid commands fail validation.
|
||
|
||
`sdk.session.info()` reports the transport host and current session authority.
|
||
Authority is global to the session, initially the host. The current authority may
|
||
`sdk.session.transfer(peer)` to another connected player; the transport host
|
||
ratifies the transfer asynchronously. The host can reclaim with
|
||
`sdk.session.claim()`. The current authority can use
|
||
`sdk.session.teleport(peer, options)` to relocate a connected skater. Receivers
|
||
validate the sender, authority generation, command sequence, and movement values.
|
||
Reconnect/host changes reset authority; the host reclaims it if its holder leaves.
|
||
These operations do not implement turns or game rules.
|
||
|
||
## Cameras and world volumes
|
||
|
||
`sdk.camera.watch(peer)` follows a remote skater's presented pose;
|
||
`sdk.camera.watch(nil)` releases spectating. Camera overrides retain the existing
|
||
single-mod ownership rule.
|
||
|
||
`sdk.volumes.box(key, {position={x,y,z}, size={x,y,z}, rotation={x,y,z,w},
|
||
visible=true, color={r,g,b}, opacity=0.3})` creates or updates an oriented box.
|
||
`sdk.volumes.read(key).inside` lists peer IDs whose observed root positions lie
|
||
inside it. This is a point overlap query, not a collider or a skater hull test.
|
||
Boxes belong to their creating mod, max 32 per mod; remove with
|
||
`sdk.volumes.remove(key)`. Changes become observable on the next snapshot.
|
||
|
||
`sdk.camera.capture(key, {position={x,y,z}, look_at={x,y,z}, fov=radians,
|
||
width=256, height=256})` creates or updates an offscreen camera. FOV defaults to
|
||
70 degrees (in radians), range 0.2–2.5. Dimensions must be multiples of 16 between
|
||
64 and 512. Limits: 2 captures per mod, 4 overall. Bind a capture using
|
||
`sdk.graphics.mesh_buffer(key, {capture="camera-key"})`, then write mesh geometry
|
||
and UVs with the existing buffer methods. `texture` and `capture` are mutually
|
||
exclusive. Capture surfaces are excluded from capture views to prevent recursive
|
||
render-target sampling. Clearing/resizing a capture updates its bound materials.
|
||
Release it with `sdk.camera.clear_capture(key)`.
|
||
|
||
`sdk.graphics.mesh(key, {path="model.glb", opacity=0.5})` supports opacity 0–1,
|
||
including replication. Camera captures and volumes are local, mod-owned resources
|
||
and are cleared when their mod stops or the world changes.
|
||
|
||
## Multiplayer names
|
||
|
||
Edit **Your name** in the Multiplayer menu. Names are saved to
|
||
`settings/player.json`, transmitted to peers, exposed as `skater.name`, and drawn
|
||
above remote players. Supported characters: ASCII letters, digits, spaces,
|
||
hyphens, and underscores, up to 16 characters. Empty names display as `Player`.
|
||
|
||
See `skate.lua` for the complete annotated API. The sample mod in
|
||
`examples/game-of-skate` implements game rules using these generic observations.
|
||
|
||
|
||
## Mod menus
|
||
|
||
`sdk.ui.menu(key, {title=..., items=...})` registers or replaces a mod-owned menu
|
||
under **Mods**. Items have `id`, `label`, optional `description`, `enabled`, and
|
||
`children`. Children create nested pages; leaves emit an action only to the owner.
|
||
Limits are 8 menus/mod, 64 items/menu, 4 levels, with unique item IDs per menu.
|
||
Menus support mouse, keyboard, and controller navigation. Remove them explicitly
|
||
with `sdk.ui.remove_menu(key)`; they also disappear when the mod stops.
|
||
|
||
```lua
|
||
return {
|
||
on_load=function()
|
||
sdk.ui.menu("challenges", {title="Challenges", items={
|
||
{id="races", label="Races", children={
|
||
{id="sprints", label="Sprints", children={
|
||
{id="downtown", label="Downtown run"}
|
||
}}
|
||
}}
|
||
}})
|
||
end,
|
||
on_event=function(event)
|
||
if event.name=="menu_action" and event.menu=="challenges" then
|
||
-- The mod owns what selecting downtown does.
|
||
sdk.log("Selected "..event.item)
|
||
end
|
||
end
|
||
}
|
||
```
|
||
|
||
Callbacks run even while paused, so start/stop/settings actions can take effect
|
||
from the menu. Simulation callbacks remain paused. The S.K.A.T.E. example uses
|
||
this interface for explicit host-only **Start session** and **Stop session**;
|
||
enabling the mod registers its menu without starting a match.
|
||
|
||
## Native contacts and joint overrides
|
||
|
||
`sdk.player.read().bailing`, `.mode`, `.state`, and `.category` describe native
|
||
player state. `sdk.player.physics()` adds local `ragdoll`, `partial_ragdoll`,
|
||
`parts`, `joints`, and `contacts`, along with a simulation `tick` and `dt`.
|
||
The convenience methods `contacts()`, `joints()`, and `parts()` return those arrays.
|
||
Detailed native physics observations are local; remote mods can publish selected
|
||
effects or results through the generic network API.
|
||
|
||
Contacts are eligible solved native contact reports, capped at 128/frame. They
|
||
include both bodies, world point/normal, normal and friction force vectors,
|
||
combined static/dynamic friction coefficients, and material tags. Force uses the
|
||
native solver's post-solve conversion, not an estimate from player speed; impulse
|
||
is that force times simulation dt. These include sustained support forces, not
|
||
just new impacts. Use the tick to avoid processing a frame twice, and apply your
|
||
own threshold/history policy. Body kind `skater` carries a physical part index;
|
||
`external` includes attached proxies and target bodies. No injury is inferred by
|
||
the engine.
|
||
|
||
Joint entries expose their native names, zero-based IDs, parent/child parts,
|
||
current angle limits, and override owner. `sdk.player.set_joint(joint.index,
|
||
options)` replaces the calling mod's override. Limits are radians, 0.01–pi;
|
||
`free_swing=true, free_twist=true` removes angular limits. Also set
|
||
`drive_enabled=false` to suppress the child's animation drives. Linear attachment
|
||
constraints remain intact: the limb becomes loose, not detached. Another mod
|
||
cannot overwrite an owned joint. Overrides are applied to temporary solver input
|
||
so native tuning is preserved; `reset_joint(index)`, `reset_joints()`, mod stop,
|
||
or world change restores native behavior. Ordinary state transitions and recovery
|
||
do not erase an override; the mod decides when to restore it.
|
||
|
||
```lua
|
||
-- Example policy, entirely in Lua: relax the left elbow during a hard bail.
|
||
local last_tick
|
||
return {on_fixed_update=function()
|
||
local physics=sdk.player.physics()
|
||
if physics.tick==last_tick then return end
|
||
last_tick=physics.tick
|
||
if not sdk.player.read().bailing then return end
|
||
for _, joint in ipairs(physics.joints) do
|
||
if joint.name=="JOINT_LEFT_ARM_FOREARM" then
|
||
for _, contact in ipairs(physics.contacts) do
|
||
local a,b=contact.a,contact.b
|
||
if (a.kind=="skater" and a.index==joint.child)
|
||
or (b.kind=="skater" and b.index==joint.child) then
|
||
local f=contact.force
|
||
-- An illustrative threshold; calibrate it for your mod's behavior.
|
||
if f[1]^2+f[2]^2+f[3]^2 > 5000^2 then
|
||
sdk.player.set_joint(joint.index,
|
||
{free_swing=true,free_twist=true,drive_enabled=false})
|
||
end
|
||
end
|
||
end
|
||
end
|
||
end
|
||
end}
|
||
```
|
||
|
||
## UI updates during pause
|
||
|
||
`on_ui_update({dt, paused})` runs every presentation update with a fresh snapshot,
|
||
including while the pause menu is open. Use it to refresh menus when network roles
|
||
or other observed state changes. It does not advance `sdk.time` or simulation
|
||
timers. `sdk.capabilities.menus >= 2` advertises this callback. Keep gameplay in
|
||
`on_fixed_update`/`on_update`. Disabled menu actions show their description and do
|
||
not invoke callbacks or report a mod failure.
|
||
|
||
## Lua-defined pause sections
|
||
|
||
Pass `section` to `sdk.ui.menu` to place the menu in the main pause sidebar:
|
||
|
||
```lua
|
||
sdk.ui.menu("skate", {section="Gamemodes", title="SKATE", items={
|
||
{id="start", label="Start session"}, {id="stop", label="Stop session"}
|
||
}})
|
||
```
|
||
|
||
Section names are supplied by Lua and shared by menus with the same name. The
|
||
engine imposes no game rules. Omit `section` to use the Mods menu. Menu removal or
|
||
mod unload removes its entry; empty sections disappear. Back returns to the
|
||
section that opened the menu. Limits: 8 custom sections, 64 section menus total,
|
||
32 bytes per section name. `sdk.capabilities.menus >= 3` supports this placement.
|
||
|
||
## Local actor suspension and peer cameras
|
||
|
||
`sdk.player.suspend(true)` freezes the local native simulation, hides its skater,
|
||
and removes its multiplayer collision participation. `false` releases only the
|
||
calling mod's suspension. Mod retirement, errors, and world changes release its
|
||
ownership. Multiple mods may suspend independently. Available with
|
||
`sdk.capabilities.player_control >= 1`; skater observations expose `suspended`.
|
||
|
||
`sdk.camera.watch(peer)` mirrors the connected player's transmitted gameplay
|
||
camera transform and vertical FOV (`sdk.capabilities.camera >= 3`). Network
|
||
latency still applies. Older peers without camera packets use the body-follow
|
||
fallback. `nil` restores the native camera. This does not transfer player input.
|
||
|
||
`sdk.net.info().host_id` is the authenticated host **player** ID from the roster,
|
||
not a Steam ID or socket endpoint. It is `"0"` until the host roster arrives.
|
||
Use this player ID with `sdk.net.read`, `sdk.player.skater`, and camera watching.
|
||
|
||
The Game of SKATE example keeps its turn roster and saved position in an `origin`
|
||
record and its compact changing state in `skate`. It waits for every participant
|
||
to acknowledge suspension, then resets the active player using
|
||
`sdk.player.teleport`. Only that player's post-reset landing/bail counters count.
|
||
Late joiners wait for the next match; a missing enabled mod aborts preparation
|
||
with an actionable status. Stop/unload returns participants to their pre-match
|
||
positions and restores their cameras. Enable the same package version everywhere.
|
||
|
||
### Native player overlap (capability `player_overlap = 1`)
|
||
|
||
`sdk.physics.read(key).player_overlapping` reports whether the enabled native
|
||
board/skater collision shapes intersect the mod-owned body. Sensors require
|
||
intersection; solids include the native solver's 0.025m contact margin. This is
|
||
an explicit geometric query independent of collision masks, updated before each
|
||
fixed callback. Suspended/attached players report false. No duplicate Rapier
|
||
player is simulated. Detect transitions in Lua to count entries rather than
|
||
counting every frame of sustained contact. It is a sampled query, not a swept
|
||
trigger: use suitably thick sensor volumes for fast-moving actors.
|
||
|
||
Native foot-support, air-trajectory and camera queries include mod-created
|
||
solids, preserving nearest-hit ordering and native map behavior.
|
||
|
||
### Settled scoring details (`landed_details = 1`)
|
||
|
||
Player observations include `landed_trick_base` (engine-authored, unlocalized
|
||
base label), `landed_spin_degrees` (signed body rotation, excluding board
|
||
rotation), `landed_clean` and `landed_sketchy`. These are captured together when
|
||
`landing_seq` advances and replicated with that observation. They describe the
|
||
last settled trick; compare sequence numbers to distinguish it from old results.
|
||
Game rules and label aliases remain in Lua. The existing localized
|
||
`landed_trick` remains available for display.
|
||
|
||
See [GENERAL_API.md](GENERAL_API.md) for native bodies, joint and part overrides, contact lifecycle, input overrides, graph access and acknowledged commands.
|