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.
13 KiB
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.
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.
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.
-- 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:
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 for native bodies, joint and part overrides, contact lifecycle, input overrides, graph access and acknowledged commands.