Files
skate-3-rust-engine/sdk/skate.lua
T
Hails fda3382e9e Audio modding 2: mod cars and peds get their own MixMap instance by default
- sdk.world_audio.spawn(key, 'traffic'|'ped', ...) without slots gets its own instance on
  the private MixMap, as mod emitters do; slots = 'shared' (alias 'retail') opts into
  retail's pools. Capability world_audio 4. Engine-published objects are unchanged.
- Full private pool (16 cars + 16 peds, all mods): the nearest play, the rest in reach wait
  silently; never refused, never in retail's pools. read(key).waiting / .slots,
  info().own, a one-time log line.
- Dev mods: audio-content-test uses the default; world-audio-test asks for 'shared'
  (retail's limits) with an "Own instances" setting.
- No mod active: e2e bench byte-identical (84 / 84); suites pass (known failures only).
2026-10-04 15:15:18 -06:00

915 lines
50 KiB
Lua
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.
---@meta
-- SDK 2 language-server declarations. Not executed at runtime.
---@alias Vec3 number[]
---@alias Quat number[] xyzw
---@class DeformationOptions
---@field yield_speed? number contact impulse/body mass threshold, m/s (default 2)
---@field compliance? number metres per m/s above yield (default 0.045)
---@field radius? number impact region metres (default 1.2)
---@field max_displacement? number cumulative offset limit metres (default 0.55)
---@field max_step? number per-impact crush metres (default 0.22)
---@field cooldown? number minimum update interval seconds (default 0.10)
---@field resolution? integer[] XYZ lattice counts, default {9,5,17}; <=2048 total
---@class BodyDesc
---@field deformation? DeformationOptions
---@field shape {type:'box'|'sphere'|'capsule'|'convex'|'mesh', half_extents?:Vec3, radius?:number, half_height?:number, points?:Vec3[], path?:string, object?:string}
---@field body_type 'dynamic'|'kinematic'|'static'
---@field mass? number
---@field position? Vec3
---@field heading? number
---@field friction? number
---@field ccd? boolean
---@field sensor? boolean
---@field membership? integer
---@field filter? integer
---@field center_of_mass? Vec3 body-local COM (weight transfer under corner loads)
---@field collider_offset? Vec3 chassis-local collider translation (legacy vehicle.json)
---@field inertia_half_extents? Vec3 optional inertia box; with COM sets MassProperties
---@field linear_damping? number default 0.08
---@field angular_damping? number default 0.5
---@class SpringRayOpts
---@field local_origin Vec3
---@field local_direction? Vec3 default {0,-1,0}
---@field rest_length number
---@field max_travel? number
---@field contact_radius? number
---@field stiffness number
---@field compression? number
---@field relaxation? number
---@field damping? number alias for compression/relaxation
---@field max_force? number
---@field dt number
---@class SpringRayHit
---@field in_contact boolean
---@field point Vec3
---@field normal Vec3
---@field hard_point Vec3
---@field direction_ws Vec3
---@field suspension_length number
---@field load number
---@field relative_velocity number
---@class BodySnapshot
---@field player_overlapping boolean Enabled native local board/skater volumes touching this body; includes sensors. Solid contact margin 0.025m; sensors require intersection. Updated each fixed callback, false while suspended/attached. Geometric query only; collision masks do not suppress it.
---@field position Vec3
---@field rotation Quat
---@field linvel Vec3
---@field angvel Vec3
---@field force Vec3 last sdk.physics.force this tick (zeros if none); Rapier user forces are cleared each tick
---@field torque Vec3 last sdk.physics.torque this tick
---@field mass number
---@field speed number |linvel|
---@class PlayerSnapshot
---@field landed_trick_base string Engine-authored base label identifier (not localized), captured with landing_seq
---@field landed_spin_degrees integer Signed body rotation from the settled scorer; excludes board shuvit rotation
---@field landed_clean boolean Quality captured at the confirmed landing
---@field landed_sketchy boolean Sketchy quality captured at the confirmed landing
---@field suspended boolean local or remote simulation/visibility suspension
---@field id? string
---@field local? boolean
---@field name? string display name from the multiplayer menu
---@field position Vec3
---@field velocity Vec3
---@field angvel Vec3
---@field forward Vec3
---@field heading number
---@field rotation Quat
---@field speed number
---@field on_board boolean
---@field state integer
---@field category integer
---@field filtered integer
---@field mode string ground|air|grind|offboard|offboard_air|bail|teleport
---@field grind string|nil
---@field bailing boolean
---@field trick string HUD trick name; empty when idle
---@field trick_seq integer increments each newly announced trick; NOT a landing
---@field landing_seq integer monotonic confirmed, banked on-board sequence counter
---@field landed_trick string last confirmed landing label; persists until another landing
---@field bail_seq integer monotonic wipeout-entry counter
---@field new_trick boolean true for the announce frame
---@field modified_trick boolean
---@field close_tricks boolean
---@field sequence boolean combo/line still live
---@field score number
---@field line number
---@field multiplier number
---@field line_time number
---@field clean boolean
---@field sketchy boolean
---@field switch boolean
---@field fakie boolean
---@field nollie boolean
---@field intents table<string, number>
---@class ContactEvent
---@field a string|nil body key or `"ground"` when the other side is owned
---@field b string|nil body key or `"ground"`
---@field started boolean true on pair begin, false on end
---@class TouchingPair
---@field a string|nil body key or `"ground"`
---@field b string|nil body key or `"ground"`
---@class PadSnapshot
---@field buttons integer XInput button bits
---@field triggers number[] LT, RT in 0..1
---@field left number[] stick XY
---@field right number[] stick XY
---@class NetworkInfo
---@field active boolean
---@field local_id string
---@field is_host boolean
---@field host_id string
---@field players string[]
---@field states table<string, table<string, table<string, any>>> mod_id → peer_id → key → value
---@field status string
---@class SDKSnapshot
---@field player PlayerSnapshot
---@field skaters table<string, PlayerSnapshot>
---@field map {name:string, generation:integer}
---@field tick integer
---@field keys table<string,boolean>
---@field actions number[]
---@field pad PadSnapshot
---@field paused boolean
---@field replay boolean
---@field camera? {position:Vec3}
---@field attach? {body:string, owner:string}
---@field physics {bodies:table<string,BodySnapshot>, contacts:ContactEvent[], touching:TouchingPair[]}
---@field network? NetworkInfo
---@class ModCallbacks
---@field on_load? fun()
---@field on_unload? fun()
---@field on_ui_update? fun(event:{dt:number,paused:boolean}) runs while paused; does not advance simulation timers (menus >= 2)
---@field on_update? fun(event:{dt:number})
---@field on_fixed_update? fun(event:{dt:number})
---@field on_settings? fun(event:{key:string,value:any})
---@field on_event? fun(event:{name:string})
sdk = {
api_version = 2,
---@type string
mod_id = "",
---@type table<string, any>
settings = {},
---@type SDKSnapshot
snapshot = {},
physics = {}, graphics = {}, player = {}, camera = {}, input = {}, ui = {}, net = {}, assets = {},
time = { elapsed = 0 },
}
---@param text string
function sdk.log(text) end
---@param path string
---@return string
function sdk.read_text(path) end
---@param path string package-relative .glb
---@return {nodes:string[], meshes:string[]}
function sdk.assets.objects(path) end
---@param key string
---@param body BodyDesc
function sdk.physics.spawn(key, body) end
---@param key string
function sdk.physics.remove(key) end
---@param key string
---@param opts {shape:{type:'mesh'|'convex', path?:string, object?:string, points?:Vec3[]}, position?:Vec3, friction?:number}
function sdk.physics.add_collider(key, opts) end
---@param key string
---@param force Vec3
---@param point? Vec3
function sdk.physics.force(key, force, point) end
---@param key string
---@param impulse Vec3
---@param point? Vec3
function sdk.physics.impulse(key, impulse, point) end
---@param key string
---@param torque Vec3
function sdk.physics.torque(key, torque) end
---@param key string
---@param torque Vec3
function sdk.physics.torque_impulse(key, torque) end
---@param origin Vec3
---@param direction Vec3
---@param opts? {max_distance?:number, filter?:'all'|'ground', exclude?:string[]}
---@return {body:string|nil, point:Vec3, normal:Vec3, toi:number}|nil
function sdk.physics.raycast(origin, direction, opts) end
---@param key string
---@param point Vec3
---@return Vec3|nil
function sdk.physics.velocity_at(key, point) end
---@param key string
---@param point Vec3
---@param direction Vec3
---@return number|nil
function sdk.physics.effective_inv_mass(key, point, direction) end
---@param key string
---@param opts SpringRayOpts
---@return SpringRayHit|nil
function sdk.physics.spring_ray(key, opts) end
---@param key string
---@param local_accel Vec3
---@param dt? number
---@return Vec3|nil
function sdk.physics.local_ang_accel_impulse(key, local_accel, dt) end
---@param key string
---@param linvel Vec3
function sdk.physics.set_linvel(key, linvel) end
---@param key string
---@param angvel Vec3
function sdk.physics.set_angvel(key, angvel) end
---@param key string
---@param position Vec3
---@param rotation Quat
function sdk.physics.set_pose(key, position, rotation) end
---@param key string
---@param joint {body_a:string, body_b:string, anchor_a:Vec3, anchor_b:Vec3, axis?:Vec3, limits?:[number,number], contacts_enabled?:boolean}
function sdk.physics.revolute(key, joint) end
---@param key string
---@param joint {body_a:string, body_b:string, anchor_a?:Vec3, anchor_b?:Vec3, axis?:Vec3, limits?:[number,number], contacts_enabled?:boolean}
function sdk.physics.prismatic(key, joint) end
---@param key string
---@param motor {mode:'velocity', target_velocity:number, factor?:number, max_force?:number}|{mode:'position', target_position:number, stiffness:number, damping:number, max_force?:number}
function sdk.physics.joint_motor(key, motor) end
---@param key string
---@param spring {position?:number, target_position?:number, stiffness:number, damping:number, max_force?:number}
function sdk.physics.joint_spring(key, spring) end
---@param key string
function sdk.physics.remove_joint(key) end
---@param key string
---@return BodySnapshot|nil
function sdk.physics.read(key) end
---@return ContactEvent[] edge events from the previous Rapier step
function sdk.physics.contacts() end
---@return TouchingPair[] pairs in contact after the previous Rapier step (`ground` included)
function sdk.physics.touching() end
---@param key string
---@param opts {path?:string, body?:string, position?:Vec3, rotation?:Quat, scale?:Vec3, color?:Vec3, visible?:boolean, opacity?:number}
-- opts.deform_nodes: optional string[] of GLB scene node names whose meshes follow the bound body deformation.
function sdk.graphics.mesh(key, opts) end
---@param key string
function sdk.graphics.remove(key) end
---@param key string
---@param visible boolean
function sdk.graphics.set_visible(key, visible) end
---@class MeshBufferOpts
---@field body? string
---@field position? Vec3 body-local when bound, world when unbound
---@field rotation? Quat
---@field scale? Vec3
---@field blend? boolean default true
---@field unlit? boolean default true
---@field visible? boolean default true
---@field depth_bias? number decal bias, default 0
---@field texture? string mod-relative PNG path, e.g. textures/skid_tread.png
---@field capture? string named camera capture; mutually exclusive with texture
---@field tint? Vec3 material tint, default white
---@class MeshBufferWrite
---@field positions Vec3[]
---@field normals? Vec3[]
---@field colors? number[][] per-vertex RGBA (packed flat by the runtime)
---@field uvs? number[][]
---@field indices integer[] 0-based triangle indices
---@class LightOpts
---@field kind? "point"|"spot"
---@field body? string
---@field position? Vec3
---@field offset? Vec3
---@field direction? Vec3 spot axis
---@field color? Vec3
---@field intensity? number
---@field range? number
---@field inner_angle? number
---@field outer_angle? number
---@param key string
---@param opts? MeshBufferOpts
function sdk.graphics.mesh_buffer(key, opts) end
---@param key string
---@param data MeshBufferWrite
function sdk.graphics.mesh_buffer_write(key, data) end
---@param key string
---@param data MeshBufferWrite append-only delta; indices are absolute in the combined mesh
function sdk.graphics.mesh_buffer_append(key, data) end
---@param key string
---@param opts? LightOpts
function sdk.graphics.light(key, opts) end
---@return PlayerSnapshot
function sdk.player.read() end
---Freeze and hide the local skater and remove their collision participation.
---False releases only this mod's suspension; unload/world change also releases it.
---@param suspended boolean
function sdk.player.suspend(suspended) end
---@return table<string, PlayerSnapshot>
function sdk.player.skaters() end
---@param id string|number
---@return PlayerSnapshot|nil
function sdk.player.skater(id) end
---@param body string
---@param offset? Vec3
function sdk.player.attach(body, offset) end
function sdk.player.detach() end
---@return string|nil
function sdk.player.attached() end
---@param body? string
---@param offset? Vec3
function sdk.camera.follow(body, offset) end
function sdk.camera.clear_follow() end
---@param position Vec3
---@param look_at? Vec3
function sdk.camera.set(position, look_at) end
---@param peer string|number|nil peer whose actual camera transform/FOV to mirror; nil restores native camera
function sdk.camera.watch(peer) end
---@param key string
---@return boolean
function sdk.input.down(key) end
---@param id integer
---@return number
function sdk.input.action(id) end
---@return PadSnapshot
function sdk.input.pad() end
---@param key string
---@param text string
function sdk.ui.text(key, text) end
---Local-only text in Pause > Multiplayer > Debug; never draws a gameplay overlay.
---Up to 8 entries per mod, 1024 UTF-8 bytes each. Empty text removes an entry.
---@param key string
---@param text string
function sdk.ui.multiplayer_debug(key, text) end
---@return NetworkInfo
function sdk.net.info() end
---@return string[]
function sdk.net.players() end
---@param key string
---@param value any JSON-compatible; at most 512 encoded bytes; nil clears
function sdk.net.publish(key, value) end
---@param peer string|number peer id from info().local_id / remote peers
---@param key string
---@return any|nil
function sdk.net.read(peer, key) end
---@class TeleportOptions
---@field position Vec3 world position, each coordinate within +/-100000
---@field heading? number radians, default 0
---@field velocity? Vec3 world linear velocity, each component within +/-200
---@param options TeleportOptions moves only the local player through native travel
function sdk.player.teleport(options) end
sdk.session = {}
---@return {active:boolean, local_id:string, is_host:boolean, authority:string, players:string[]}
function sdk.session.info() end
---The transport host may reclaim authority; the current authority may renew it.
function sdk.session.claim() end
---@param peer string|number connected peer; transfer is ratified asynchronously by host
function sdk.session.transfer(peer) end
---@param peer string|number connected peer; requires session authority
---@param options TeleportOptions
function sdk.session.teleport(peer, options) end
sdk.volumes = {}
---@param key string mod-owned id; at most 32 boxes per mod
---@param options {position:Vec3, size:Vec3, rotation?:Quat, visible?:boolean, color?:Vec3, opacity?:number}
function sdk.volumes.box(key, options) end
---@param key string
function sdk.volumes.remove(key) end
---@param key string
---@return {position:Vec3,size:Vec3,rotation:Quat,inside:string[]}|nil point overlaps, not physical collisions
function sdk.volumes.read(key) end
---@param key string mod-owned id; at most 2 per mod, 4 total
---@param options {position:Vec3,look_at:Vec3,fov?:number,width?:integer,height?:integer} fov radians [0.2,2.5]; sizes [64,512], multiples of 16
function sdk.camera.capture(key, options) end
---@param key string releases the named target; bound meshes lose their image
function sdk.camera.clear_capture(key) end
---@class MenuItem
---@field id string unique within this menu
---@field label string
---@field description? string
---@field enabled? boolean default true
---@field children? MenuItem[] submenu, up to 4 levels
---@param key string mod-owned id; up to 8 menus per mod
---@param options {title:string,section?:string,items:MenuItem[]} up to 64 total items
---Actions dispatch on_event {name="menu_action",menu=key,item=item.id} to the owner, including while paused.
function sdk.ui.menu(key, options) end
---@param key string
function sdk.ui.remove_menu(key) end
---@class NativeContact
---@field a {kind:string,index?:integer} board, skater, external, or world
---@field b {kind:string,index?:integer}
---@field point Vec3 world contact point
---@field normal Vec3 native A-side normal
---@field force Vec3 native solved normal plus friction force
---@field normal_force Vec3
---@field friction_force Vec3
---@field impulse Vec3 force times simulation dt
---@field static_friction number combined contact coefficient
---@field dynamic_friction number combined contact coefficient
---@field material_tags integer[] A and B material tags
---@field id string stable body pair, shared by manifold points
---@field phase 'begin'|'stay'|'end'
---@field relative_velocity_before_solve Vec3 A minus B at contact point, including angular velocity
---@field closing_speed number m/s; zero on end
---@class NativeJoint
---@field index integer zero-based native joint ID (use this, not Lua array index)
---@field name string native authored joint name
---@field parent integer native part index
---@field child integer native part index
---@field swing_limit number radians
---@field twist_limit number radians
---@field free_swing boolean
---@field free_twist boolean
---@field drive_enabled boolean false when a mod suppresses the child's animation drives
---@field override_owner? string
---@field enabled boolean
---@field possession_enabled boolean
---@field load? NativeJointLoad most recent eligible solved row
---@field parameters integer[] native words, read-only
---@field frames integer[] native words, read-only
---@return {tick:integer,dt:number,contacts:NativeContact[],joints:NativeJoint[],parts:table[],ragdoll:boolean,partial_ragdoll:boolean} local player only
function sdk.player.physics() end
---@return NativeContact[] most recent solved frame, max 128 active plus end reports to reach 256
function sdk.player.contacts() end
---@return NativeJoint[]
function sdk.player.joints() end
---@return {index:integer,position:Vec3,velocity:Vec3,angvel:Vec3,inverse_mass:number}[]
function sdk.player.parts() end
---@param joint integer zero-based index from sdk.player.joints()
---@param options JointOverride angles [0.01,pi], radians
---Replaces this mod's override; omitted properties follow native state. Another mod cannot take an owned joint.
function sdk.player.set_joint(joint, options) end
---@param joint integer restores this mod's joint override
function sdk.player.reset_joint(joint) end
---Restores all joint AND part overrides owned by this mod.
function sdk.player.reset_joints() end
-- General engine access: see GENERAL_API.md for units, lifecycle and limitations.
sdk.commands = {}
---@class CommandResult
---@field token integer latest request token for this key
---@field ok boolean host execution succeeded; not remote acknowledgement
---@field error? string
---@field value? any catalog for engine_inspect, otherwise nil
---@field tick integer
---@param key string at most 64 result keys per mod
---@param command table validated native command; no nested request
---@return integer token
function sdk.commands.request(key, command) end
---@param key string
---@return CommandResult|nil nil until the latest token is observed
function sdk.commands.result(key) end
sdk.engine = {version=1}
---@return string[]
function sdk.engine.systems() end
---@param system string player, rig, bodies, input, graphs, animation, scoring, world, camera, network, commands
---@return table|nil latest snapshot
function sdk.engine.read(system) end
---@param key string command result key
---@param system 'graphs'|'scoring'|'audio_catalog'|string `audio_tuning:<domain>[/path]` reads a tuning domain
function sdk.engine.inspect(key, system) end
---@class BodyReference
---@field kind 'skater'|'board'|'mod'
---@field index? integer zero-based native physical body ID
---@field key? string mod-owned Rapier body key
---@class NativeBody: BodyReference
---@field position Vec3 world centre of mass
---@field rotation Quat XYZW
---@field velocity Vec3 world m/s
---@field angvel Vec3 world rad/s
---@field inverse_mass number
---@field inverse_inertia Vec3[] world matrix columns
---@field state_flags integer effective native solve state
---@field name? string mapped animation bone name
---@field joint? integer native joint whose child is this body
---@field collision_enabled? boolean effective enabled flag
---@field animation_drives? boolean permits native animation drives
---@field possession_drives? boolean permits native board-holding drives
---@field material? {static_friction:number,dynamic_friction:number}
---@field override? {owner:string,options:PartOverride}
sdk.bodies = {}
---@param ref BodyReference
---@return NativeBody|table|nil
function sdk.bodies.read(ref) end
---@param ref BodyReference
---@param value Vec3 world N s
---@param point? Vec3 world point; omitted means centre of mass
function sdk.bodies.impulse(ref, value, point) end
---@param ref BodyReference
---@param value Vec3 world N m s
function sdk.bodies.angular_impulse(ref, value) end
---@class JointOverride
---@field swing_limit? number [0.01,pi] radians
---@field twist_limit? number [0.01,pi] radians
---@field free_swing? boolean
---@field free_twist? boolean
---@field enabled? boolean false removes entire constraint including linear attachment
---@field drive_enabled? boolean false suppresses child animation drives
---@field possession_enabled? boolean false suppresses child board-holding drives
---@field descendants? boolean extends drive suppressions to child subtree
---@class PartOverride
---@field motion? 'dynamic'|'frozen'|'static' solve flags 4/2/1; not a player state change
---@field collision? boolean
---@field friction? number [0,10], both material coefficients
---@field animation_drives? boolean
---@field possession_drives? boolean
---@class NativeJointLoad
---@field linear_impulse Vec3 world N s on child/A
---@field angular_impulse Vec3 world N m s, direct angular couple
---@field force Vec3 N
---@field torque Vec3 N m, excluding linear anchor lever-arm contribution
---@field solver_words integer[] packed native u32 values
---@class NativeRig
---@field tick integer completed solve cursor
---@field dt number seconds
---@field parts NativeBody[]
---@field board NativeBody[]
---@field joints NativeJoint[]
---@field contacts NativeContact[] at most 128 active, plus end reports to reach 256
---@field contacts_truncated boolean observation rows were omitted; pair tracking continues
---@field ragdoll boolean native mode
---@field partial_ragdoll boolean native mode
sdk.rig = {}
---@return NativeRig
---@param fields? string[] Optional top-level rig fields; omit for the complete rig.
function sdk.rig.read(fields) end
---@return NativeBody[]
function sdk.rig.parts() end
---@return NativeJoint[]
function sdk.rig.joints() end
---@return NativeContact[]
function sdk.rig.contacts() end
---@param index integer native joint ID, not a Lua array index
---@param options JointOverride replacement override; omitted fields follow native settings
function sdk.rig.configure_joint(index, options) end
---@param index integer native joint ID
function sdk.rig.reset_joint(index) end
---@param index integer physical skater part ID
---@param options PartOverride
function sdk.rig.configure_part(index, options) end
---@param index integer physical skater part ID
function sdk.rig.reset_part(index) end
---Restores all joint AND part overrides owned by this mod.
function sdk.rig.reset() end
---@param id integer mapped gameplay action 64..81
---@param value? number [-1,1]; nil restores normal input
function sdk.input.override_action(id, value) end
sdk.graphs = {}
---@param graph 'action'|'motion'
---@return {current?:integer,previous?:integer,name?:string,dt:number,state_times:table,active_behaviors:integer[]}|nil
function sdk.graphs.read(graph) end
---@param graph 'action'|'motion'
---@param target 'state'|'transition'|'behavior'
---@param index integer runtime ID from the catalog; zero-based
---@param enabled? boolean nil restores the original gate
function sdk.graphs.set_enabled(graph, target, index, enabled) end
---Feature discovery: extension name → version, compiled into the engine (an older engine lacks
---newer keys, so test `(sdk.capabilities.audio or 0) >= 1` before relying on a feature). The
---manifest API stays 2. Includes among others `audio`, `world_audio`, `engine_access`,
---`command_results`, `player_physics`, `menus`, `camera`, `volumes`, `capture`.
---@type table<string, integer>
sdk.capabilities = {}
-- Audio extension 1 (capability `audio`): the mod's own sounds. Keys and files belong to the calling
-- mod. Files: mod-relative PCM16 WAV (`.wav`, no `..`, `\`, `:`, `#`, `?`), 1–2 channels, 8–48 kHz,
-- at most 30 s and 8 MiB each; metadata chunks are dropped. Limits: 32 clips / 32 MiB / 32 voices per
-- mod, 128 / 128 MiB / 128 in all. By default (audio >= 3) a voice plays through the game's native
-- mixer (see `native`); a Bevy voice (`native = false`, or no free native voice) plays outside the
-- game's native audio engine: master volume and `--mute` apply, but no retail reverb, distance curves
-- or ducking. They pause while the
-- game is paused or a replay runs, and stop when the mod is disabled, reloaded or fails (clips are
-- released then too). A voice bound to a body stops when that body is removed.
---@class AudioPlayOptions
---@field path string mod-relative PCM16 WAV
---@field body? string follow one of this mod's physics bodies (exclusive with position)
---@field position? Vec3 world position of an unbound voice (default {0,0,0})
---@field offset? Vec3 added to the body pose or the position, -100..100 m per axis (default {0,0,0})
---@field loop? boolean default false
---@field volume? number 0..1 (default 1)
---@field pitch? number playback speed 0.25..4: changes pitch and duration (default 1)
---@field spatial? boolean positional (default true); false plays at the listener
---@field spatial_scale? number 0.001..1, world metres → spatial units (default 0.1)
---@field paused? boolean start paused (default false)
---@field fade_in? number seconds 0..2 (default 0.01)
---@field native? boolean audio >= 3: the game's native mixer. **Default (nil): native** while the game's
---native audio runs and one of its 24 native voices is free, else the Bevy voice above; `true` = native
---only (an error without it: use `sdk.commands.request` to get it as a result); `false` = always the
---Bevy voice. The WAV joins a per-mod bank; the voice follows the retail world-emitter law: dry level
---and pan from a MixMap Emitter instance, the environment (reverb) send rolling off with camera
---distance 4 → 70 m, and the sound's own reach (`falloff`, the `.ems` record test). Category volume:
---`group`. 24 native voices in all (they count in the 32 / 128 voice limits and share the mixer with
---the game). `spatial_scale` is for the Bevy voice only (ignored by a native one).
---@field falloff? AudioFalloff native, positional: the reach (default {radius = 40, curve = 'squared'},
---retail's traffic-car reach: the vehicle list is cut at 40 m). Refused with `native = false`.
---@field reverb? boolean native: send into the environment bus (default true, as retail emitters)
---@field group? 'world'|'player' native: Ambience volume (`world`, default) or Effects volume
---@class AudioFalloff
---@field radius number reach in metres (0.1..10000): silent at and beyond it
---@field core? number inner fraction 0..1 at full level (default 0)
---@field curve? 'squared'|'linear'|'flat' retail eVolumeFalloffType 0 / 1 / 2 (default 'squared')
---@class AudioUpdateOptions
---@field volume? number 0..1
---@field pitch? number 0.25..4
---@field paused? boolean
---@field position? Vec3 unbound voices only
---@field offset? Vec3
---@type {version:integer}
sdk.audio = { version = 3 }
---Load and validate a WAV now so the first play doesn't read it. A bad file, an unknown body or a
---full limit fails the mod (as `play` does); wrap the command in `sdk.commands.request` to get the
---error as a result instead.
---@param path string mod-relative PCM16 WAV
function sdk.audio.preload(path) end
---Start a voice under `key`, replacing any voice this mod already plays under that key.
---@param key string
---@param opts AudioPlayOptions
function sdk.audio.play(key, opts) end
---Change a playing voice; never restarts it. Unknown or finished keys are ignored. Volume and pitch
---changes are smoothed (≈ 12 ms / 25 ms).
---@param key string
---@param opts AudioUpdateOptions
function sdk.audio.update(key, opts) end
---Stop a voice with a fade (seconds, default 0.03; 0 = at once). Repeated stops don't extend it.
---@param key string
---@param fade_out? number
function sdk.audio.stop(key, fade_out) end
---Stop every voice of this mod at once (the clips stay loaded).
function sdk.audio.stop_all() end
-- Audio extension 2 (capability `audio` >= 2): the game's own native audio, the same calls engine
-- systems make. Keys belong to the calling mod: 32 handles per mod, 128 in all, 16 posts per frame.
-- Posts and globals are applied at the start of the next audio pass. Everything is released and
-- every global restored when the mod stops, fails or reloads; posts also end at a map change (the
-- handle reads `live = false`; post again on `world_changed`) and when the game's sound restarts
-- for an audio content change. A post runs the retail bank's program, which draws from the one
-- random generator every retail post uses: with a mod posting, the retail random sequence differs.
---@class AudioHandle
---@field live boolean the post is held by the running audio (false after a map change / restart)
---@field class string the class it was posted to
---@class AudioMixMapRow
---@field slot string
---@field object integer
---@field instance integer
---@field output integer
---@field level integer 0..32767 (a Q15 level, or a filter cutoff in Hz)
---@field raw integer 0..65535 (an azimuth: 65536 = 360°)
---@field pitch integer 4096 = 1.0
---@field half integer the output word as stored
---@class AudioWatchKey
---@field slot "global"|"player"|"ambience"|"collision"|"traffic"|"pedestrian"|"emitter"
---@field object? integer 0..127 (default 0)
---@field instance? integer 0..31 (default 0)
---@field output integer 0..31
---@class AudioWatchOptions
---@field globals? string[] up to 16 retail globals
---@field mixmap? AudioWatchKey[] up to 16 MixMap outputs
---@class AudioEvent
---@field kind "post"|"release"|"splice"|"emitter_start"|"emitter_stop"|"zone"|"speech"
---@field source "player"|"world"|"npc"|"emitter"|"ambience"|"speech"
---@field class string retail class (posts), bank (Splice starts, emitters), "speech" / "maincast" (speech lines) or ""
---@field slot string the poster's slot (`grind`, `footstep`, `horn`, `ped_tazer`, `body_fall`, `ring`, …) or ""
---@field id integer Splice sound id, emitter patch, slot index, speech event
---@field owner string world / NPC object, zone key, speaker ("0" for the local player)
---@field tag? "pop"|"land"|"grind_start"|"grind_end"|"footstep"|"horn"|"alarm"|"tazer"|"body_fall"|"emitter"|"zone_change"|"speech"
---@class AudioInfo
---@field native boolean the native audio runtime runs
---@field map_epoch? integer
---@field generation? integer audio content generation (bumped by every content change: a swap or a restart)
---@field restarts? integer runtime restarts (only where a change cannot be swapped in place: the MixMap file, the rolling bed's grains)
---@field swaps? integer audio content changes swapped into the running audio without a restart (audio_content >= 3)
---@field last_change? string the last content change: "swap", or "restart: <reasons>"
---@field mixmap_inputs? integer MixMap inputs written by mods (audio >= 4)
---@field seed? {owner:string, seed:integer} the audio random state's seed in force (audio >= 4)
---@field overlays? string[] mods whose audio.json is applied
---@field conflicts? integer
---@field map? {stem:string, district:string, ems:string[], sources:string[]}
---@field limits table
---@field tags string[]
---Post to a retail class (e.g. `c_emitter`) with up to 32 payload words; a key's post replaces its
---last one. An unknown class is a command error (use `sdk.commands.request` to get it as a result).
---@param key string
---@param class string
---@param words? integer[]
function sdk.audio.post(key, class, words) end
---Rewrite a held post's payload.
---@param key string
---@param words integer[]
function sdk.audio.redeliver(key, words) end
---@param key string
function sdk.audio.release(key) end
---Set a retail global; `nil` restores the value seen before this mod's first write. The first mod
---to set a global owns it; it is restored when that mod stops and at a map change.
---@param name string
---@param value? integer
function sdk.audio.set_global(name, value) end
---Replace this mod's watch lists (read with `sdk.audio.global` / `sdk.audio.mixmap`).
---@param opts AudioWatchOptions
function sdk.audio.watch(opts) end
---@param key string
---@return AudioHandle|nil
function sdk.audio.handle(key) end
---A watched global's value after the last audio pass (or the value this mod set).
---@param name string
---@return integer|nil
function sdk.audio.global(name) end
---A watched MixMap output after the last audio pass.
---@return AudioMixMapRow|nil
function sdk.audio.mixmap(slot, object, instance, output) end
---@return AudioInfo
function sdk.audio.info() end
-- Audio events (capability `audio_events`): observe only, one frame late, at most 256 rows a frame.
-- Nothing is recorded while no mod subscribes.
---Subscribe to the rows with these tags (`{tags={}}` = every row); `nil` stops.
---@param opts? {tags:string[]}
function sdk.audio.subscribe(opts) end
---The rows of the last frame not returned yet (each frame's rows once).
---@return AudioEvent[]
function sdk.audio.events() end
-- Rules (capability `audio_events` >= 2): mute / replace / layer the game's own sounds where they are
-- posted, the same frame (Lua can't run in the audio pass, so rules are declarative). Sites: the local
-- player's component posts and Splice starts (pops, landings, foley), the world / NPC hosts' posts and
-- Splice starts, the world emitters' starts. `mute`: the request is dropped (a post is not made: its
-- updates and release do nothing; a Splice sound does not start; an emitter keeps its state silently);
-- `replace`: dropped + `play`; `layer`: kept + `play`. The rule's sound plays where the game's would
-- (at its owner, following it) unless `play.at` says otherwise. The first matching rule decides (mods
-- in mod-id order). Event rows still report muted requests. 32 rules per mod, 64 in all; removed when the mod
-- stops. Static rules also go in audio.json `rules` (capability audio_content >= 2).
---@class AudioRuleMatch
---@field tag? 'pop'|'land'|'grind_start'|'grind_end'|'footstep'|'horn'|'alarm'|'tazer'|'body_fall'|'emitter'
---@field kind? 'post'|'splice'|'emitter_start'
---@field source? 'player'|'world'|'npc'|'emitter'
---@field class? string a retail class (posts) or bank (Splice starts, emitters)
---@field slot? string the poster's slot (grind, wind, footstep, horn, ped_footstep, ...)
---@field id? integer slot index (posts), sound id (Splice), patch (emitters)
---@class AudioRulePlay
---@field path string mod-relative PCM16 WAV, played through the native mixer as a one-shot
---@field volume? number 0..1 (default 1)
---@field pitch? number 0.25..4 (default 1)
---@field reverb? boolean environment send (default true)
---@field group? 'player'|'world' Effects (default) or Ambience volume
---@field at? 'owner'|'world'|'centre' where it plays (default 'owner'): `owner` = at the owner of the
---replaced / layered sound (the local skater's centre of mass, the car or ped, the NPC skater, the
---emitter), following it while it plays (an owner that is gone leaves it where it was; one never found
---plays it centred); `world` = at the fixed `position`; `centre` = non-positional, centred (the retail
---non-positional emitter outputs). Positional sounds use the retail emitter law (MixMap Emitter dry
---level, pan, reverb send rolling off with camera distance) and a reach (`falloff`).
---@field offset? Vec3 `owner` only: metres added to the owner's position, -100..100 (world axes, y up; with `frame = 'owner'` the owner's axes)
---@field frame? 'world'|'owner' `owner` only (audio_events >= 3): the axes of `offset`: 'world' (default) or 'owner' = x its right, y up, z its facing (the board's nose for skaters, a car's direction, a ped's walking direction, an emitter's forward), turning with it. A rule sound at a published emitter follows it when it moves.
---@field position? Vec3 `world` only (required there): the world position
---@field falloff? AudioFalloff positional only: the reach; default the owner's retail reach: the emitter
---record's own shape and curve, 40 m for cars (retail's traffic list), 50 m for peds (the ped list),
---30 m for skaters (the local player too: retail's skater audio radius); squared curve
---@class AudioRule
---@field match AudioRuleMatch at least one field; all given fields must hold
---@field action 'mute'|'replace'|'layer'
---@field play? AudioRulePlay required for replace / layer
---@field min_interval? number seconds between two plays of the rule's sound (default 0.05)
---Set (a table) or remove (`nil`) this mod's rule `key`. A bad rule or WAV is a command error.
---@param key string
---@param rule AudioRule|nil
function sdk.audio.rule(key, rule) end
-- Audio extension 4 (capability `audio` >= 4).
---Write (or with `value = nil` release) one input (0..15) of a retail MixMap controller: drive the
---game's own controllers (the Master category gains `'global', 2, 0, 1..4`, the duck flags of the
---Global objects, an instance's inputs) through retail's curves and envelopes. Applied every audio
---pass after the game's own writes, right before the evaluations, so it holds against inputs the game
---writes too. The first mod to write an input owns it; 16 per mod, 64 in all; an unknown controller is
---a command error. Released (the value before the first write comes back) by nil, when the mod stops,
---fails or reloads; kept across map changes, written again after an audio restart.
---@param slot 'global'|'player'|'ambience'|'collision'|'traffic'|'pedestrian'|'emitter'
---@param object integer 0..127
---@param instance integer 0..31
---@param input integer 0..15
---@param value integer|number|nil an integer word (or with `opts.float` an f32, the distance inputs)
---@param opts? {float?:boolean}
function sdk.audio.set_mixmap_input(slot, object, instance, input, value, opts) end
---The MixMap inputs this mod writes.
---@return {slot:string, object:integer, instance:integer, input:integer, value:number}[]
function sdk.audio.mixmap_inputs() end
---Seed the audio random state for reproducible tests: every audio generator (programs, Splice picks,
---grain picks, eEQChain rolls, Jitter, the world and speech hosts) is set from `n` at the start of the
---next audio pass, so the same seed at the same point draws the same. One owner (the first mod);
---`nil` (or the mod stopping) puts back the states the generators had at the first seed. Unseeded the
---draws are retail's, unchanged. `SKATE_AUDIO_SEED=<n>` seeds a whole run.
---@param n integer|nil
function sdk.audio.seed(n) end
-- Audio content (capability `audio_content`): a mod ships `audio.json` at its root (no Lua needed):
-- replace / add retail audio content by identity (banks, sample slots, Splice trees, grain members,
-- wheel streams, ambience beds, emitter records, location sets, zones, crossfades, speech takes,
-- tuning fields, map audio, Csis projects) with its own files. Applied only while the mod runs.
-- Capability audio_content >= 3: turning the mod on or off, or editing audio.json / its files while
-- it runs, is swapped into the running audio in place (only what changed is replaced; held sounds
-- continue on the new content); only a MixMap or grain change restarts the game's sound (a short
-- cut). `add.projects` = the mod's own `.csi` projects (new classes, functions, globals; names must
-- not be the install's or another mod's). Two mods on one identity: the first by mod id wins
-- and the mod menu shows the conflict. Check it with `check_mod <package> --install <assets>`.
-- Reference: docs/hails-additions/16-audio-modding.md.
-- Audio tuning (capability `audio_tuning`): patch the game's typed tuning while this mod runs.
-- Domains: 'player' (player_tuning: surfaces, grinds, seams, landing / collision materials, tricks,
-- treatment, contacts), 'world' (world_tuning: traffic engine records, ped footsteps / objects,
-- speech events and voice), 'bus' (bus_tuning: reverb presets, eEQChain buses, FlangeSub returns),
-- 'reverb' (bus_tuning.reverb: preset key → its 44 values by index). A patch is a field merge
-- (tables by field, arrays by decimal index `{['3'] = 0.5}`, leaves of the same type: numbers,
-- booleans, strings, number arrays of the same length); a field the install lacks or another type is
-- a command error. Applied between audio passes; the first mod to write a field owns it (another
-- mod's write of it is an error); restored when the mod stops, fails or reloads. Kept across map
-- changes. The MixMap's layout is not tunable.
---Set (a table) or restore (`nil`) this mod's patch of a tuning domain.
---@param domain 'player'|'world'|'bus'|'reverb'
---@param patch table|nil
function sdk.audio.set_tuning(domain, patch) end
---Request a domain (or a path inside it, e.g. 'traffic_engine/c04_taxi01') as the game uses it now;
---read it as `sdk.commands.result(key).value` (at most 256 KiB: read a path for big domains).
---@param key string command result key
---@param domain 'player'|'world'|'bus'|'reverb'
---@param path? string
function sdk.audio.tuning(key, domain, path) end
---The tuning fields this mod owns, as applied at the last audio pass ('world_tuning/traffic_engine/c04_taxi01/idle_rpm', …).
---@return string[]
function sdk.audio.tuned() end
-- World audio extension 1 (capability `world_audio`): publish traffic vehicles, pedestrians and
-- skaters to the game's retail world audio, exactly as an engine system would (doc
-- docs/hails-additions/15-world-audio.md). Keys belong to this mod; 48 objects per mod, 128 in all;
-- an object not updated for 0.5 s is parked; everything is removed on disable / reload.
-- Cars and peds (capability `world_audio` >= 4): by default each takes its own instance of a
-- private MixMap (as mod emitters do; not retail): it plays whenever it is within retail's list
-- radius (40 m cars, 50 m peds) and among the 16 nearest own cars / 16 nearest own peds of all mods;
-- a farther one waits, silent (`read(key).waiting`), and takes an instance as soon as it is among
-- the nearest; it is never refused and never takes one of retail's instances, so the map's objects
-- keep retail's pools. `slots = 'shared'` (alias 'retail') puts a car / ped in retail's pools with
-- the map's objects instead, where the game decides who is audible with retail's limits (4 nearest
-- cars within 40 m, 15 nearest peds within 50 m, footsteps for the nearest 3; 8 / 24 / 3 with the
-- opt-in non-retail "more audible" setting). Skaters: one within 30 m (retail's Player slot).
-- (`world_audio` 3 had 'retail' as the default and `slots = 'own'` as the opt-in.)
-- Extension 2 (capability `world_audio` >= 2): 'emitter' and 'reverb_zone' objects: `.ems`-style
-- records added to the map's live lists. An emitter plays an AEMS bank bound to c_emitter (a retail
-- bank or one a content overlay adds) through retail's reach test (sphere when the three extents are
-- equal, else an ellipsoid along forward / up / side; inner core at full level), falloff curve and
-- c_emitter post with the MixMap Emitter words; by default it has its own emitter instance (the map's
-- emitters keep retail's 5 emitter states; mod emitters take instances of a private MixMap with the
-- same words, up to 32 shared with native mod voices); settings/audio.json "mod_emitter_slots":
-- "shared" makes them share retail's 5 with the map's emitters instead (first reached, first served).
-- A reverb zone joins the zones the reverb selector walks, after
-- the map's. Neither parks; `read(key).audible` = playing / holding the listener.
---@class WorldAudioOptions
---@field position? Vec3 world position (ignored while body is set)
---@field velocity? Vec3 m/s; default: from the position change (give it for teleporting objects)
---@field heading? number rad about +Y, 0 = +Z
---@field body? string follow one of this mod's physics bodies (position, rotation, velocity)
---@field engine? string traffic: aud_traffic_engine record (c01_family01, c03_sports01, c04_taxi01, c05_truck01, c00_heavy01, c06_sports02, c07_family02, c08_family03) or a living-world model mapped to one as retail does (taxi01, patrol01, sedan02, hatchback01, sports03, muscle01, suv02, pickup01, minivan01, ...); unknown = silent
---@field speed? number traffic / lite skater: m/s (default |velocity|)
---@field load? number traffic: the driver's signed acceleration m/s² (default: from the speed change; hard stop ≈ -15)
---@field horn? integer traffic: 0 none, 1..5 horn kind, 6 alarm (prefer the events)
---@field skidding? boolean traffic: the tyres skid
---@field voice? integer ped: the model = speech voice id 41..96 (0 none); its shoe class, kind, speech words and far threshold follow (retail's aud_characteristics). skater: its voice (AI skaters 89..96): the bail grunt
---@field shoe_class? integer ped: 1..5 (default: the model's, else 2; 1 is silent)
---@field weight? integer ped: 1..5 (default 1)
---@field close_range? boolean ped: a security guard's close-range footstep / speech levels (default: the model's)
---@field feet? boolean[] ped: {foot A planted, foot B planted}
---@field materials? integer[] ped: audio surface materials under the feet (default 0)
---@field footsteps? boolean ped: footsteps on (default: retail's 3-nearest rule)
---@field tazing? boolean ped: tazing (the c_tazer zap burst plays while it holds; prefer the tazer event)
---@field photo_flag? boolean ped: raise the game flag of the photographer's repeat (a ped holding speech value 29, PictureTaking, repeats it every second while any published ped sets this)
---@field source? string skater (spawn only): 'lite' (default) or 'state_log:<name>' (a ghost replaying logs/<name>.tsv of this mod or SKATE_AUDIO_STATE_LOGS/<name>.tsv)
---@field from? number ghost: window start (s)
---@field seconds? number ghost: window length (s, default 20), looped
---@field wheels? boolean[] lite skater: wheels down {FL, FR, RL, RR}
---@field material? integer lite skater: material under the board (default: the ground's)
---@field grinding? boolean lite skater
---@field grind_material? integer lite skater
---@field air? boolean lite skater: in the air
---@field loose_board? integer lite skater: the loose board (0 none, 1 upside down, 2 on its side): the board slide holds while set (ghosts take it from their log)
---@field bank? string emitter (spawn: required): the AEMS bank bound to c_emitter (unknown bank = command error)
---@field patch? integer emitter: the attribute patch = the c_emitter selector 0..500 (default 0)
---@field volume? number emitter: attribute volume 0..1 (default 1): level = volume × curve(d)
---@field falloff? 'squared'|'linear'|'flat' emitter: eVolumeFalloffType 0 / 1 / 2 (default 'squared')
---@field extent? Vec3 emitter / reverb_zone (spawn: required): extents in m (0.1..10000)
---@field forward? Vec3 emitter / reverb_zone: the ellipsoid's forward axis, turned by heading (default {1,0,0})
---@field core? number emitter / reverb_zone: inner core fraction 0..1 (default 0)
---@field preset? string reverb_zone (spawn: required): the aud_reverb preset key, 16 hex digits, one the install has
---@field slots? 'own'|'shared'|'retail' traffic / ped, spawn only: 'own' (the default since world_audio 4) = its own instance of a private MixMap (not retail): it plays within retail's list radius (40 m cars, 50 m peds) when among the 16 nearest own cars / 16 nearest own peds (all mods), else it waits (`read(key).waiting`); the map's objects keep retail's pools. 'shared' (alias 'retail', the world_audio 3 default) = retail's pools shared with the map's objects, the nearest win
sdk.world_audio = {}
---@param key string
---@param kind 'traffic'|'ped'|'skater'|'emitter'|'reverb_zone'
---@param opts? WorldAudioOptions
function sdk.world_audio.spawn(key, kind, opts) end
---Merge fields into the object's description (a field of another kind is an error).
---@param key string
---@param opts WorldAudioOptions
function sdk.world_audio.update(key, opts) end
---@param key string
---@param event 'horn'|'alarm'|'speech'|'tazer'|'body_fall'|'reaction'
---@param opts? {kind?:integer, seconds?:number, value?:string|integer, by?:integer} reaction (skaters): value slam / slam_b / trick / crash / chase, by = the other skater's model (0 = the player; a pro 1..29 picks the pro-on-pro lines): the skater's own speech process says the matching line of its voice for one console frame (AI skaters 89..96 on the living-world channel, pros 1..29 / special cast 30..38 on the main cast); horn: kind 1..5 for seconds; alarm: retail's 8 s; speech: value name (warn = 53, cheer, slam, flee, nearby, DoWarning, LongCheer, ...) or number; the ped says a line of its voice through retail's speech manager (gated by the event's timers and probability) when the speech decode is installed; a repeated value re-triggers (49 rings a phone, then the ped answers); tazer: the ped zaps for seconds (default the state graph's 2 s): retail's c_tazer burst; body_fall: one BodyFallType key of a knock-down animation (kind 9, 8 or any other value 1..255: three Skate_Collisions sounds; retail's falls go 9, other, 9, 9 about 0.1 / 0.5 / 0.16 s apart)
function sdk.world_audio.event(key, event, opts) end
---@param key string
function sdk.world_audio.remove(key) end
---@param key string
---audible: holds an instance / plays; own: the instance is a private MixMap's; slots (cars, peds; world_audio >= 4): 'own' or 'shared', the object's setting; waiting (>= 4): in reach but every instance of its pool is held by a nearer object.
---@return {kind:string, audible:boolean, instance?:integer, own:boolean, slots?:'own'|'shared', waiting:boolean, parked:boolean}|nil
function sdk.world_audio.read(key) end
---The retail pools (instances, published, audible, waiting) and, under `own` (world_audio >= 4), the private MixMap's (instances 16 / 16, published, audible, waiting).
---@return {more_audible:boolean, instances:{traffic:integer,peds:integer,skaters:integer}, published:table, audible:table, waiting:integer, speech_lines:integer, own:{instances:{traffic:integer,peds:integer}, published:table, audible:table, waiting:integer}}
function sdk.world_audio.info() end