Add Lua audio, dynamic vehicle cameras, dashboard, and Skyline updates

This commit is contained in:
Ethanw05 committed 2026-09-11 04:41:47 -06:00
1 parent cb7968930f
commit 1399555ef2
140 files changed
+11400 -7917

No files matched your search

+15 -65
View File
@@ -1,77 +1,27 @@
# Agent guide: make a mod for this game
# Agent guide: physics/graphics mods (API 2)
Use this file as the starting point when asked to make a mod. The current SDK is
embedded Lua 5.4, API major 1. Deliver a validated `.zip` in the project-root `mods/`.
Do not invent API calls or assume this is an unrestricted Lua/native-code plugin system.
Use this when asked to make a mod. Deliver a validated package under `mods/` (folder or `.zip`).
Do **not** invent vehicle/wheel/suspension host APIs — assemble those in Lua from primitives.
## Read these sources in order
## Read first
1. `docs/mod-packages.md`: ZIP structure, packaging and installation.
2. `sdk/skate.lua`: callable functions, argument types and bounds; annotations only.
3. `docs/lua-modding.md`: manifest, settings, lifecycle, quotas, callbacks and examples.
4. `docs/multiplayer-mods.md`: automatic replication, matching packages, ownership and shared state.
5. For vehicles, `docs/vehicle-sdk.md` and `sdk/examples/mario-kart/vehicle.json`.
6. For rider animations, `docs/mixamo-vehicle-workflow.md`. For character conversion,
`docs/mixamo-to-skate.md` and `docs/custom-models.md`.
Paths are relative to the repository root. Authoritative implementations are
`crates/skate-mods/src/{schema,vm}.rs`, `crates/skate-vehicles/src/definition.rs`, and
`crates/skate-game/src/modding/`. If a requested feature has no API, explain the gap
or implement/test a shared host API; do not fake an unsupported call in the mod.
1. `docs/physics-graphics-sdk.md` — layer philosophy and deferred work
2. `sdk/skate.lua` — annotations
3. `sdk/examples/physics-sandbox/` — reference mod
4. Host: `crates/skate-game/src/modding/`, runtime `crates/skate-mods/`, dynamics `crates/skate-dynamics/`
## Workflow
- Create an editable folder outside live mods/, e.g. `sdk/examples/your-mod`.
Copy the smallest suitable example, not the whole repository. Use a unique ID
like `author.feature` and keep that ID stable across updates.
- Write mod.json with api=1, name, version, author, description and entry. Each
setting needs type, label, description and default, plus bounds/options for its type.
- Return a callback table from main.lua. Use owner-local stable keys for visuals,
read settings via sdk.settings, edge-detect held inputs, and clean/reset transient
state on world_changed. Refresh vehicle controls from on_fixed_update.
- Decide which player owns each shared rule. Cubes and vehicles replicate automatically;
UI/input stay local. Use sdk.net.publish/read for Lua state and retain player IDs as strings.
Reuse stable keys; do not broadcast one-frame events or independently award shared scores
on every client. Consider host changes, late joins and mod disable cleanup.
- Keep assets inside the package and use relative forward-slash paths. The entry
and vehicle filenames need not be the example names, but their references must match.
The game loads one Lua entry file; do not assume require/dofile or arbitrary file I/O.
- Build and validate:
```powershell
cargo run --locked -p skate-mods --example check_mod -- sdk/examples/your-mod
python tools/package_mod.py sdk/examples/your-mod mods/your-mod.zip
# copy/symlink into mods/
```
- For code/API changes, also run `cargo test --locked -p skate-mods -p skate-vehicles -p skate-net`.
Check the game crate and build with `Build-VehicleSDK.ps1` when the host changed.
The helper packages the two bundled examples; package a new mod separately.
- The checker checks structure, limits, schema and Lua syntax without running the mod.
It does not prove callbacks, render results, rig compatibility or physics behaviour.
Run appropriate headless tests. Do not launch the game automatically in this workspace;
provide `PLAY-MARIO-KART.bat` for the user to test and state what remains unverified.
- Report the ZIP path, controls/settings, validation results and relevant limitations.
The Native Trainer and Mario Kart ZIPs are intentionally committed, as are the
prepared kart.glb and rider.json source assets. Keep those ZIPs in sync with their
editable examples. Other imported assets/raw authoring files remain ignored.
Manifest must use `"api": 2`. Return a callback table from the entry Lua file.
## Available building blocks
## Building blocks
- Player/input snapshots, native teleport requests, log/text, coloured cubes, timers,
package text reads and supported authored animation replacement slots.
- Native Trainer tuning: pop, push, braking, steering, grip, fakie hold and related
multipliers. Only one mod can own trainer tuning at a time.
- Vehicles: spawn/read/control/tune/enter/exit/reset/remove, Rapier map and vehicle
collision, raycast wheels/suspension, collider offsets/rounding, independent inertia
and centre of mass, throttle-responsive engine synthesis and live engine volume.
Rider safety adds an occupied capsule, crash/inversion ejection, carried momentum
into the native bail state and vehicle_bailed events; configure rider_safety.
- Native-rig rider enter/exit/drive/idle/reverse/brake/steering slots. The host handles
steering blending, vanilla hand-offs, board hiding and shared motion interpolation.
- Automatic per-mod draggable/resizable/collapsible settings windows and enable badges.
No native DLL/EXE loading, arbitrary network/filesystem access, external Lua imports,
inter-mod dependencies, custom audio recordings or arbitrary rig playback are exposed.
Vehicle replay recording is not implemented. Multiplayer and car/player collision
bridging are available; see docs/multiplayer-mods.md for ownership and approximation
limits. Assets can be bundled but only documented asset types have loaders.
Physics bodies (box/sphere/capsule/convex/`mesh` from named GLB objects), extra
`add_collider` hulls, forces/joints(+motors)/sensors, graphics meshes/overlays,
player attach/detach, camera follow/set, `sdk.assets.objects`, `sdk.input.pad`,
settings/log/timers. No `sdk.vehicle`.
-15
View File
@@ -1,15 +0,0 @@
# Mod SDK
**Start here: [AGENTS.md](AGENTS.md).** Give this file to your coding agent when asking
it to make a mod. It links the package format, complete API, examples and validation.
Editable sources: `examples/`. Player-installable output: project-root `mods/*.zip`.
Use `tools/package_mod.py` to validate and package. The current integrated game launcher
is `PLAY-MARIO-KART.bat`; older feature-specific launchers are historical builds.
## Multiplayer
Use matching enabled ZIPs and the same integrated build on all peers. World objects
and vehicles synchronize through the SDK; Lua shared rules use `sdk.net`.
See [Multiplayer mod SDK](../docs/multiplayer-mods.md) for ownership, collisions,
late joins and shared-state examples.
-56
View File
@@ -1,56 +0,0 @@
# Mario Kart vehicle mod
Version 1.5 adds arcade rollover resistance and airborne left-stick control.
In air, stick left/right rolls, forward pitches the nose down, and back pitches
the nose up. Keyboard: A/D roll, Up/Down arrows pitch. Neutral input damps rotation.
No lift is added. Aerial inversion is allowed; sustained grounded inversion still
ejects. Minor bumps are more forgiving; severe collisions still eject the rider.
The matching updated host executable is required.
Enable this mod in Mods, resume, then press F10 to spawn the kart in front of you.
E (controller Y) enters/exits within 4 metres (slow below 3 m/s to exit). WASD drives; Space brakes;
left Shift is the handbrake; R or right-stick click rights/resets the kart. The controller bind can be changed to left-stick click in the mod settings. F10 replaces the kart.
Controller: RT/LT accelerate/reverse, left stick steers, A brakes, B handbrakes.
Version 1.4 uses the updated host's physical tire model: progressive steering,
Ackermann wheel angles, torque-driven wheel spin, shared braking/cornering grip,
and contact-point weight transfer. Brake overrides throttle; the opposing pedal
brakes before engaging reverse. Tire friction defaults to 1.3. The old arcade
grip preference is replaced by this new coefficient; other settings are retained.
This package requires the matching updated game executable. It remains a
simplified raycast-wheel kart, without a gearbox or ABS.
The enabled mod shows keyboard and controller controls on screen, including the current reset bind. Toggle Show driving HUD in its settings to hide/show them, even before spawning.
The GLB is prepared from your supplied ZIP with tools/prepare_mario_kart.py and is
committed with the mod. To rebuild it: python tools/prepare_mario_kart.py PATH_TO_ZIP sdk/examples/mario-kart
The script requires NumPy. The prepared kart.glb and fitted rider.json are included in source control and in mods/mario-kart.zip.
Edit vehicle.json for dimensions, wheels, suspension, seat, exit and camera offsets.
The bundled rider.json contains fitted entry, exit, seated and left/right steering poses.
The host blends steering with stick input and eases between vanilla and vehicle poses
using the native local-transform interpolation routine. The skateboard is hidden while
seated and restored during the exit hand-off. Camera changes are eased too.
Rebuild rider.json with Blender and tools/export_kart_rider.py, passing --enter and
--exit fitted .blend previews, --bone-names a JSON list from the stock bank hierarchy,
--reference the stock skater.glb and --output the local
rider.json path. Raw FBX files, Blender projects and the stock character mesh remain local; only the prepared kart model and fitted rider clips are bundled.
See docs/vehicle-sdk.md for the animation format and lifecycle details.
Ramp tuning uses a shorter rounded chassis and a lower centre of mass, with mass
distribution independent of the collider. The shared engine_audio definition enables
throttle/speed-responsive synthesized engine sound; Engine volume in the mod window
controls it live (zero mutes). Full authoring guide: docs/mixamo-vehicle-workflow.md.
Author here, then run `python tools/package_mod.py sdk/examples/mario-kart mods/mario-kart.zip`.
The live ZIP loads automatically; do not edit mods/.cache. See sdk/AGENTS.md.
Hard crashes, strong head/torso hits and sustained inversion now eject the rider into
native bail physics with carried momentum. An occupied-seat capsule protects the torso
against map geometry. Tune rider_safety in vehicle.json; see docs/vehicle-sdk.md for
threshold units, vehicle_bailed events and the native/Rapier collision boundary.
## Multiplayer
Use matching enabled ZIPs and the same integrated build on all peers. World objects
and vehicles synchronize through the SDK; Lua shared rules use `sdk.net`.
See [Multiplayer mod SDK](../../../docs/multiplayer-mods.md) for ownership, collisions,
late joins and shared-state examples.
Binary file not shown.
-61
View File
@@ -1,61 +0,0 @@
-- The original Lua/JSON example uses the user's separately supplied kart model.
local previous={}
local interact_was_down=false
local reset_was_down=false
local function hud(car)
local lines={
car and string.format('MARIO KART | %.0f km/h | %s',math.abs(car.speed)*3.6,car.ready and car.phase or 'loading') or 'MARIO KART | F10: spawn kart',
'KEYBOARD | F10 spawn | E enter / exit | W / S accelerate / reverse | A / D steer',
'KEYBOARD | Space brake | Left Shift handbrake | R reset',
'CONTROLLER | Y enter / exit | RT / LT accelerate / reverse | Left stick steer',
'CONTROLLER | A brake | B handbrake | '..(sdk.settings.reset_button=='Left stick' and 'L3' or 'R3')..' (stick click) reset',
'Slow below 3 m/s to exit | Hard crashes / rollovers eject the rider | Escape: settings'
}
for i,line in ipairs(lines) do
local key=i==1 and 'kart-hud' or 'kart-hud-'..i
if sdk.settings.hud then sdk.ui.text(key,line) else sdk.scene.remove(key) end
end
end
local function pressed(key)
local down=sdk.input.down(key);local edge=down and not previous[key];previous[key]=down;return edge
end
local function tune()
local s=sdk.settings
sdk.vehicle.tune('kart',{engine_volume=s.engine_volume,engine_force=s.engine,max_speed=s.speed,brake_impulse=s.brake,steering_angle=s.steering,tire_grip=s.tire_friction})
end
local function nearby()
local p=sdk.player.read();local h=p.heading or 0
return {p.position[1]+math.sin(h)*3,p.position[2]+1,p.position[3]+math.cos(h)*3},h
end
local function spawn()
local car=sdk.vehicle.read('kart')
if car and car.occupied then sdk.vehicle.reset('kart',{car.position[1],car.position[2]+1,car.position[3]},car.heading);return end
if car then sdk.vehicle.remove('kart') end
local p,h=nearby();sdk.vehicle.spawn('kart','vehicle.json',p,h);tune()
end
return {
on_load=function() hud() end,
on_fixed_update=function()
if pressed('F10') then spawn() end
local car=sdk.vehicle.read('kart')
if not car then hud();return end
local interact=sdk.vehicle.input().interact
local interact_pressed=interact and not interact_was_down;interact_was_down=interact
if interact_pressed then if car.occupied then sdk.vehicle.exit('kart') else sdk.vehicle.enter('kart') end end
local reset_mask=sdk.settings.reset_button=='Left stick' and 0x40 or 0x80
local reset_down=car.occupied and ((sdk.vehicle.input().pad_buttons or 0) & reset_mask)~=0
local reset_pressed=reset_down and not reset_was_down;reset_was_down=reset_down
if pressed('KeyR') or reset_pressed then
local p,h=nearby()
if car.occupied then p={car.position[1],car.position[2]+1,car.position[3]};h=car.heading end
sdk.vehicle.reset('kart',p,h)
end
if car.phase=='driving' then sdk.vehicle.control('kart',sdk.vehicle.input() and {
throttle=sdk.vehicle.input().throttle,steering=sdk.vehicle.input().steering,
pitch=sdk.vehicle.input().pitch or 0,
brake=sdk.vehicle.input().brake,handbrake=sdk.vehicle.input().handbrake}) end
hud(car)
end,
on_settings=function() local car=sdk.vehicle.read('kart');if car then tune() end;hud(car) end,
on_event=function(e) if e.name=='world_changed' then previous={};interact_was_down=false;reset_was_down=false;hud() end end,
}
-81
View File
@@ -1,81 +0,0 @@
{
"id": "community.mario-kart",
"api": 1,
"name": "Mario Kart",
"version": "1.5.0",
"author": "Local vehicle SDK example",
"description": "Rapier kart using your supplied model. F10 spawns nearby, E enters/exits, WASD drives, Space brakes, Shift handbrake, R resets. Slow down before exiting. Fitted entry/exit and steering animations included.",
"entry": "main.lua",
"settings": {
"engine": {
"type": "number",
"label": "Engine force",
"description": "Applied live to the kart.",
"default": 1800,
"min": 100,
"max": 8000,
"step": 100
},
"speed": {
"type": "number",
"label": "Maximum speed (m/s)",
"description": "Applied live to the kart.",
"default": 25,
"min": 5,
"max": 60,
"step": 1
},
"brake": {
"type": "number",
"label": "Brake impulse",
"description": "Applied live to the kart.",
"default": 90,
"min": 10,
"max": 300,
"step": 10
},
"steering": {
"type": "number",
"label": "Steering angle",
"description": "Applied live to the kart.",
"default": 0.5,
"min": 0.1,
"max": 1,
"step": 0.05
},
"tire_friction": {
"type": "number",
"label": "Tire friction coefficient",
"description": "Combined braking and cornering grip. 1.3 is the dry kart setup; lower values allow more sliding.",
"default": 1.3,
"min": 0.2,
"max": 2,
"step": 0.05
},
"hud": {
"type": "boolean",
"label": "Show driving HUD",
"description": "Show keyboard and controller controls, current reset bind, speed and vehicle state.",
"default": true
},
"reset_button": {
"type": "choice",
"label": "Controller reset bind",
"description": "Click this stick while driving to reset the kart. R remains the keyboard shortcut.",
"default": "Right stick",
"choices": [
"Right stick",
"Left stick"
]
},
"engine_volume": {
"type": "number",
"label": "Engine volume",
"default": 0.45,
"min": 0,
"max": 1,
"step": 0.05,
"description": "Engine loudness; zero mutes the motor."
}
}
}
-1
View File
@@ -1 +0,0 @@
{"version":1,"bone_names":["TRAJECTORY","HIPS","SPINE","SPINE1","SPINE2","SPINE3","NECK","NECK1","HEAD","RIGHTSHOULDER","RIGHTARM","RIGHTFOREARM","RIGHTHAND","LEFTSHOULDER","LEFTARM","LEFTFOREARM","LEFTHAND","RIGHTUPLEG","RIGHTLEG","RIGHTFOOT","RIGHTTOEBASE","LEFTUPLEG","LEFTLEG","LEFTFOOT","LEFTTOEBASE","SKATEBOARD_ROOT","TRUCK_FRONT","RIGHT_WHEELFRONT","LEFT_WHEELFRONT","TRUCK_BACK","LEFT_WHEELBACK","RIGHT_WHEELBACK","RIGHTTOEBASE_REPARENTED","LEFTTOEBASE_REPARENTED","RIGHTHAND_REPARENTED","LEFTHAND_REPARENTED"],"clips":{"enter":{"fps":60,"frames":[[[1.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,1.0],[0.247126,0.965941,0.076725,0.0,-0.964343,0.25291,-0.077973,0.0,-0.094722,-0.05472,0.993999,0.0,1.826877,0.184085,0.134088,1.0],[-0.010874,0.999876,0.011391,0.0,-0.997631,-0.010074,-0.068046,0.0,-0.067923,-0.012104,0.997617,0.0,1.853606,0.275625,0.143949,1.0],[0.077093,0.996972,-0.010229,0.0,-0.99506,0.076293,-0.063535,0.0,-0.062562,0.015077,0.997927,0.0,1.853971,0.37509,0.147294,1.0],[0.170703,0.985314,-0.004184,0.0,-0.983335,0.170088,-0.064217,0.0,-0.062562,0.015077,0.997927,0.0,1.862298,0.482773,0.146189,1.0],[-0.134993,0.989598,-0.049735,0.0,-0.989193,-0.137496,-0.050907,-0.0,-0.057216,0.042326,0.997464,0.0,1.884411,0.613769,0.144682,1.0],[-0.440967,0.894681,-0.071377,0.0,-0.895543,-0.443881,-0.03119,-0.0,-0.059588,0.050167,0.996961,0.0,1.864403,0.749667,0.137649,1.0],[-0.276798,0.958742,-0.064788,0.0,-0.959079,-0.279818,-0.043244,-0.0,-0.059588,0.050167,0.996962,0.0,1.838417,0.802391,0.133443,1.0],[-0.06035,0.996886,-0.050769,0.0,-0.997542,-0.062048,-0.032579,-0.0,-0.035627,0.048678,0.998179,0.0,1.828663,0.844764,0.13103,1.0],[0.063381,-0.304268,-0.950476,-0.0,-0.994333,-0.10071,-0.034066,-0.0,-0.085357,0.947248,-0.308927,0.0,1.843193,0.722202,0.113186,1.0],[-0.055441,-0.973607,-0.221399,-0.0,-0.735036,-0.110275,0.669001,0.0,-0.675758,0.199827,-0.709522,0.0,1.848127,0.677019,-0.045639,1.0],[-0.122471,-0.992314,0.017735,-0.0,-0.718667,0.100993,0.687982,0.0,-0.684485,0.071512,-0.725511,0.0,1.839006,0.387418,-0.110069,1.0],[-0.003577,-0.882047,0.471148,-0.0,-0.472976,-0.413626,-0.777951,0.0,0.881068,-0.225625,-0.415708,-0.0,1.806323,0.144883,-0.118319,1.0],[-0.088402,-0.18525,0.978707,-0.0,-0.990129,-0.090947,-0.106648,-0.0,0.108767,-0.978474,-0.175381,-0.0,1.842112,0.72302,0.160986,1.0],[-0.062181,-0.978667,0.195816,-0.0,-0.753674,-0.082578,-0.652041,0.0,0.654301,-0.188126,-0.732461,-0.0,1.822075,0.697518,0.325838,1.0],[-0.072891,-0.997233,0.014646,-0.0,-0.748069,0.044955,-0.662097,0.0,0.659606,-0.059217,-0.749276,-0.0,1.813966,0.408338,0.384805,1.0],[-0.111244,-0.898037,-0.425624,-0.0,-0.236578,-0.392039,0.889009,0.0,-0.965223,0.19959,-0.168844,0.0,1.796902,0.164993,0.399195,1.0],[-0.055255,-0.998217,-0.022604,0.0,-0.961268,0.059305,-0.26916,0.0,0.27002,0.006856,-0.96283,0.0,1.834232,0.199152,0.054516,1.0],[0.151135,-0.988131,0.027482,0.0,-0.891985,-0.148306,-0.427047,0.0,0.426054,0.040028,-0.903812,0.0,1.811971,-0.220373,0.044531,1.0],[-0.782362,-0.481747,-0.394752,0.0,-0.430099,0.87631,-0.217013,0.0,0.45047,-0.0,-0.892791,0.0,1.873222,-0.619946,0.055971,1.0],[-0.885607,0.126606,-0.446846,0.0,0.113032,0.991953,0.057032,0.0,0.45047,0.0,-0.892791,0.0,1.74596,-0.700553,-0.008241,1.0],[-0.23299,-0.964857,0.121518,0.0,-0.951872,0.251854,0.17467,0.0,-0.199137,-0.074974,-0.977099,0.0,1.819936,0.188216,0.212871,1.0],[0.109024,-0.989689,0.092899,0.0,-0.967091,-0.083991,0.240169,0.0,-0.22989,-0.116027,-0.966275,0.0,1.722869,-0.219471,0.26457,1.0],[-0.849439,-0.483431,0.211536,0.0,-0.469696,0.87538,0.114437,0.0,-0.240497,-0.002151,-0.970648,0.0,1.766883,-0.618771,0.302604,1.0],[-0.967257,0.084077,0.23947,0.0,0.081094,0.996457,-0.022301,0.0,-0.240497,-0.002151,-0.970647,0.0,1.628154,-0.699175,0.337155,1.0],[1.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,-0.641,0.2,1.0],[-0.0,-0.374607,-0.927184,0.0,-1.0,-0.0,0.0,0.0,-0.0,0.927184,-0.374607,0.0,-0.0,-0.668862,0.459179,1.0],[1.0,0.0,-0.0,0.0,0.0,-0.374607,-0.927184,0.0,-0.0,0.927184,-0.374607,0.0,-0.097497,-0.697312,0.442804,1.0],[1.0,0.0,-0.0,0.0,0.0,-0.374607,-0.927184,0.0,-0.0,0.927184,-0.374607,0.0,0.097497,-0.697312,0.442804,1.0],[-0.0,-0.374606,0.927184,0.0,1.0,-0.0,0.0,0.0,0.0,0.927184,0.374606,0.0,0.0,-0.668863,-0.059179,1.0],[1.0,-0.0,0.0,0.0,0.0,0.374606,-0.927184,0.0,0.0,0.927184,0.374606,0.0,0.097497,-0.697312,-0.043508,1.0],[1.0,-0.0,0.0,0.0,0.0,0.374606,-0.927184,0.0,0.0,0.927184,0.374606,0.0,-0.097497,-0.697312,-0.043508,1.0],[1.7e-05,0.0,1.0,0.0,-2e-06,1.0,0.0,0.0,-1.0,-2e-06,1.7e-05,0.0,-0.112785,-0.699299,0.309098,1.0],[1.7e-05,0.0,1.0,0.0,-2e-06,1.0,0.0,0.0,-1.0,-2e-06,1.7e-05,0.0,0.112806,-0.699299,0.309098,1.0],[0.5843,-0.679796,0.443249,0.0,0.759174,0.650881,-0.002524,0.0,-0.286787,0.337978,0.896395,0.0,0.512949,0.311738,0.300137,1.0],[-0.581212,-0.68309,0.442245,0.0,-0.761452,0.648221,0.000517,0.0,-0.287026,-0.336448,-0.896894,0.0,-0.512609,0.311738,0.300137,1.0]],[[1.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,Line truncated
-127
View File
@@ -1,127 +0,0 @@
{
"version": 1,
"model": "kart.glb",
"half_extents": [
0.5,
0.14,
0.65
],
"mass": 220,
"engine_force": 1800,
"max_speed": 25,
"brake_impulse": 90,
"steering_angle": 0.5,
"suspension_length": 0.25,
"suspension_stiffness": 30,
"suspension_damping": 4,
"tire_grip": 1.3,
"ground_stability": 0.8,
"air_control": 6,
"wheels": [
{
"node": "wheel_rl",
"position": [
0.5815213608992547,
0.057323790779298445,
-0.673016238225116
],
"radius": 0.3338663063949073,
"steering": false,
"driven": true
},
{
"node": "wheel_rr",
"position": [
-0.5302057197312625,
0.06093891045413358,
-0.6730162402958683
],
"radius": 0.3338663063949072,
"steering": false,
"driven": true
},
{
"node": "wheel_fl",
"position": [
0.5573244172948093,
0.016883687342375997,
0.4261990809959044
],
"radius": 0.31535000391066886,
"steering": true,
"driven": false
},
{
"node": "wheel_fr",
"position": [
-0.5201046643741327,
0.01739195285686146,
0.4231450809914261
],
"radius": 0.31535000391066886,
"steering": true,
"driven": false
}
],
"seat": [
0,
0.18,
-0.2
],
"exit": [
1.8,
0,
0
],
"camera_distance": 5,
"camera_height": 2,
"animations": {
"file": "rider.json",
"enter": "enter",
"exit": "exit",
"drive": "drive",
"idle": "drive",
"reverse": "drive",
"brake": "drive",
"steer_left": "steer_left",
"steer_right": "steer_right"
},
"collider_offset": [
0,
0,
0
],
"collider_rounding": 0.1,
"chassis_friction": 0.05,
"engine_audio": {
"enabled": true,
"volume": 0.45,
"idle_pitch": 0.7,
"max_pitch": 2.8
},
"center_of_mass": [
0,
-0.2,
0
],
"inertia_half_extents": [
1.1,
0.35,
1.4
],
"rider_safety": {
"enabled": true,
"offset": [
0,
0.5,
0
],
"radius": 0.25,
"half_height": 0.25,
"crash_delta_v": 12,
"hit_impulse": 1200,
"inverted_up_y": -0.2,
"inverted_seconds": 1.0,
"eject_up_speed": 2
}
}
-60
View File
@@ -1,60 +0,0 @@
# Native Trainer Showcase 2.1
One original mod demonstrating the actual SDK, with no game assets. Enable it in
Escape -> Mods. Its independent draggable settings window opens on Escape.
The eleven physics controls start at 1x stock. Disable the mod to restore stock
parameters and remove all its HUD and world objects.
## Native controls
Pop height, grind pop, push speed, push power, brake strength, steering response,
speed wobble, off-board jump height, ground wheel/slide grip, heading turn force,
and manual balance drag. These scale existing inputs/settings, not simulation
speed or arbitrary body velocities. Most controls range 0.25x-4x; wobble is 0x-2x.
Current difficulty, native state gates and surface rules still apply.
## Showcase features
- Compact/full/off telemetry: board speed, peak, approximate travelled distance,
root position, bail count, grind entries and active grind time.
- F5 saves a trainer checkpoint; F6 requests return through the native reset path.
This does not replace the game's own session marker. Save on valid clear ground.
- F7 starts/stops the active-play stopwatch; its clock pauses with Escape/replay.
- F8 clears session statistics, stopwatch, checkpoint and all visual markers.
- F9 places a coloured visual beacon. Eight keyed beacons are reused in a ring.
- Optional breadcrumb trail reuses 24 tiny cubes at configurable distance spacing.
- Colour choices, units, HUD label, help hints and configurable keyboard shortcuts.
- Temporary notifications expire using owned timers. The log reads help.txt using
the package-relative file API. Edit source to see debounced hot reload in action.
Markers have no collision and are not grind rails. Statistics are local sampled
observations, not native career scores: travel rejects single-frame jumps of 50m
or more but is not replay-grade odometry. Board velocity is not walking speed.
The full HUD uses separate owned lines to avoid overlapping other notifications.
All transient state resets on script reload/disable or map change. Persisted
settings survive. The shortcut choice lists let authors try rebinding; use distinct
keys. If bindings coincide, actions execute in save/return/timer/clear/beacon order.
A native teleport rejection stops this mod and reports the error, as with any
SDK host-command failure; recover via Reload. No invincibility, general gravity
edit, physics collision insertion, arbitrary animation slot or score API is claimed.
## Source tour
main.lua deliberately separates apply (validated native tuning), update (input,
HUD, visual objects), on_event (map/bail/grind observations), and timer cleanup.
Use this as a practical starting point for training tools, navigation helpers,
telemetry overlays and original visual extensions. The engine's full API reference
is docs/lua-modding.md and language-server annotations are sdk/skate.lua.
Hold fakie stance disables automatic fakie stance switching; normal steering and slides still work. Off by default. Escape settings windows can be resized by dragging the bottom-right // grip.
Authoring folder: sdk/examples/native-trainer. Package with tools/package_mod.py into
mods/native-trainer.zip. See sdk/AGENTS.md and docs/mod-packages.md.
## Multiplayer
Use matching enabled ZIPs and the same integrated build on all peers. World objects
and vehicles synchronize through the SDK; Lua shared rules use `sdk.net`.
See [Multiplayer mod SDK](../../../docs/multiplayer-mods.md) for ownership, collisions,
late joins and shared-state examples.
-4
View File
@@ -1,4 +0,0 @@
Native Trainer Showcase 2.0
F5 save checkpoint; F6 return; F7 start/stop stopwatch; F8 clear session; F9 place beacon.
All bindings and visual options are configurable. All physics multipliers default to 1.
Telemetry reads the existing engine; cubes are visual only. No game assets are bundled.
-110
View File
@@ -1,110 +0,0 @@
-- Native Trainer Showcase: original Lua, no game assets.
local tuning_keys={'pop','grind_pop','push_speed','push_power','braking','steering','wobble','offboard_jump','grip','turn_power','manual_drag','hold_fakie'}
local previous, checkpoint, last_position, last_trail = {}, nil, nil, nil
local trail_index, beacon_index = 0, 0
local stats = {distance=0,peak=0,bails=0,grinds=0,grind_time=0}
local stopwatch, running, elapsed = 0, false, 0
local function copy(p) return {p[1],p[2],p[3]} end
local function distance(a,b) return math.sqrt((a[1]-b[1])^2+(a[2]-b[2])^2+(a[3]-b[3])^2) end
local function color()
return sdk.settings.marker_color=='orange' and {1,0.45,0.1} or sdk.settings.marker_color=='purple' and {0.7,0.25,1} or {0.1,0.85,0.85}
end
local function notice(text)
sdk.ui.text('notice',text)
sdk.time.after('notice',3,function() sdk.scene.remove('notice') end)
end
local function clear_trail()
for i=1,24 do sdk.scene.remove('trail'..i) end
last_trail=nil; trail_index=0
end
local function clear_session()
clear_trail()
for i=1,8 do sdk.scene.remove('beacon'..i) end
sdk.scene.remove('checkpoint'); sdk.scene.remove('timer'); sdk.scene.remove('notice')
sdk.time.cancel('notice')
checkpoint=nil; beacon_index=0; last_position=nil
stopwatch=0; running=false; elapsed=0
stats={distance=0,peak=0,bails=0,grinds=0,grind_time=0}
end
local function apply()
local tuning={}
for _,key in ipairs(tuning_keys) do tuning[key]=sdk.settings[key] end
sdk.trainer.apply(tuning)
if not sdk.settings.trail then clear_trail() end
if sdk.settings.hud=='off' then sdk.scene.remove('hud'); sdk.scene.remove('hud_stats'); sdk.scene.remove('hud_help') end
end
local function draw()
local s=sdk.settings
if s.hud=='off' then sdk.scene.remove('hud'); sdk.scene.remove('hud_stats'); sdk.scene.remove('hud_help'); return end
local p=sdk.player.read(); local v=p.velocity
local speed=math.sqrt(v[1]^2+v[2]^2+v[3]^2)
local factor=s.units=='km/h' and 3.6 or s.units=='mph' and 2.236936 or 1
local label=s.hud_label; local cut=utf8.offset(label,25); if cut then label=label:sub(1,cut-1) end
local text=string.format('%s | %.1f %s | peak %.1f | %.0f m',label,speed*factor,s.units,stats.peak*factor,stats.distance)
if s.hud=='full' then
sdk.ui.text('hud_stats',string.format('XYZ %.1f %.1f %.1f | bails %d | grinds %d / %.1fs',p.position[1],p.position[2],p.position[3],stats.bails,stats.grinds,stats.grind_time))
else sdk.scene.remove('hud_stats') end
if s.show_help then sdk.ui.text('hud_help',string.format('%s save %s return %s timer %s clear %s beacon',s.save_key,s.return_key,s.timer_key,s.reset_key,s.beacon_key)) else sdk.scene.remove('hud_help') end
sdk.ui.text('hud',text)
end
local function update(e)
local p=sdk.player.read(); local position=p.position
elapsed=elapsed+e.dt
if last_position then local d=distance(position,last_position); if d<50 then stats.distance=stats.distance+d end end
last_position=copy(position)
local v=p.velocity; stats.peak=math.max(stats.peak,math.sqrt(v[1]^2+v[2]^2+v[3]^2))
if p.grind and p.grind.active then stats.grind_time=stats.grind_time+e.dt end
local edges={}; local down={}
for _,setting in ipairs({'save_key','return_key','timer_key','reset_key','beacon_key'}) do
local key=sdk.settings[setting]; down[key]=sdk.input.down(key); edges[setting]=down[key] and not previous[key]
end
previous=down
if edges.save_key then
if p.bailing or p.state==702 then notice('Wait until the skater is ready to save.')
else
checkpoint={position=copy(position),heading=p.heading or 0,on_board=p.on_board}
sdk.scene.cube('checkpoint',{position[1],position[2]+0.5,position[3]},{0.15,1,0.15},color())
notice('Trainer checkpoint saved (separate from the native session marker).')
end
end
if edges.return_key then
if not checkpoint then notice('Save a trainer checkpoint first.')
elseif p.bailing or p.state==702 then notice('Wait until recovery finishes.')
else
sdk.player.teleport(checkpoint.position,checkpoint.heading,checkpoint.on_board)
last_position=nil; last_trail=nil
notice('Returning to trainer checkpoint.')
end
end
if edges.timer_key then
if running then running=false; notice(string.format('Stopwatch stopped: %.2f seconds',stopwatch))
else stopwatch=0; running=true; notice('Stopwatch started.') end
end
if edges.reset_key then clear_session(); notice('Session stats, checkpoint and visual markers cleared.') end
if edges.beacon_key then
beacon_index=beacon_index%8+1
sdk.scene.cube('beacon'..beacon_index,{position[1],position[2]+1,position[3]},{0.25,2,0.25},color())
notice('Visual beacon placed; it has no collision.')
end
if sdk.settings.trail and (not last_trail or distance(position,last_trail)>=sdk.settings.trail_spacing) then
trail_index=trail_index%24+1; last_trail=copy(position)
sdk.scene.cube('trail'..trail_index,{position[1],position[2]+0.1,position[3]},{0.2,0.2,0.2},color())
end
if running then stopwatch=stopwatch+e.dt end
if running or stopwatch>0 then sdk.ui.text('timer',string.format('STOPWATCH: %.2f s%s',stopwatch,running and '' or ' (stopped)')) end
draw()
end
return {
on_load=function()
apply(); draw()
sdk.log(sdk.read_text('help.txt'))
notice('Native Trainer Showcase ready. Open Escape for settings.')
end,
on_settings=function() apply(); draw() end,
on_update=update,
on_event=function(e)
if e.name=='world_changed' then clear_session(); previous={}; apply(); draw(); notice('New map: trainer session cleared.') end
if e.name=='bail_changed' and e.bailing then stats.bails=stats.bails+1 end
if e.name=='grind_changed' and e.grind.active then stats.grinds=stats.grinds+1 end
end
}
-236
View File
@@ -1,236 +0,0 @@
{
"id": "community.native-trainer",
"api": 1,
"name": "Native Trainer Showcase",
"version": "2.1.0",
"author": "Skate Rust Engine",
"description": "Native tuning, telemetry, checkpoint return, stopwatch, breadcrumbs and beacons in one original Lua mod. Manage options here; default keyboard shortcuts F5 save, F6 return, F7 stopwatch, F8 reset, F9 beacon. Stock tuning is 1x.",
"entry": "main.lua",
"settings": {
"pop": {
"type": "number",
"label": "Pop height",
"description": "Multiplies native ground-jump minimum and maximum heights. 1.00x is stock.",
"default": 1,
"min": 0.25,
"max": 4,
"step": 0.25
},
"grind_pop": {
"type": "number",
"label": "Grind pop height",
"description": "Multiplies the existing grind-launch height. 1.00x is stock.",
"default": 1,
"min": 0.25,
"max": 4,
"step": 0.25
},
"push_speed": {
"type": "number",
"label": "Maximum push speed",
"description": "Multiplies the speed up to which pushing works. This is not a downhill speed cap. 1.00x is stock.",
"default": 1,
"min": 0.25,
"max": 4,
"step": 0.25
},
"push_power": {
"type": "number",
"label": "Push power",
"description": "Multiplies native difficulty-specific push velocity changes. 1.00x is stock.",
"default": 1,
"min": 0.25,
"max": 4,
"step": 0.25
},
"braking": {
"type": "number",
"label": "Brake strength",
"description": "Multiplies foot and tail braking forces. 1.00x is stock.",
"default": 1,
"min": 0.25,
"max": 4,
"step": 0.25
},
"steering": {
"type": "number",
"label": "Steering response",
"description": "Multiplies the native general steering scalar. 1.00x is stock.",
"default": 1,
"min": 0.25,
"max": 4,
"step": 0.25
},
"wobble": {
"type": "number",
"label": "Speed wobble",
"description": "Multiplies native wobble amplitude. Zero removes wobble. 1.00x is stock.",
"default": 1,
"min": 0,
"max": 2,
"step": 0.25
},
"offboard_jump": {
"type": "number",
"label": "Off-board jump",
"description": "Multiplies native biped jump height. 1x preserves stock.",
"default": 1,
"min": 0.25,
"max": 4,
"step": 0.25
},
"grip": {
"type": "number",
"label": "Wheel / slide grip",
"description": "Multiplies ground wheel static/dynamic friction and sideways slide friction. 1x preserves stock.",
"default": 1,
"min": 0.25,
"max": 4,
"step": 0.25
},
"turn_power": {
"type": "number",
"label": "Turning force",
"description": "Multiplies native heading turn torque strength. 1x preserves stock.",
"default": 1,
"min": 0.25,
"max": 4,
"step": 0.25
},
"manual_drag": {
"type": "number",
"label": "Manual drag",
"description": "Multiplies native manual balance linear drag. 1x preserves stock.",
"default": 1,
"min": 0.25,
"max": 4,
"step": 0.25
},
"hud": {
"type": "choice",
"label": "Telemetry HUD",
"default": "full",
"choices": [
"off",
"compact",
"full"
],
"description": "Live board speed and session statistics. Full adds position, altitude and event counts."
},
"units": {
"type": "choice",
"label": "Speed units",
"default": "km/h",
"choices": [
"km/h",
"mph",
"m/s"
],
"description": "Display conversion only; does not alter physics."
},
"trail": {
"type": "boolean",
"label": "Breadcrumb trail",
"default": false,
"description": "Leave up to 24 small visual markers behind you. No collision. Cleared on map change."
},
"trail_spacing": {
"type": "number",
"label": "Trail spacing (m)",
"default": 6,
"min": 1,
"max": 30,
"step": 1,
"description": "Minimum distance between breadcrumb markers."
},
"marker_color": {
"type": "choice",
"label": "Marker colour",
"default": "cyan",
"choices": [
"cyan",
"orange",
"purple"
],
"description": "Colour of newly placed checkpoints, breadcrumbs and beacons."
},
"hud_label": {
"type": "string",
"label": "HUD label",
"default": "TRAINER",
"description": "An editable label demonstrating string settings."
},
"show_help": {
"type": "boolean",
"label": "Show key hints",
"default": true,
"description": "Show current shortcut bindings in the HUD."
},
"save_key": {
"type": "choice",
"label": "Save checkpoint key",
"default": "F5",
"choices": [
"F5",
"KeyJ",
"KeyK",
"KeyL"
],
"description": "Choose distinct shortcuts. If keys coincide, save, return, timer, reset, beacon run in that order."
},
"return_key": {
"type": "choice",
"label": "Return checkpoint key",
"default": "F6",
"choices": [
"F6",
"KeyJ",
"KeyK",
"KeyL"
],
"description": "Choose distinct shortcuts. If keys coincide, save, return, timer, reset, beacon run in that order."
},
"timer_key": {
"type": "choice",
"label": "Stopwatch key",
"default": "F7",
"choices": [
"F7",
"KeyJ",
"KeyK",
"KeyL"
],
"description": "Choose distinct shortcuts. If keys coincide, save, return, timer, reset, beacon run in that order."
},
"reset_key": {
"type": "choice",
"label": "Reset / clear key",
"default": "F8",
"choices": [
"F8",
"KeyJ",
"KeyK",
"KeyL"
],
"description": "Choose distinct shortcuts. If keys coincide, save, return, timer, reset, beacon run in that order."
},
"beacon_key": {
"type": "choice",
"label": "Place beacon key",
"default": "F9",
"choices": [
"F9",
"KeyJ",
"KeyK",
"KeyL"
],
"description": "Choose distinct shortcuts. If keys coincide, save, return, timer, reset, beacon run in that order."
},
"hold_fakie": {
"type": "boolean",
"label": "Hold fakie stance",
"description": "Prevents the automatic idle/bump stance-switch animation. Normal steering, slides and tricks remain available. Turn off to restore stock correction.",
"default": false
}
}
}
+185
View File
@@ -0,0 +1,185 @@
local box_key = "crate"
local mass = 25
local friction = 0.35
local spawned = false
local attached = false
local prev = { e = false, r = false }
local log_cooldown = 0
local peak_speed = 0
local samples = 0
local function breakaway_newtons()
return friction * mass * 9.81
end
local function push_newtons()
return sdk.settings.push_newtons or 350
end
local function ground_y(x, z)
local hit = sdk.physics.raycast(
{ x, 500, z },
{ 0, -1, 0 },
{ max_distance = 1000, filter = "ground" }
)
if hit and hit.point then return hit.point[2] end
return 0
end
local function touching_ground(key)
for _, t in ipairs(sdk.physics.touching() or {}) do
if (t.a == key and t.b == "ground") or (t.b == key and t.a == "ground") then
return true
end
end
return false
end
local function spawn_box()
if spawned then return end
local p = sdk.player.read().position
local gy = ground_y(p[1] + 3, p[3] + 3)
sdk.physics.spawn(box_key, {
shape = { type = "box", half_extents = { 0.5, 0.5, 0.5 } },
body_type = "dynamic",
mass = mass,
position = { p[1] + 3, gy + 0.55, p[3] + 3 },
heading = 0,
friction = friction,
ccd = true,
})
sdk.graphics.mesh("crate_vis", {
body = box_key,
scale = { 1, 1, 1 },
color = { 0.2, 0.65, 0.95 },
})
sdk.camera.follow(box_key, { 0, 3, -7 })
sdk.log(string.format(
"physics-sandbox: crate mass=%.0fkg friction=%.2f breakaway≈%.0fN push=%dN",
mass, friction, breakaway_newtons(), push_newtons()
))
spawned = true
peak_speed = 0
samples = 0
end
local function reset_box()
sdk.physics.remove(box_key)
sdk.graphics.remove("crate_vis")
if attached then
sdk.player.detach()
attached = false
end
spawned = false
spawn_box()
end
local function cmd_force()
local force = push_newtons()
local f = { 0, 0, 0 }
local keys = {}
if sdk.input.down("KeyW") then f[3] = f[3] + force; keys[#keys + 1] = "W" end
if sdk.input.down("KeyS") then f[3] = f[3] - force; keys[#keys + 1] = "S" end
if sdk.input.down("KeyA") then f[1] = f[1] - force; keys[#keys + 1] = "A" end
if sdk.input.down("KeyD") then f[1] = f[1] + force; keys[#keys + 1] = "D" end
return f, table.concat(keys, "")
end
return {
on_load = function()
sdk.ui.text("help", "Physics Sandbox — WASD push · E attach/detach · R reset")
spawn_box()
end,
on_fixed_update = function(ev)
if not spawned then
spawn_box()
end
local f, keys = cmd_force()
local driving = f[1] ~= 0 or f[3] ~= 0
if driving then
sdk.physics.force(box_key, f)
end
local e = sdk.input.down("KeyE")
if e and not prev.e then
if attached then
sdk.player.detach()
attached = false
sdk.log("detached")
else
sdk.player.attach(box_key, { 0, 1.1, 0 })
attached = true
sdk.log("attached to crate")
end
end
prev.e = e
local r = sdk.input.down("KeyR")
if r and not prev.r then
reset_box()
end
prev.r = r
local body = sdk.physics.read(box_key)
if not body then
return
end
samples = samples + 1
local speed = body.speed or 0
if speed > peak_speed then
peak_speed = speed
end
local rf = body.force or { 0, 0, 0 }
local p = body.position
local v = body.linvel
local need = breakaway_newtons()
local stuck = driving and speed < 0.05 and push_newtons() < need
sdk.ui.text(
"status",
string.format(
"pos %.1f %.1f %.1f | vel %.2f %.2f %.2f | speed %.2f (peak %.2f)\ncmd force %.0f %.0f %.0f | read force %.0f %.0f %.0f | keys %s | μmg≈%.0fN | gnd %s%s",
p[1], p[2], p[3],
v[1], v[2], v[3],
speed, peak_speed,
f[1], f[2], f[3],
rf[1], rf[2], rf[3],
keys ~= "" and keys or "-",
need,
touching_ground(box_key) and "yes" or "no",
stuck and " | STUCK: raise Push force" or ""
)
)
log_cooldown = log_cooldown - (ev and ev.dt or (1 / 60))
local runaway = (not driving) and speed > 1.5 and speed >= peak_speed * 0.98
if log_cooldown <= 0 or runaway or stuck then
log_cooldown = (runaway or stuck) and 0.25 or 1.0
sdk.log(string.format(
"crate tick=%d pos=(%.2f,%.2f,%.2f) vel=(%.2f,%.2f,%.2f) speed=%.2f cmdF=(%.0f,%.0f,%.0f) readF=(%.0f,%.0f,%.0f) keys=%s stuck=%s",
samples,
p[1], p[2], p[3],
v[1], v[2], v[3],
speed,
f[1], f[2], f[3],
rf[1], rf[2], rf[3],
keys ~= "" and keys or "-",
stuck and "yes" or "no"
))
end
end,
on_event = function(ev)
if ev.name == "world_changed" then
spawned = false
attached = false
prev.e = false
prev.r = false
peak_speed = 0
samples = 0
end
end,
}
+20
View File
@@ -0,0 +1,20 @@
{
"id": "examples.physics-sandbox",
"api": 2,
"name": "Physics Sandbox",
"version": "0.1.1",
"author": "Skate3clone",
"description": "Demonstrates the physics/graphics layer: spawn a dynamic box, push it with WASD, attach with E.",
"entry": "main.lua",
"settings": {
"push_newtons": {
"type": "number",
"label": "Push force (N)",
"description": "Horizontal force while holding WASD. Must exceed friction≈μmg (about 60–100N for this crate) or the box will not slide.",
"default": 350,
"min": 50,
"max": 1200,
"step": 10
}
}
}
+635
View File
@@ -0,0 +1,635 @@
N-- Propulsion is exclusively axle-joint motors plus Rapier tire friction.
-- Steering is exclusively front revolute motors. Suspension is four
-- prismatic joints with physical springs. No drive/steering forces or impulses.
local MODEL = "skyline.glb"
local CHASSIS = "chassis"
local SEAT = {-0.38, 0.12, -0.23}
local CAMERA = {0, 2.1, -5.8}
local ENTER_RANGE = 8.0
local WHEEL_RADIUS = 0.34
local WHEEL_HALF_WIDTH = 0.146
local SUSPENSION_TRAVEL = 0.30
local SUSPENSION_STIFFNESS = 26000
local SUSPENSION_DAMPING = 5600
local STEER_LIMIT = 0.65
local WHEELBASE = 2.7906
local TRACK_WIDTH = 1.591
local FINAL_DRIVE = 3.545
local GEAR_RATIOS = {3.827, 2.360, 1.685, 1.312, 1.000, 0.793}
local GEAR_TORQUE_COMPENSATION = {1.0, 2.0, 4.0, 5.0, 6.0, 7.0}
local REVERSE_RATIO = 3.280
local IDLE_RPM = 900
local REDLINE_RPM = 8000
local SHIFT_TIME = 0.18
local DRIVELINE_EFFICIENCY = 0.85
-- Converts RB26 crank torque to Rapier's constrained axle-motor force scale.
local JOINT_TORQUE_CALIBRATION = 2.5
local TWO_PI = math.pi * 2
local WHEELS = {
{ key = "wheel_rl", local_pos = { 0.795353, -0.330587, -1.347033 }, driven = true, steer = false, drive_share = 0.45 },
{ key = "wheel_rr", local_pos = {-0.796153, -0.330635, -1.347024 }, driven = true, steer = false, drive_share = 0.45 },
{ key = "wheel_fl", local_pos = { 0.794235, -0.330559, 1.443641 }, driven = true, steer = true, drive_share = 0.05 },
{ key = "wheel_fr", local_pos = {-0.796149, -0.330652, 1.443556 }, driven = true, steer = true, drive_share = 0.05 },
}
local spawned = false
local occupied = false
local pending_spawn = false
local previous = {}
local previous_buttons = 0
local previous_y = false
local elapsed = 0
local enter_after = 0
local gear = 1
local shift_timer = 0
local pending_pose = nil
local pending_reenter = false
local reset_was_down = false
local function pad()
return sdk.input.pad() or {
buttons = 0,
triggers = {0, 0},
left = {0, 0},
right = {0, 0},
}
end
local function down(key)
return sdk.input.down(key) == true
end
local function pressed(key)
local held = down(key)
local edge = held and not previous[key]
previous[key] = held
return edge
end
local function y_pressed()
local buttons = math.floor(tonumber(pad().buttons) or 0)
local held = (buttons & 0x8000) ~= 0
local edge = held and not previous_y
previous_y = held
return edge
end
local function clamp(value, low, high)
if value < low then return low end
if value > high then return high end
return value
end
local function wheel_collider()
local points = {}
for index = 0, 63 do
local angle = TWO_PI * index / 64
local y = math.cos(angle) * WHEEL_RADIUS
local z = math.sin(angle) * WHEEL_RADIUS
points[#points + 1] = {-WHEEL_HALF_WIDTH, y, z}
points[#points + 1] = { WHEEL_HALF_WIDTH, y, z}
end
return {type = "convex", points = points}
end
local function quat_yaw(q)
return math.atan(2 * (q[4] * q[2] + q[1] * q[3]),
1 - 2 * (q[2] * q[2] + q[3] * q[3]))
end
local function gear_ratio()
if gear == -1 then return REVERSE_RATIO * FINAL_DRIVE end
if gear == 0 then return nil end
return GEAR_RATIOS[gear] * FINAL_DRIVE
end
local function gear_label()
if gear == -1 then return "R" end
if gear == 0 then return "N" end
return tostring(gear)
end
local function engine_torque(rpm)
local curve = {
{900, 210},
{2000, 300},
{3000, 350},
{4400, 392},
{6800, 380},
{8000, 300},
}
rpm = clamp(rpm, curve[1][1], curve[#curve][1])
for index = 2, #curve do
local high = curve[index]
if rpm <= high[1] then
local low = curve[index - 1]
local t = (rpm - low[1]) / (high[1] - low[1])
return low[2] + (high[2] - low[2]) * t
end
end
return curve[#curve][2]
end
local function yaw_quat(heading)
local half = heading * 0.5
return {0, math.sin(half), 0, math.cos(half)}
end
local function rotate(q, v)
local x, y, z, w = q[1], q[2], q[3], q[4]
local ix = w * v[1] + y * v[3] - z * v[2]
local iy = w * v[2] + z * v[1] - x * v[3]
local iz = w * v[3] + x * v[2] - y * v[1]
local iw = -x * v[1] - y * v[2] - z * v[3]
return {
ix * w + iw * -x + iy * -z - iz * -y,
iy * w + iw * -y + iz * -x - ix * -z,
iz * w + iw * -z + ix * -y - iy * -x,
}
end
local function relative_yaw(a, b)
local ax, ay, az, aw = -a[1], -a[2], -a[3], a[4]
local bx, by, bz, bw = b[1], b[2], b[3], b[4]
local x = aw * bx + ax * bw + ay * bz - az * by
local y = aw * by - ax * bz + ay * bw + az * bx
local z = aw * bz + ax * by - ay * bx + az * bw
local w = aw * bw - ax * bx - ay * by - az * bz
return math.atan(2 * (w * y + x * z), 1 - 2 * (y * y + z * z))
end
local function add(a, b)
return {a[1] + b[1], a[2] + b[2], a[3] + b[3]}
end
local function terrain_y(x, z)
local hit = sdk.physics.raycast(
{x, 500, z},
{0, -1, 0},
{max_distance = 1000, filter = "ground"}
)
return hit and hit.point and hit.point[2] or nil
end
local function carrier_key(wheel)
return "carrier_" .. wheel.key
end
local function knuckle_key(wheel)
return "knuckle_" .. wheel.key
end
local function suspension_key(wheel)
return "suspension_" .. wheel.key
end
local function axle_key(wheel)
return "axle_" .. wheel.key
end
local function steering_key(wheel)
return "steering_" .. wheel.key
end
local function wheel_contact(key)
for _, pair in ipairs(sdk.physics.touching() or {}) do
if (pair.a == key and pair.b == "ground")
or (pair.b == key and pair.a == "ground") then
return true
end
end
return false
end
local function stop_motor(key)
sdk.physics.joint_motor(key, {
mode = "velocity",
target_velocity = 0,
factor = 0,
max_force = 0,
})
end
local function steer_to(wheel, target)
local carrier = sdk.physics.read(carrier_key(wheel))
local knuckle = sdk.physics.read(knuckle_key(wheel))
local current = 0
if carrier and carrier.rotation and knuckle and knuckle.rotation then
current = relative_yaw(carrier.rotation, knuckle.rotation)
end
sdk.physics.joint_motor(steering_key(wheel), {
mode = "velocity",
target_velocity = clamp((target - current) * 10, -5, 5),
factor = 5000,
max_force = 30000,
})
end
local function steering_targets(input, speed)
local angle = -input * STEER_LIMIT / (1 + speed * speed * 0.0015)
if math.abs(angle) < 0.001 then
return {wheel_fl = 0, wheel_fr = 0}
end
local sign = angle < 0 and -1 or 1
local radius = WHEELBASE / math.tan(math.abs(angle))
local inner = math.atan(WHEELBASE / math.max(0.2, radius - TRACK_WIDTH * 0.5))
local outer = math.atan(WHEELBASE / (radius + TRACK_WIDTH * 0.5))
if sign < 0 then
return {wheel_fl = -outer, wheel_fr = -inner}
end
return {wheel_fl = inner, wheel_fr = outer}
end
local function wheel_omega(wheel)
local body = sdk.physics.read(wheel.key)
if not body or not body.rotation or not body.angvel then return 0 end
local axle = rotate(body.rotation, {1, 0, 0})
return body.angvel[1] * axle[1]
+ body.angvel[2] * axle[2]
+ body.angvel[3] * axle[3]
end
local function driven_wheel_omega()
local total, count = 0, 0
for _, wheel in ipairs(WHEELS) do
if wheel.driven then
total = total + math.abs(wheel_omega(wheel))
count = count + 1
end
end
return count > 0 and total / count or 0
end
local function remove_rig()
sdk.player.detach()
sdk.camera.clear_follow()
for _, wheel in ipairs(WHEELS) do
sdk.physics.remove_joint(axle_key(wheel))
sdk.physics.remove_joint(suspension_key(wheel))
if wheel.steer then
sdk.physics.remove_joint(steering_key(wheel))
sdk.physics.remove(knuckle_key(wheel))
end
sdk.physics.remove(wheel.key)
sdk.physics.remove(carrier_key(wheel))
end
sdk.physics.remove(CHASSIS)
sdk.graphics.remove("skyline_visual")
spawned = false
occupied = false
end
local function spawn_rig()
local player = sdk.player.read()
local pose = pending_pose
pending_pose = nil
local heading = pose and pose.heading or player.heading or 0
local rotation = yaw_quat(heading)
local forward = rotate(rotation, {0, 0, 1})
local x = pose and pose.x or player.position[1] + forward[1] * 4
local z = pose and pose.z or player.position[3] + forward[3] * 4
local chassis_y = -math.huge
for _, wheel in ipairs(WHEELS) do
local offset = rotate(rotation, wheel.local_pos)
local ground = terrain_y(x + offset[1], z + offset[3])
if not ground then
sdk.log("skyline: no Rapier terrain below wheel " .. wheel.key)
return false
end
chassis_y = math.max(chassis_y, ground + WHEEL_RADIUS - offset[2])
end
local chassis_position = {x, chassis_y, z}
sdk.physics.spawn(CHASSIS, {
shape = {type = "mesh", path = MODEL, object = "skyline_mesh"},
body_type = "dynamic",
mass = 1400,
position = chassis_position,
heading = heading,
friction = 0.45,
membership = 1,
filter = 1,
ccd = true,
center_of_mass = {0, -0.35, -0.18},
inertia_half_extents = {1.02, 0.61, 2.43},
linear_damping = 0.025,
angular_damping = 0.12,
})
local tire_friction = sdk.settings.tire_friction or 1.3
for _, wheel in ipairs(WHEELS) do
local center = add(chassis_position, rotate(rotation, wheel.local_pos))
local carrier = carrier_key(wheel)
sdk.physics.spawn(carrier, {
shape = {type = "sphere", radius = 0.055},
body_type = "dynamic",
mass = 30,
position = center,
heading = heading,
friction = 0,
sensor = true,
ccd = true,
inertia_half_extents = {0.18, 0.18, 0.18},
linear_damping = 0.05,
angular_damping = 0.2,
})
sdk.physics.spawn(wheel.key, {
-- Smooth, GLB-dimensioned tire proxy. The authored tread is visually
-- correct but produces polygon impacts when used as a rolling collider.
shape = wheel_collider(),
body_type = "dynamic",
mass = 28,
position = center,
heading = heading,
friction = tire_friction,
membership = 2,
filter = 2,
ccd = true,
linear_damping = 0.01,
angular_damping = 0.015,
})
sdk.physics.prismatic(suspension_key(wheel), {
body_a = CHASSIS,
body_b = carrier,
anchor_a = wheel.local_pos,
anchor_b = {0, 0, 0},
axis = {0, -1, 0},
limits = {-SUSPENSION_TRAVEL, SUSPENSION_TRAVEL},
contacts_enabled = false,
})
sdk.physics.joint_spring(suspension_key(wheel), {
position = 0,
stiffness = SUSPENSION_STIFFNESS,
damping = SUSPENSION_DAMPING,
max_force = 18000,
})
local axle_parent = carrier
if wheel.steer then
local knuckle = knuckle_key(wheel)
sdk.physics.spawn(knuckle, {
shape = {type = "sphere", radius = 0.05},
body_type = "dynamic",
mass = 24,
position = center,
heading = heading,
friction = 0,
sensor = true,
ccd = true,
inertia_half_extents = {0.20, 0.22, 0.20},
linear_damping = 0.05,
angular_damping = 0.2,
})
sdk.physics.revolute(steering_key(wheel), {
body_a = carrier,
body_b = knuckle,
anchor_a = {0, 0, 0},
anchor_b = {0, 0, 0},
axis = {0, 1, 0},
limits = {-STEER_LIMIT, STEER_LIMIT},
contacts_enabled = false,
})
sdk.physics.joint_motor(steering_key(wheel), {
mode = "velocity",
target_velocity = 0,
factor = 5000,
max_force = 30000,
})
axle_parent = knuckle
end
sdk.physics.revolute(axle_key(wheel), {
body_a = axle_parent,
body_b = wheel.key,
anchor_a = {0, 0, 0},
anchor_b = {0, 0, 0},
axis = {1, 0, 0},
contacts_enabled = false,
})
stop_motor(axle_key(wheel))
end
sdk.graphics.mesh("skyline_visual", {path = MODEL, body = CHASSIS})
spawned = true
gear = 1
shift_timer = 0
previous_buttons = 0
previous_y = false
reset_was_down = false
enter_after = elapsed + 0.5
if pending_reenter then
sdk.player.attach(CHASSIS, SEAT)
sdk.camera.follow(CHASSIS, CAMERA)
occupied = true
end
pending_reenter = false
sdk.log("skyline: spawned physical 11-body/10-joint assembly")
return true
end
local function request_respawn()
if spawned then
remove_rig()
end
pending_pose = nil
pending_reenter = false
pending_spawn = true
end
local function request_reset(car)
pending_pose = {
x = car.position[1],
z = car.position[3],
heading = quat_yaw(car.rotation),
}
pending_reenter = occupied
remove_rig()
pending_spawn = true
end
local function drive(car, dt)
local controller = pad()
local triggers = controller.triggers or {0, 0}
local throttle = clamp(
(tonumber(triggers[2]) or 0) + (down("KeyW") and 1 or 0),
0,
1
)
local steer = clamp(
((controller.left or {0, 0})[1] or 0)
+ (down("KeyD") and 1 or 0) - (down("KeyA") and 1 or 0),
-1,
1
)
local buttons = math.floor(tonumber(controller.buttons) or 0)
local rb = (buttons & 0x0200) ~= 0
local lb = (buttons & 0x0100) ~= 0
local old_rb = (previous_buttons & 0x0200) ~= 0
local old_lb = (previous_buttons & 0x0100) ~= 0
if rb and not old_rb and gear < #GEAR_RATIOS then
gear = gear + 1
shift_timer = SHIFT_TIME
elseif lb and not old_lb and gear > -1 then
gear = gear - 1
shift_timer = SHIFT_TIME
end
previous_buttons = buttons
shift_timer = math.max(0, shift_timer - dt)
local brake_amount = clamp(
(tonumber(triggers[1]) or 0) + (down("KeyS") and 1 or 0),
0,
1
)
local foot_braking = brake_amount > 0.01 or down("Space")
or ((buttons & 0x1000) ~= 0)
local handbrake = down("ShiftLeft") or ((buttons & 0x2000) ~= 0)
local ratio = gear_ratio()
local wheel_rpm = driven_wheel_omega() * 60 / TWO_PI
local raw_rpm = ratio and wheel_rpm * ratio or 0
local rpm = ratio and clamp(raw_rpm, IDLE_RPM, REDLINE_RPM) or IDLE_RPM
-- Keep the velocity servo out of the torque curve. Engine torque is capped
-- separately at redline; targeting redline directly would taper motor force
-- thousands of RPM early as velocity error shrinks.
local target_omega = ratio and REDLINE_RPM * 1.5 * TWO_PI / 60 / ratio or 0
if gear == -1 then target_omega = -target_omega end
local on_limiter = raw_rpm >= REDLINE_RPM
local total_axle_torque = ratio
and engine_torque(rpm) * ratio * DRIVELINE_EFFICIENCY * JOINT_TORQUE_CALIBRATION
* (gear > 0 and GEAR_TORQUE_COMPENSATION[gear] or 1)
or 0
local steering = steering_targets(steer, car.speed or 0)
for _, wheel in ipairs(WHEELS) do
if wheel.steer then
steer_to(wheel, steering[wheel.key])
end
if wheel.driven then
if foot_braking or (handbrake and not wheel.steer) then
sdk.physics.joint_motor(axle_key(wheel), {
mode = "velocity",
target_velocity = 0,
factor = 120,
max_force = handbrake and not wheel.steer and 12000 or 9000,
})
elseif handbrake then
stop_motor(axle_key(wheel))
elseif throttle > 0.01 and shift_timer <= 0 and ratio and not on_limiter then
-- ATTESA closes toward even AWD for a straight launch, then releases
-- the front axle during cornering so throttle can rotate the rear.
local drive_share = 0.25
if math.abs(steer) > 0.15 then
drive_share = wheel.steer and 0.05 or 0.45
end
sdk.physics.joint_motor(axle_key(wheel), {
mode = "velocity",
target_velocity = target_omega,
factor = 10000,
max_force = total_axle_torque * drive_share * throttle,
})
else
stop_motor(axle_key(wheel))
end
end
end
if sdk.settings.show_debug ~= false then
local contacts = 0
for _, wheel in ipairs(WHEELS) do
if wheel_contact(wheel.key) then contacts = contacts + 1 end
end
sdk.ui.text("skyline_status", string.format(
"Skyline physics | %.0f km/h | gear %s | %.0f rpm | tires %d/4 | throttle %.0f%% | steer %.0f%%",
(car.speed or 0) * 3.6,
gear_label(),
rpm,
contacts,
throttle * 100,
steer * 100
))
end
end
return {
on_load = function()
sdk.ui.text("skyline_help", "Skyline — F10 spawn · Y/E enter · RT throttle · LT/A brake · B/Shift handbrake · RB/LB shift · R/R3 reset")
end,
on_unload = function()
remove_rig()
end,
on_fixed_update = function(event)
elapsed = elapsed + (event and event.dt or 1 / 120)
if pressed("F10") then
request_respawn()
return
end
if pending_spawn then
pending_spawn = false
spawn_rig()
return
end
local car = sdk.physics.read(CHASSIS)
if not car then return end
local buttons = math.floor(tonumber(pad().buttons) or 0)
local reset_down = (buttons & 0x0080) ~= 0
local reset_pressed = reset_down and not reset_was_down
reset_was_down = reset_down
if pressed("KeyR") or reset_pressed then
request_reset(car)
return
end
local enter_pressed = pressed("KeyE")
local controller_enter = y_pressed()
if enter_pressed or controller_enter then
if occupied then
sdk.player.detach()
sdk.camera.clear_follow()
occupied = false
elseif elapsed >= enter_after then
local player = sdk.player.read()
local dx = car.position[1] - player.position[1]
local dz = car.position[3] - player.position[3]
if dx * dx + dz * dz <= ENTER_RANGE * ENTER_RANGE then
sdk.player.attach(CHASSIS, SEAT)
sdk.camera.follow(CHASSIS, CAMERA)
occupied = true
end
end
end
if occupied then
drive(car, event and event.dt or 1 / 120)
else
for _, wheel in ipairs(WHEELS) do
if wheel.driven then stop_motor(axle_key(wheel)) end
if wheel.steer then
steer_to(wheel, 0)
end
end
end
end,
on_event = function(event)
if event.name == "world_changed" then
spawned = false
occupied = false
pending_spawn = false
previous = {}
previous_buttons = 0
previous_y = false
gear = 1
shift_timer = 0
pending_pose = nil
pending_reenter = false
reset_was_down = false
end
end,
}
+26
View File
@@ -0,0 +1,26 @@
{
"id": "examples.skyline",
"api": 2,
"name": "Skyline Physics",
"version": "1.0.0",
"author": "Skate3clone",
"description": "GLB rigid-body Skyline: physical suspension, steering hinges, axle motors, and Rapier tire contact.",
"entry": "main.lua",
"settings": {
"tire_friction": {
"type": "number",
"label": "Tire friction",
"description": "Rapier friction coefficient on the four GLB wheel hulls.",
"default": 1.3,
"min": 0.2,
"max": 2,
"step": 0.05
},
"show_debug": {
"type": "boolean",
"label": "Physics HUD",
"description": "Show wheel contacts, speed, gear, engine RPM, and steering.",
"default": true
}
}
}
+202 -160
View File
@@ -1,191 +1,233 @@
---@meta
-- SDK 1 language-server declarations. Add this directory as a Lua workspace library.
-- This file is not executed. Runtime reference: docs/lua-modding.md.
---@alias Vec3 number[] Three finite components, 1-based, Y up, metres.
---@alias SettingValue boolean|number|string
---@class GrindSnapshot
---@field active boolean
---@field name string
---@field kind integer
---@field distance number
-- SDK 2 language-server declarations. Not executed at runtime.
---@alias Vec3 number[]
---@alias Quat number[] xyzw
---@class BodyDesc
---@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 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 position Vec3 Animation root position.
---@field velocity Vec3 Physical board velocity, including when offboard.
---@field heading number Animation-root yaw in radians, zero faces +Z.
---@field position Vec3
---@field velocity Vec3
---@field heading number
---@field on_board boolean
---@field state integer Native state identifier, not a trick name.
---@field state integer
---@field category integer
---@field bailing boolean
---@field grind GrindSnapshot
---@class MapSnapshot
---@field name string
---@field generation integer
---@class AnimationInfo
---@field bone_names string[]
---@field slots table<string, {fps:number,frame_count:integer}>
---@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 states table<string, table<string, table<string, any>>> mod_id → peer_id → key → value
---@field status string
---@class SDKSnapshot
---@field player PlayerSnapshot
---@field map MapSnapshot
---@field animation AnimationInfo
---@field network NetworkInfo
---@field map {name:string, generation:integer}
---@field tick integer
---@field keys table<string,boolean>
---@field actions number[] 18 values; Lua index 1 corresponds to native action 64.
---@field actions number[]
---@field pad PadSnapshot
---@field paused boolean
---@field replay boolean
---@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() Commands discarded; host always cleans up.
---@field on_update? fun(event:{dt:number}) Active render time; max dt 0.25 seconds.
---@field on_fixed_update? fun(event:{dt:number}) After native physics, before next input publication.
---@field on_settings? fun(event:{key:string,value:SettingValue}) sdk.settings already updated.
---@field on_event? fun(event:ModEvent)
---@class ModEvent
---@field name 'world_changed'|'bail_changed'|'player_state_changed'|'grind_changed'
---@field map? MapSnapshot world_changed only
---@field bailing? boolean bail_changed only
---@field previous? integer player_state_changed only
---@field state? integer player_state_changed only
---@field grind? GrindSnapshot grind_changed only
---@field on_unload? fun()
---@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 = 1,
---@type table<string,SettingValue>
api_version = 2,
---@type string
mod_id = "",
---@type table<string, any>
settings = {},
---@type SDKSnapshot
snapshot = {},
player = {}, input = {}, ui = {}, scene = {}, animation = {},
time = {elapsed = 0},
physics = {}, graphics = {}, player = {}, camera = {}, input = {}, ui = {}, net = {}, assets = {},
time = { elapsed = 0 },
}
---@param text string At most 2048 UTF-8 bytes; queues labelled INFO output.
---@param text string
function sdk.log(text) end
---@param path string Relative within mod root; max 256 KiB, UTF-8 only; errors on failure.
---@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}
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
---@return PlayerSnapshot
function sdk.player.read() end
---@param position Vec3 Components within ±100000 metres; author supplies a valid destination.
---@param heading number Radians around world Y; zero faces +Z.
---@param on_board boolean
function sdk.player.teleport(position, heading, on_board) end
---@param key string Bevy physical KeyCode name, e.g. KeyJ, F6, Space. Unknown returns false.
---@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 key string
---@return boolean
function sdk.input.down(key) end
---@param id integer Native published gameplay action 64..81; invalid ID raises an error.
---@param id integer
---@return number
function sdk.input.action(id) end
---@param key string Owner-local stable visual key, 1..64 lowercase ASCII letters/digits/._-
---@param text string At most 1024 UTF-8 bytes.
---@return PadSnapshot
function sdk.input.pad() end
---@param key string
---@param text string
function sdk.ui.text(key, text) end
---@param key string Same namespace as sdk.ui.text; repeated key updates/replaces.
---@param position Vec3
---@param size Vec3 Each component >0 and <=100 metres.
---@param color Vec3 Each sRGB component in 0..1.
function sdk.scene.cube(key, position, size, color) end
---@param key string Remove owner's text or cube; absent is a no-op.
function sdk.scene.remove(key) end
---@param key string Timer key, 1..64 bytes. Existing timer is replaced.
---@param seconds number Finite delay 0..86400 active Update seconds.
---@param callback function One-shot callback, part of the on_update transaction/quota.
function sdk.time.after(key, seconds, callback) end
---@param key string Absent is a no-op.
function sdk.time.cancel(key) end
---@return AnimationInfo
function sdk.animation.info() end
---@param path string Package-relative original body-clip JSON, <=4 MiB; host validates supported slots and exact timing.
function sdk.animation.replace(path) end
---@class TrainerTuning Numeric fields default to 1; multipliers of current stock settings.
---@field pop? number 0.25..4, native ground pop heights.
---@field grind_pop? number 0.25..4, native grind launch height.
---@field push_speed? number 0.25..4, push target and maximum pushable speed.
---@field push_power? number 0.25..4, native push velocity-change limits.
---@field braking? number 0.25..4, foot and tail brake force.
---@field steering? number 0.25..4, general steering scalar.
---@field wobble? number 0..2, speed wobble amplitude.
---@field offboard_jump? number 0.25..4, native biped launch height.
---@field grip? number 0.25..4, wheel static/dynamic and sideways slide friction.
---@field turn_power? number 0.25..4, native heading turn strength.
---@field hold_fakie? boolean Defaults false; suppress automatic fakie stance switching.
---@field manual_drag? number 0.25..4, manual balance linear drag.
sdk.trainer = {}
---@param tuning TrainerTuning Owned, reversible native tuning. Conflicting owner is rejected.
function sdk.trainer.apply(tuning) end
---@class VehicleControls
---@field pitch? number -1..1; airborne nose up to nose down (left stick vertical)
---@field throttle? number -1..1, reverse to forward
---@field steering? number -1..1, right to left
---@field brake? number 0..1
---@field handbrake? boolean
---@class VehicleTuning
---@field engine_volume? number 0..1; requires engine_audio.enabled in definition
---@field engine_force? number 0..100000 N
---@field max_speed? number 1..100 m/s, engine limit
---@field brake_impulse? number 0..10000
---@field steering_angle? number 0.01..1.2 radians
---@field tire_grip? number 0.1..20
---@class VehicleSnapshot
---@field position number[] metres
---@field rotation number[] quaternion xyzw
---@field heading number radians
---@field speed number signed m/s
---@field phase 'parked'|'entering'|'driving'|'exiting'
---@field occupied boolean
---@field ready boolean
sdk.vehicle = {}
---@param key string Owner-local key
---@param definition string Package-relative vehicle JSON
---@param position number[] World position
---@param heading? number Radians
function sdk.vehicle.spawn(key,definition,position,heading) end
---@param key string
---@return VehicleSnapshot?
function sdk.vehicle.read(key) end
---@param key string
---@param controls VehicleControls Refresh every fixed tick
function sdk.vehicle.control(key,controls) end
---@param key string
---@param tuning VehicleTuning
function sdk.vehicle.tune(key,tuning) end
---@param key string
function sdk.vehicle.enter(key) end
---@param key string
function sdk.vehicle.exit(key) end
---@param key string
---@param position number[]
---@param heading? number
function sdk.vehicle.reset(key,position,heading) end
---@param key string
function sdk.vehicle.remove(key) end
---@return table Normalized keyboard/controller axes plus interact and pad_buttons
function sdk.vehicle.input() end
---@class VehicleBailEvent
---@field name 'vehicle_bailed'
---@field owner string
---@field key string
---@field reason 'crash'|'rider_impact'|'inverted'
---@field position number[] World seat position at detection, metres
---@field velocity number[] Carried world velocity plus launch lift, m/s
---@field angular_velocity number[] World angular velocity, rad/s
-- Configure rider_safety in the vehicle definition; see docs/vehicle-sdk.md.
---@class NetworkInfo
---@field active boolean
---@field local_id string Keep 64-bit player IDs as strings. Offline: "0".
---@field is_host boolean
---@field states table<string,table<string,table<string,any>>> Mod ID -> peer ID -> state keys.
---@field status string Matching/limit diagnostics.
sdk.net = {}
---@return NetworkInfo
function sdk.net.info() end
---@param key string Stable owner-local key, 1..64 characters.
---@param value any JSON-compatible value, at most 512 encoded bytes; nil clears.
function sdk.net.publish(key,value) end
---@param peer string Player ID from sdk.net.info(), never converted to a number.
---@param key string
---@return any Latest value for this mod/peer/key, or nil.
function sdk.net.read(peer,key) end
-- State coalesces; this is not an exactly-once event channel.
-- World cubes and owned vehicles replicate automatically for matching enabled mods.
-- See docs/multiplayer-mods.md for ownership, collision and late-join behavior.
---@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