mirror of
https://github.com/SK8-ENGINE/skate-3-rust-engine.git
synced 2026-10-26 19:47:44 +00:00
Add Lua audio, dynamic vehicle cameras, dashboard, and Skyline updates
This commit is contained in:
1 parent
cb7968930f
commit
1399555ef2
140 files changed
+11400
-7917
No files matched your search
+19
-12
@@ -97,18 +97,25 @@ Do not ship a local `data` folder in release ZIPs.
|
||||
The release manifest includes fingerprints for core data, HUDs, character,
|
||||
environment and maps. They are derived from the packaged extractor sources and
|
||||
shared dependencies, not the game build number. An in-place program update keeps
|
||||
`data` intact. Next startup offers an asset refresh only when these fingerprints
|
||||
change. A HUD extractor edit refreshes HUDs without converting maps; changes to
|
||||
core VLT/animation decoding invalidate dependent groups too. Shared map/material
|
||||
parsers can invalidate both maps and environment assets. New converter modules
|
||||
must be assigned to their consuming groups in `asset_pipeline/versions.py`.
|
||||
`data` intact. Next startup compares those fingerprints (and any listed
|
||||
`pipeline_equivalence` pairs) with `data/installation.json`. Matching installs
|
||||
are reused immediately; equivalent fingerprints are stamped to the current
|
||||
release without opening setup. Setup opens only when groups actually need work
|
||||
or character preparation is outdated, and a refresh reuses the previously
|
||||
recorded Xbox source path when it still exists. A HUD extractor edit refreshes
|
||||
HUDs without converting maps; changes to core VLT/animation decoding invalidate
|
||||
dependent groups too. Shared map/material parsers can invalidate both maps and
|
||||
environment assets. New converter modules must be assigned to their consuming
|
||||
groups in `asset_pipeline/versions.py`.
|
||||
|
||||
Refresh uses the same `Select ISO or default.xex` file picker as initial setup;
|
||||
the player never needs to select an intermediate extraction folder. ISO sources
|
||||
must be unpacked again when a refresh needs disc files; unchanged asset groups
|
||||
are reused without conversion. The chosen Xbox executable
|
||||
must match the original edition. Source paths stay in local installation records
|
||||
and are never included in published release metadata.
|
||||
Refresh prefers the saved `default.xex` / ISO path from the installation record.
|
||||
Only if that path is missing or fails does it show the same
|
||||
`Select ISO or default.xex` file picker as initial setup. The player never needs
|
||||
to select an intermediate extraction folder. ISO sources must be unpacked again
|
||||
when a refresh needs disc files; unchanged asset groups are reused without
|
||||
conversion. The chosen Xbox executable must match the original edition. Source
|
||||
paths stay in local installation records and are never included in published
|
||||
release metadata.
|
||||
|
||||
Updates run only affected exports. Core/HUD/character/environment/map recipes
|
||||
have separate dependency identities; setup UI and transaction changes do not
|
||||
@@ -120,7 +127,7 @@ Core refreshes create a new installation before publication. Unchanged prepared
|
||||
maps and immutable character generations use hardlinks (copy fallback on filesystems without support); mutable user files and
|
||||
outputs being rebuilt have independent storage. Customiser-only refreshes keep
|
||||
the current core installation and publish a new character generation. Custom
|
||||
models, profiles, settings, selections and mods are retained. Core gameplay inputs and at least one map must pass validation before the
|
||||
models, profiles, settings and selections are retained. Core gameplay inputs and at least one map must pass validation before the
|
||||
installation record changes. Missing optional content is recorded separately;
|
||||
fatal failures preserve the previous record.
|
||||
A kernel lock prevents concurrent setup and releases automatically on process
|
||||
|
||||
@@ -1,267 +0,0 @@
|
||||
# Lua modding SDK 1
|
||||
|
||||
Start with [the agent instructions](../sdk/AGENTS.md) and [ZIP package guide](mod-packages.md).
|
||||
Use `PLAY.bat` / `Build.ps1` for the current integrated build;
|
||||
older named trainer/window launchers are historical snapshots. The project launcher
|
||||
loads top-level `mods/`, starts paused, and writes `.local/vehicle-sdk/session.log`.
|
||||
Prepared engine assets are required; Lua itself is embedded.
|
||||
|
||||
Open Escape/Start → Mods. New mods start disabled. Each has a red DISABLED or green
|
||||
ENABLED badge and explicit enable/disable action. Enabled mods have separate movable,
|
||||
resizable and collapsible settings windows with continuous scrolling and a scrollbar.
|
||||
The ordinary pause menu stays centred. Minus/plus controls change values; Tab/controller
|
||||
X cycles focus, arrows/D-pad select/adjust, and strings open the text editor. Changes
|
||||
are saved outside the package. Window layout is retained for the current process.
|
||||
|
||||
## Quick start
|
||||
|
||||
Copy `sdk/examples/native-trainer` into a new authoring folder, change its `id` and `name`, then package it with `tools/package_mod.py`. A ZIP contains:
|
||||
|
||||
```text
|
||||
my-mod.zip (archive root)/
|
||||
mod.json
|
||||
main.lua
|
||||
assets/ # optional original text data or authored body clip JSON
|
||||
```
|
||||
|
||||
`SKATE3_MODS` overrides the discovery directory; its default is the top-level `mods` folder **beside the running game executable**, independent of the working directory. The engine creates it if absent and logs its full path; the Mods list has an **Open mods folder** action. Immediate `.zip` files and non-hidden development folders are packages. ZIPs require mod.json at their root and are extracted automatically into a session cache. `SKATE3_MOD_SETTINGS` overrides the writable preferences directory; the default is `settings/mods` beside the engine asset directory. Keep preferences outside packages.
|
||||
|
||||
A minimal manifest:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "yourname.first-mod",
|
||||
"api": 1,
|
||||
"name": "My first mod",
|
||||
"version": "1.0.0",
|
||||
"author": "Your name",
|
||||
"description": "An original example.",
|
||||
"entry": "main.lua",
|
||||
"settings": {
|
||||
"greeting": {
|
||||
"type": "string", "label": "Greeting",
|
||||
"description": "Text shown on screen.", "default": "Hello skater"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```lua
|
||||
local function draw()
|
||||
sdk.ui.text('greeting', sdk.settings.greeting)
|
||||
end
|
||||
return {
|
||||
on_load = draw,
|
||||
on_settings = draw,
|
||||
on_event = function(event)
|
||||
if event.name == 'world_changed' then draw() end
|
||||
end
|
||||
}
|
||||
```
|
||||
|
||||
Edit the greeting in the menu; Lua receives it immediately. Edit and save the script; enabled mods reload after files settle. Disable it and its text disappears. Add `sdk.log('loaded')` for a labelled log line. Use `sdk/skate.lua` as a Lua language-server library (Lua 5.4); do not execute this annotation file or package it as your entry point.
|
||||
|
||||
## Manifest and settings reference
|
||||
|
||||
Unknown manifest/setting fields are errors, including `dependencies`, `conflicts`, `permissions` and `enabled`. SDK 1 does **not** implement inter-mod dependency resolution or cross-mod imports. Packages must be independent; do not depend on another mod's callback side effects. Conflicting IDs reject all copies, regardless of directory ordering. Conflicting animation slots reject the later installing mod. Packages execute in ascending, case-sensitive ID order.
|
||||
|
||||
All manifest fields in the minimal example except `settings` are required. `api` is the integer `1`, an exact compatibility major, not a range. `version` is three unsigned integer components `MAJOR.MINOR.PATCH`. IDs and setting/object keys contain 1–64 ASCII lower-case letters, digits, dot, underscore or hyphen; `.` and `..` alone and Windows device-name stems such as `con` or `aux` are invalid. Use a globally distinctive author prefix. IDs identify saved preferences across directory moves and upgrades. Name and author are at most 120 bytes; name is nonempty. Description is at most 2048 bytes. Entry is a relative UTF-8 Lua source file, at most 256 KiB. Bytecode is not accepted.
|
||||
|
||||
`settings` is an object keyed by stable setting ID (at most 24). Rows sort by key. Every schema entry requires `type`, `label`, `description`, and `default`. Label is at most 120 bytes and description at most 512 bytes. Types:
|
||||
|
||||
| Type | Value | Schema fields |
|
||||
| --- | --- | --- |
|
||||
| `boolean` | JSON/Lua boolean | `default` true or false |
|
||||
| `number` | finite JSON/Lua number | required finite `min`, `max`, positive `step`, and default in range |
|
||||
| `string` | string without control characters, at most 128 Unicode characters | `default` string |
|
||||
| `choice` | string equal to a listed choice | required nonempty `choices`, at most 32 strings, each at most 128 bytes |
|
||||
|
||||
`min`, `max`, `step` and `choices` are optional on other types and have no effect there. Number step controls the menu increment; values need not lie on that increment grid. Saved values are type/range validated. Missing, invalid or renamed values use defaults; removed keys are discarded on loading the current schema. Same-key compatible values survive version changes. Renaming a key intentionally resets it. There is no arbitrary Lua migration callback or persisted mutable Lua state in API 1. Changing the package ID creates a distinct preference identity. Reset delivers one `on_settings` notification per setting in key order. Preferences store `{ "enabled": boolean, "values": {...} }` as `<id>.json` through a sibling temporary file and rename. Save errors appear in the menu; the live edit may already have applied. Never distribute these preference files with a mod. Enable preference and actual running status differ after a fault: the error remains visible and the mod stays stopped until reload, a settled edit, or a future launch.
|
||||
|
||||
There is no generic gamepad rebinding setting yet. A choice containing supported key names, as an ordinary key-choice schema, is an ordinary validated setting; it does not remap native input.
|
||||
|
||||
## Lifecycle, ordering and scheduling
|
||||
|
||||
Each package owns a fresh Lua state. Entry code must return a table containing only these optional function fields:
|
||||
|
||||
| Callback | Argument | Timing |
|
||||
| --- | --- | --- |
|
||||
| `on_load` | nil | after source evaluation; top-level and load commands commit together |
|
||||
| `on_update` | `{dt:number}` | once per active rendered Update, after engine animation; real seconds clamped to 0.25 |
|
||||
| `on_fixed_update` | `{dt:number}` | after the existing fixed physics set; fixed time delta in seconds |
|
||||
| `on_event` | event table below | at the indicated host boundary |
|
||||
| `on_settings` | `{key:string,value:boolean\|number\|string}` | immediately after validation, with new `sdk.settings` already installed |
|
||||
| `on_unload` | nil | before dropping a running state, including normal host shutdown; commands emitted here are discarded |
|
||||
|
||||
Update/fixed callbacks pause during the pause menu and replay. Discovery, settings, load/unload and world notifications remain available while paused. Fixed callbacks observe the just-completed simulation; a teleport request feeds the existing actor reset path on subsequent input publication. Lua never executes on rendering/GPU or map-worker threads. Mods menu input precedes map publication; maintenance follows publication in PreUpdate. Update callbacks run after `FrameSet::Animation`. There is no deterministic lockstep promise across frame rates, filesystems or machines; use fixed callbacks for tick-count logic. Do not expect several fixed callbacks in one render frame to observe distinct keyboard states.
|
||||
|
||||
Within each dispatch, callbacks run by ascending mod ID. Commands are collected at the host boundary and grouped by ascending owner ID, preserving that owner's emission order, with its sole teleport deferred until its reversible commands succeed. At most one teleport per owner batch is accepted; if multiple mods request one, the first accepted request makes later ones fail as busy. Host failures stop the offending mod and retire its current owned resources. Log output cannot be retracted. Already completed teleports are gameplay actions and are not reversed when a mod is disabled later.
|
||||
|
||||
No callbacks accumulate across reload: there is one returned lifecycle table and one isolated state per running package. Timers and all Lua locals reset. On disable/reload/fault, owned text, meshes, materials, timers and animation overrides are removed. Old queued actions are discarded. `on_unload` errors cannot prevent cleanup. A Lua error discards the entire failing invocation's queue; source/load errors commit no commands. Prior reversible changes made by that mod are also retired when it stops. Lua memory mutations before an error are immaterial because that state is dropped.
|
||||
|
||||
Polling occurs at most every 500 ms and hashes bounded package contents (not just timestamps). Changed valid files must retain the same hash for at least 750 ms, so usual effective latency is 1–1.5 seconds. Invalid manifests get a 750 ms grace interval before the old package is retired and diagnostics replace its listing. Deleted directories retire immediately at the next poll. New packages are discovered disabled unless their ID already has a saved enable preference. Explicit Rescan/Reload bypasses debounce: finish saving before using it. A failed reload does not retain the previous version; it remains visibly stopped. Fix/save or explicitly reload to recover. Polling runs on the main thread, so large packages or slow storage can hitch; no zero-hitch guarantee is made. A root-directory I/O error reports diagnostics; a missing root retires its packages.
|
||||
|
||||
Successful world changes remove **all** mod objects and animation overrides before `world_changed` is delivered. Lua locals, settings and timers survive this event. Rebuild world-specific objects in the callback, cancel any world-specific timers, and clear stored positions when retaining world-specific positions. `sdk.snapshot.map.generation` identifies the current world, including reloading the same map. Failed map preparation retains the previous world and emits no world-change event.
|
||||
|
||||
## SDK API
|
||||
|
||||
The callable names/signatures are also in [sdk/skate.lua](../sdk/skate.lua).
|
||||
Vehicle commands, collider/mass/audio configuration and rider slots are documented
|
||||
in [vehicle-sdk.md](vehicle-sdk.md). Trainer tuning is documented below.
|
||||
|
||||
All vectors are 1-based Lua arrays `{x,y,z}` in engine world coordinates, metres, Y up. All API functions below use dot calls, not colon calls. Mutable snapshot/settings tables are per-state copies: editing them does not mutate the engine. Retained snapshots are stale; query again inside callbacks.
|
||||
|
||||
| Function/value | Return and effect |
|
||||
| --- | --- |
|
||||
| `sdk.api_version` | integer `1` |
|
||||
| `sdk.settings` | current validated setting-key → value table |
|
||||
| `sdk.snapshot` | current observation table described below |
|
||||
| `sdk.log(text:string)` | nil; queue labelled INFO diagnostic, maximum 2048 UTF-8 bytes |
|
||||
| `sdk.read_text(path:string)` | UTF-8 string; synchronously read a package-relative file, at most 256 KiB; raises on missing/invalid/traversing/non-UTF8 path or file |
|
||||
| `sdk.player.read()` | player snapshot table, never a live object |
|
||||
| `sdk.player.teleport(position:Vec3, heading:number, on_board:boolean)` | nil; queue native manual actor reset; radians about Y, zero facing +Z; preserves the separate bail and session-marker checkpoints |
|
||||
| `sdk.input.down(key:string)` | boolean; whether Bevy physical key name is held at snapshot time; unrecognized/unpressed names return false |
|
||||
| `sdk.input.action(id:integer)` | number from published native gameplay action 64–81; errors outside range |
|
||||
| `sdk.ui.text(key:string,text:string)` | nil; create/update owned plain screen text, max 1024 bytes; stable top-left rows sorted by owner/key, 19px font, 28px row spacing |
|
||||
| `sdk.scene.cube(key:string,position:Vec3,size:Vec3,color:Vec3)` | nil; create/update owned visual cuboid; no collision, grind attachment or rigid body |
|
||||
| `sdk.scene.remove(key:string)` | nil; remove owned cube/text; absent key is a no-op; cannot address another mod's objects |
|
||||
| `sdk.time.elapsed` | active Update seconds since state creation, advanced after `on_update` |
|
||||
| `sdk.time.after(key:string,seconds:number,callback:function)` | nil; replace keyed one-shot timer; delay 0–86400 finite seconds, key 1–64 bytes, at most 64 timers |
|
||||
| `sdk.time.cancel(key:string)` | nil; remove timer; absent key is a no-op |
|
||||
| `sdk.animation.info()` | `{bone_names:string[],slots:table<string,{fps:number,frame_count:integer}>}`; authoring metadata for installed supported body slots |
|
||||
| `sdk.animation.replace(path:string)` | nil; queue installation of package-relative body-clip JSON, at most 4 MiB; replaces this mod's previous set; conflicting owners or invalid clips stop the mod |
|
||||
|
||||
Timers run after `on_update`, within its same instruction budget and transaction. Due timer keys sort lexicographically. A timer is removed before invocation. Newly scheduled timers normally wait for the next Update (replacing an already due, not-yet-delivered key can affect that key's pending delivery). Pausing stops the timer clock. Errors disable the owner. `sdk.time.elapsed` and snapshot fields should be treated as read-only by authors.
|
||||
|
||||
Cube positions and teleport positions must be finite, each component within ±100000. Size components must be finite in `(0,100]`. RGB components must be finite in `[0,1]`, interpreted as sRGB. Heading must be finite. Text and cubes share an owner-local key namespace: switching type replaces the old object. At most 64 visual objects per mod; update existing keys to avoid churn. At most 128 commands per Lua invocation. Invalid arguments raise Lua errors; exceeding limits stops the mod. Commands return nil on successful **queuing**, not proof of engine completion. Host rejection appears in the mod's error panel and log. Teleports reject map loading, replay and busy actor-reset conditions. This is a free-position command, not a terrain-safe spawn finder: authors must select valid destinations. It does not set velocities directly or bypass the native deferred reset/camera-cut sequence.
|
||||
|
||||
### Snapshot and event payloads
|
||||
|
||||
`sdk.snapshot` contains:
|
||||
|
||||
```lua
|
||||
{
|
||||
player = {
|
||||
position = {x,y,z}, -- current animation-to-world root translation
|
||||
velocity = {x,y,z}, -- physical board velocity; not biped walking speed
|
||||
on_board = boolean, -- physical category != 500
|
||||
state = integer, category = integer, -- native identifiers, not a trick name
|
||||
bailing = boolean,
|
||||
grind = {active=boolean,name=string,kind=integer,distance=number}
|
||||
},
|
||||
map = {name=string,generation=integer},
|
||||
tick = integer, -- engine physics tick, may reset on map replacement
|
||||
keys = { [physical_key_name]=true },
|
||||
actions = {number,...}, -- 18 values, array slot 1 is native action 64
|
||||
paused = boolean, replay = boolean,
|
||||
animation = {bone_names={...},slots={...}}
|
||||
}
|
||||
```
|
||||
|
||||
Keyboard names follow Bevy 0.18 `KeyCode` debug names, e.g. `KeyA`, `KeyJ`, `F6`, `Space`, `ArrowUp`, `ShiftLeft`. Held-state reads do not consume input or inject native actions. Track the previous held state in a Lua local for edge detection. Native actions expose the current published mapping: 64/65 left stick axes, 66 left trigger, 67/68 right stick axes, 69 right trigger, 70/71 stick clicks, 72/73 shoulders, 74–77 face buttons, 78/79 Start/Back, 80/81 D-pad horizontal/vertical as provided by the existing expression map. These are observations, not a binding editor. Consult `crates/skate-core/src/input/gameplay_map.rs` for the retained source mapping; no new native input semantics are introduced.
|
||||
|
||||
`on_event` receives one of:
|
||||
|
||||
| `name` | Additional fields | Boundary |
|
||||
| --- | --- | --- |
|
||||
| `world_changed` | `map={name,generation}` | PreUpdate after successful map publication and mod resource retirement |
|
||||
| `bail_changed` | `bailing:boolean` | after fixed physics when observed bail flag changes |
|
||||
| `player_state_changed` | `previous:integer,state:integer` | after fixed physics when native state identifier changes |
|
||||
| `grind_changed` | `grind={active,name,kind,distance}` | after fixed physics when accepted grind active flag changes |
|
||||
|
||||
Fixed event order is bail, state, grind, then `on_fixed_update`. These are sampled transitions, not a complete native event bus: multiple transitions within one engine tick can be coalesced. Initial observations can emit state changes from the host's initial zero/false baseline. No trick-completed, combo or score event is promised by SDK 1; a native state number must not be relabelled as proof of a scored trick. The separate scoring owner can add a source-backed API in a future compatible revision.
|
||||
|
||||
### Body-animation asset format
|
||||
|
||||
This reuses `animation_pose/authored_clips.rs`, not a parallel animation system. `sdk.animation.info()` supplies exact installed names and timing; author original global joint transforms with your own tools and write:
|
||||
|
||||
```text
|
||||
{
|
||||
"version": 1,
|
||||
"bone_names": [exact installed skeleton order],
|
||||
"clips": {
|
||||
"supported slot name": {
|
||||
"fps": exact installed slot fps,
|
||||
"frames": [ [ [16 floats for bone 0], [16 floats for bone 1], ... ], ... ]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Matrices are affine, column-major 4×4 global joint transforms. Every frame must contain every bone, in exact name order. The existing parser requires finite matrices, positive determinants, scale components between 0.9 and 1.1, homogeneous last row approximately `(0,0,0,1)`, and exact stock frame count/FPS. It reconstructs board and helper targets and only substitutes its supported body range, leaving stock trajectory, board, timing, graph and channel metadata authoritative. Empty `clips` removes that owner's effective substitutions. Supported slots are `R_ANTIC_OLLIE_N_0_INTO`, `R_ANTIC_OLLIE_N_0_CYC`, `R_ANTIC_360SHUVIT_N_0_CYC`, `360FLIP_D_HIGH_G`, `360FLIP_D_HIGH_A`, `360FLIP_D_LOW_G`, `360FLIP_D_LOW_A`. No other slot is accepted. Call replacement at load and after world changes; parsing is synchronous and should not be repeated per frame. The pre-existing optional private replacement loader remains beneath mod overrides. This SDK does not activate or rename any disabled private clip or climbing file.
|
||||
|
||||
## Trust, assets, compatibility and limits
|
||||
|
||||
Only install mods from authors you trust. Isolation is an engine fault-containment boundary, **not** an OS sandbox or a promise to safely execute hostile code. There is no raw world/entity access, pointer API, FFI, raw socket/network access, writable Lua filesystem API, `io`, `os`, `debug`, `package`, `require`, coroutine library, `load`, `dofile`, `loadfile`, `pcall`, `xpcall`, or Lua GC controls. Standard base operations plus table/string/math/UTF8 are available. Removing protected calls prevents catching and endlessly retrying quota errors. Scripts must be UTF-8 source returning a callback table. Errors are reported rather than caught by mods.
|
||||
|
||||
Each Lua allocator is limited to 8 MiB and each entry evaluation/invocation to approximately 101000 VM instructions (hook every 1000 instructions). A long-running C library operation does not execute VM hooks; parsing, garbage collection, file I/O and host work are not hard wall-clock bounded. Aggregate mod load can still lower frame rate. There is no process isolation, hostile-filesystem race defense, hard timeout for native operations or guarantee against implementation bugs in dependencies. Package paths reject traversal and absolute paths, canonicalize within the root and reject escapes. Symlink entries are rejected during scanning; do not use junctions/links in mods. Do not mutate package trees maliciously while reading them.
|
||||
|
||||
Discovery inspects at most 128 sorted immediate packages (ZIPs or development folders).
|
||||
Packages are limited to 512 files/directories and 64 MiB; ZIP compressed/expanded bounds
|
||||
and path rules are in mod-packages.md. Lua allocator quotas exclude host asset storage,
|
||||
which is bounded separately. `sdk.read_text` reads UTF-8 package data; there is no general
|
||||
JSON decoder in Lua. Vehicle definitions and native animation JSON use dedicated host
|
||||
parsers. Vehicles support embedded GLB models and synthesized engine audio. Arbitrary
|
||||
scene/texture/font/audio-bank loading is not exposed. Text uses the existing engine font.
|
||||
|
||||
|
||||
No camera-controller override, native audio command, customiser-profile mutation, physics force injection, collision-geometry insertion, climbing controller installation, map-selection command, score manipulation, arbitrary network messaging, cross-mod messages or persistent Lua save store is exposed in API 1. Existing character customisation, native audio integration and private climbing loaders retain their own lifetimes. Broad engine-internal access is deliberately absent. Useful current extension types include HUDs, timers, route/training challenges, native-reset navigation tools, visual world annotations, grind/bail instrumentation and constrained original body animation replacement.
|
||||
|
||||
The runtime pins `mlua = 0.12.1`, with `lua54`, `vendored`, `serde`, `send`; Cargo.lock pins transitive build inputs. `send` supports Bevy Resource storage, but host scheduling keeps execution on the main thread. Lua is compiled into the application using the vendored static build; no player-side DLL/Lua install is required. The dependency has no Bevy coupling and compiles against the engine's Bevy 0.18.1/local patches. Release uses Windows MSVC with `-C target-feature=+crt-static` and no default game features. Primary selection references: [mlua source/readme](https://github.com/mlua-rs/mlua), [Lua memory and hook API](https://docs.rs/mlua/0.12.1/mlua/struct.Lua.html). A lockfile entry for the optional LuaJIT source builder does not enable LuaJIT: the selected language feature is Lua 5.4.
|
||||
|
||||
Distribute your package directory with source, original permitted data, a license/readme and engine/API requirements. Ship no preferences, local paths, dependency cache, executable or game assets. New compatible SDK functions may be added under API 1; removing/changing existing signatures requires a new API major. Unknown callback names and manifest fields are rejected now, so adding them to an old host fails visibly. Lua/package errors are visible under the selected mod; directory/schema errors are on the list page. If a hot edit fails, fix it and save or Reload. Reload and settings changes are intentionally usable while gameplay is paused.
|
||||
|
||||
## Validation and build
|
||||
|
||||
```powershell
|
||||
cargo test --locked -p skate-mods -p skate-vehicles
|
||||
cargo run --locked -p skate-mods --example check_mod -- path/to/mod.zip
|
||||
python tools/package_mod.py sdk/examples/native-trainer mods/native-trainer.zip
|
||||
```
|
||||
|
||||
The checker validates package bounds, manifest/settings and Lua syntax. It does not
|
||||
execute callbacks, launch gameplay, or prove native animation/handling correctness.
|
||||
ZIP integration tests cover asset reads, replacement, deletion, duplicate IDs, invalid
|
||||
paths and expansion limits. Validate in-game behaviour manually after the headless checks.
|
||||
|
||||
## Native trainer and examples
|
||||
|
||||
`sdk.trainer.apply(tuning)` installs one owner's reversible native tuning. Only one mod
|
||||
can own it at a time. Numeric fields default to 1 on each call; `hold_fakie` defaults false.
|
||||
Supported multipliers are pop, grind_pop, push_speed, push_power, braking, steering,
|
||||
offboard_jump, grip, turn_power and manual_drag (0.25..4), and wobble (0..2).
|
||||
`hold_fakie=true` suppresses the automatic stance switch. Unknown/nonfinite values fail.
|
||||
Disable/reload/fault restores defaults; native contact/state conditions still apply.
|
||||
These are parameter multipliers, not guarantees of measured speed or height.
|
||||
|
||||
`sdk/examples/native-trainer` demonstrates tuning, HUD text, checkpoints, timers,
|
||||
breadcrumbs, beacons and configurable shortcuts. Defaults: F5 save checkpoint, F6 return,
|
||||
F7 stopwatch, F8 clear transient data, F9 beacon. Its text data stays in the package.
|
||||
`sdk/examples/mario-kart` demonstrates vehicles, controller binds, driving HUD, fitted
|
||||
rider animations, steering, ramp collision tuning and engine audio. Both complete
|
||||
example ZIPs are committed in top-level mods/, including the prepared kart model and rider clips.
|
||||
|
||||
All owner-created visuals are removed on disable, reload or fault. Native skating and
|
||||
vehicle physics remain host-controlled; Lua does not receive memory pointers or World.
|
||||
|
||||
## Multiplayer
|
||||
|
||||
The integrated launcher includes native multiplayer. Matching enabled mod packages
|
||||
automatically replicate world cubes and vehicles. Trainer/animation/teleport results
|
||||
follow native player replication; UI and input remain local. `sdk.net.info()`,
|
||||
`sdk.net.publish(key,value)` and `sdk.net.read(peer,key)` expose bounded, owner-scoped
|
||||
latest state for custom Lua rules. This is not an exactly-once event API.
|
||||
|
||||
Read [Multiplayer mod SDK](multiplayer-mods.md) before authoring shared scores,
|
||||
races or world objects. It specifies exact matching, string player IDs, limits,
|
||||
collision authority, cleanup, host changes and supported versus local-only features.
|
||||
|
||||
|
||||
## Combined engine integration
|
||||
|
||||
The Mods menu sits alongside Updates, Teleport, Multiplayer and both character
|
||||
menus. Trainer grind-pop tuning applies to the current grind launch controller;
|
||||
normal settings retain native behavior. Existing map-change cleanup and crash
|
||||
reporting remain enabled. Protocol 5 is required for multiplayer mod state, so
|
||||
all peers must update together. The bundled trainer and kart packages are included;
|
||||
private authoring sources and per-feature development launchers are excluded.
|
||||
@@ -1,129 +0,0 @@
|
||||
# Mixamo to native vehicle animations
|
||||
|
||||
This is the workflow used for the working Mario Kart rider. The game plays a native
|
||||
Skate skeleton pose on the currently selected compatible character. It does not swap
|
||||
the rider to the mesh bundled in the Mixamo download. Other characters must first be
|
||||
converted to the supported native render rig; arbitrary skeletons are not interchangeable.
|
||||
Different proportions may still need contact adjustments to reach a wheel or pedals.
|
||||
|
||||
## 1. Keep a matched calibration pair
|
||||
|
||||
Use your owned stock `assets/private/skater.glb` as the native reference. Upload the
|
||||
same stock character mesh to Mixamo, rig it there, and retain its returned rest/T-pose
|
||||
with skin and skeleton. Convert that returned reference FBX to GLB without altering
|
||||
its bind pose (Blender import FBX, export glTF/GLB with skin). This paired stock return
|
||||
is the calibration source, not a Mario mesh or a posed entry/exit frame.
|
||||
|
||||
The converter's `--calibrate` path accepts a GLB. It verifies mesh bounds and records
|
||||
the reference hash, source hash, scale and Mixamo bind transforms. Example:
|
||||
|
||||
```powershell
|
||||
python tools/mixamo_to_skate/main.py returned-stock.glb --reference assets/private/skater.glb --profile calibration.json --calibrate --noninteractive
|
||||
```
|
||||
|
||||
Keep calibration.json with that exact stock reference. For this local kart, the matched
|
||||
profile is `your converter’s calibration.json`. Do not replace it
|
||||
with a profile built from another character. Calibration transfers the change from a
|
||||
Mixamo bind pose into the original native bind basis; copying raw rotations ignores
|
||||
bone axes and is what produced the forward shoulders in the earlier attempt.
|
||||
|
||||
## 2. Download the animations from the same Mixamo character
|
||||
|
||||
Select Entering Car and Exiting Car on that same uploaded stock character. Download FBX
|
||||
**with skin** for this pipeline, retaining its skeleton. The skin is useful for verifying
|
||||
the reference and is not used as the in-game rider. Use consistent frame rate and no
|
||||
keyframe reduction. Preserve root motion; these clips need to move between standing
|
||||
and seated positions. Keep the original downloads untouched.
|
||||
|
||||
The accepted local inputs were `Entering Car (2).fbx` and `Exiting Car (2).fbx` in Downloads.
|
||||
The earlier `(1)` files used another character and are not the calibrated source.
|
||||
|
||||
## 3. Verify the raw transfer before fitting a vehicle
|
||||
|
||||
Run Blender in background mode with the stock GLB and matched profile. Use fresh output
|
||||
directories: the tools refuse to overwrite existing preview folders.
|
||||
|
||||
```powershell
|
||||
& $blender -b --python tools/preview_mixamo_animation.py -- --source 'Entering Car (2).fbx' --reference assets/private/skater.glb --profile calibration.json --output .local/raw-enter
|
||||
& $blender -b --python tools/preview_mixamo_animation.py -- --source 'Exiting Car (2).fbx' --reference assets/private/skater.glb --profile calibration.json --output .local/raw-exit
|
||||
```
|
||||
|
||||
`$blender` is the path to your Blender executable. Open each `Raw-Skate-Animation.blend`.
|
||||
Check the first, middle and final frames, especially shoulders, wrists, hips and feet.
|
||||
The tool checks the calibration hash and source bind compatibility. Do not bypass a
|
||||
mismatch by forcing rotation offsets into the native rest skeleton. Native rest bones
|
||||
and skin inverse-bind matrices must remain unchanged. Fix the calibration/source first.
|
||||
|
||||
## 4. Fit the approved poses to the kart
|
||||
|
||||
```powershell
|
||||
& $blender -b --python tools/fit_kart_animation_preview.py -- --raw .local/raw-enter/Raw-Skate-Animation.blend --kart sdk/examples/mario-kart/kart.glb --mode enter --output .local/fit-enter
|
||||
& $blender -b --python tools/fit_kart_animation_preview.py -- --raw .local/raw-exit/Raw-Skate-Animation.blend --kart sdk/examples/mario-kart/kart.glb --mode exit --output .local/fit-exit
|
||||
```
|
||||
|
||||
This example script aligns the seated hips, eases torso lean and solves arms/legs to
|
||||
wheel and footwell targets. It bakes a separate fitted action, retains the raw action,
|
||||
checks unchanged rest bones and writes a fit report. Review both .blend files before
|
||||
export. Feet need clearance over the side pod, hands need a reachable wheel position,
|
||||
and the last entry pose should match the first exit pose. The seated pose is also
|
||||
used for the driving loop. Fingers are limited by the native character's hand rig.
|
||||
|
||||
Targets in this script use Blender Z-up: seat `(0, .20, .73)`, wheel grips
|
||||
`(+/-.19, -.36, .90)`, feet `(+/-.17, -.55, .33)`; the preview kart is raised `.55`.
|
||||
These are **kart example values**. For another vehicle adjust alignment, contact targets,
|
||||
clearance and torso lean in the authoring script or bake equivalent edits in Blender.
|
||||
Do not change rest bones, and keep the fitted armature object transform identity.
|
||||
|
||||
## 5. Export in the game's exact skeleton order
|
||||
|
||||
Obtain the order from the installed native bank (or `sdk.animation.info().bone_names`).
|
||||
Do not use the larger reference list in the vendor importer: this build's bank has 36
|
||||
bones, including reparented helpers. Generate the JSON list with the provided utility:
|
||||
|
||||
```powershell
|
||||
cargo run --locked -p skate-data --example vehicle_bone_names -- assets/private/stock/data/anim/OnBoard.abin .local/bone-names.json
|
||||
& $blender -b --python tools/export_kart_rider.py -- --enter .local/fit-enter/Kart-enter.blend --exit .local/fit-exit/Kart-exit.blend --reference assets/private/skater.glb --bone-names .local/bone-names.json --output sdk/examples/mario-kart/rider.json
|
||||
```
|
||||
|
||||
The exporter preserves the glTF skin bind basis while converting Blender poses into
|
||||
column-major native matrices. It subtracts the preview seat, exports enter/exit clips,
|
||||
uses the final entry pose as drive, and generates left/right steering endpoints with
|
||||
length-preserving arm reach. It renders both endpoints for inspection. The host blends
|
||||
between them continuously with steering input. For another cockpit, change the exporter
|
||||
seat, steering-wheel centre/axis and steering angle alongside the fitted scene targets.
|
||||
|
||||
The example's game seat is `[0, .18, -.20]`: convert the Blender seat to Y-up and subtract
|
||||
the preview kart's .55 m lift. Model fitting offsets are not an excuse to alter native
|
||||
bone axes. Export fps comes from each scene; retain its timing instead of relabelling
|
||||
frames with a different rate. The output is a full native pose, not additive rotations.
|
||||
|
||||
## 6. Package, validate and playtest
|
||||
|
||||
Put `rider.json`, the embedded `kart.glb`, `vehicle.json`, `mod.json` and `main.lua` in
|
||||
`sdk/examples/mario-kart` (or your own authoring folder). Set animations.file to rider.json,
|
||||
enter/exit to their names, idle/drive/reverse/brake to drive for this example, and
|
||||
steer_left/steer_right to the corresponding endpoints. Package it with:
|
||||
|
||||
```powershell
|
||||
python tools/package_mod.py sdk/examples/mario-kart mods/mario-kart.zip
|
||||
```
|
||||
|
||||
The archive has mod.json at its root. The game extracts it automatically; do not edit
|
||||
mods/.cache. See mod-packages.md and vehicle-sdk.md for limits, schema and lifecycle.
|
||||
|
||||
The host validates names, frame counts, finite rigid transforms and clip references at
|
||||
load. The source loader tests cover malformed inputs; local validation also checked the
|
||||
export against the actual stock bank and matched the final entry pose to drive. The
|
||||
runtime applies the same native pose to whichever compatible character is selected.
|
||||
|
||||
`Build.ps1` stages a build, example ZIPs and documentation. Repackage approved
|
||||
source changes to update the live ZIP. The game rescans and reloads enabled changed mods;
|
||||
use Rescan packages to force discovery. Existing compatible settings survive updates.
|
||||
Use PLAY-MARIO-KART.bat for a manual session. Check entry/exit from multiple vanilla
|
||||
stances, steering in both directions, ramps, braking, reset, Escape, and mod disable.
|
||||
Also try characters with different proportions; native-rig compatibility does not
|
||||
promise exact contact for every body size. Gameplay is never launched by these tools.
|
||||
|
||||
Source code and authoring scripts are reusable. The supplied Mario Kart model and
|
||||
fitted rider.json are committed as the bundled example, including mods/mario-kart.zip.
|
||||
Raw FBX/Blender authoring files and the stock skater mesh remain local and ignored.
|
||||
@@ -1,142 +0,0 @@
|
||||
# ZIP mod packages (SDK 1)
|
||||
|
||||
For agents, start at [sdk/AGENTS.md](../sdk/AGENTS.md). For function signatures, use
|
||||
[sdk/skate.lua](../sdk/skate.lua); for callbacks/settings use [Lua SDK](lua-modding.md).
|
||||
|
||||
## Installation and updates
|
||||
|
||||
Drop a `.zip` directly into top-level `mods/` and launch `PLAY.bat`.
|
||||
Open Escape → Mods, select it and enable it. New IDs start disabled. Open mods folder
|
||||
opens this same directory. SKATE3_MODS overrides discovery; a standalone executable
|
||||
defaults to its adjacent mods/. No manual extraction is needed.
|
||||
|
||||
The game scans approximately every 500 ms. Changed enabled packages reload after a
|
||||
750 ms debounce; Rescan forces discovery. Replace the ZIP to update; remove it to
|
||||
uninstall. Stable IDs preserve compatible settings, stored outside the ZIP. Reloading
|
||||
restarts Lua and removes/recreates owner objects. Removing a driving vehicle releases
|
||||
its rider. Disabling a mod keeps its package available for later use.
|
||||
|
||||
The loader supports editable folders too, but **never install a folder and ZIP with
|
||||
the same mod ID**. All copies of a duplicate ID are rejected. Hidden directories are
|
||||
not packages. The two editable examples are under sdk/examples; live mods/ contains
|
||||
native-trainer.zip and mario-kart.zip. Archives remain local and untracked.
|
||||
|
||||
## Exact archive structure
|
||||
|
||||
```text
|
||||
my-mod.zip
|
||||
mod.json REQUIRED: manifest at archive root
|
||||
main.lua REQUIRED if manifest.entry is main.lua
|
||||
README.md recommended controls and credits
|
||||
vehicle.json optional vehicle definition
|
||||
assets/
|
||||
kart.glb optional embedded vehicle model
|
||||
help.txt optional package text data
|
||||
animations/
|
||||
rider.json optional native vehicle clips
|
||||
```
|
||||
|
||||
Do not wrap this in a my-mod/ parent folder. `entry` may name a different relative
|
||||
Lua file. Optional directory names are conventions, not magic. Paths in vehicle.json
|
||||
are **package-root relative**, even if the definition itself lives in a subdirectory.
|
||||
For the above layout, model=`assets/kart.glb`, animations.file=`animations/rider.json`.
|
||||
Lua uses `sdk.vehicle.spawn('kart','vehicle.json',position,heading)` and
|
||||
`sdk.read_text('assets/help.txt')`. A ZIP's filename does not determine its mod ID.
|
||||
|
||||
Include only files your mod needs. Vehicle GLBs must embed geometry, buffers and textures.
|
||||
Rider JSON must match the installed native skeleton exactly; FBX and .blend are authoring
|
||||
inputs, not runtime animation files. Engine audio is currently synthesized through the
|
||||
vehicle definition; adding arbitrary WAV/MP3 files does not make them playable.
|
||||
No executable binaries or external Lua modules are loaded.
|
||||
|
||||
## Minimal complete mod
|
||||
|
||||
mod.json:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "yourname.hello", "api": 1, "name": "Hello Skater",
|
||||
"version": "1.0.0", "author": "Your name", "description": "Example HUD mod.",
|
||||
"entry": "main.lua",
|
||||
"settings": {
|
||||
"message": { "type": "string", "label": "Message",
|
||||
"description": "Text shown on screen.", "default": "Hello skater" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
main.lua:
|
||||
|
||||
```lua
|
||||
local function draw()
|
||||
sdk.ui.text('hello', sdk.settings.message)
|
||||
end
|
||||
return {
|
||||
on_load = draw,
|
||||
on_settings = draw,
|
||||
on_event = function(event)
|
||||
if event.name == 'world_changed' then draw() end
|
||||
end,
|
||||
}
|
||||
```
|
||||
|
||||
The host removes owner visuals on disable/reload. Number settings require finite min,
|
||||
max, positive step and an in-range default. Choice settings require choices. Unknown
|
||||
manifest fields are rejected; there is no permissions or dependency declaration.
|
||||
Read the full setting and callback reference before extending this example.
|
||||
|
||||
## Package and check
|
||||
|
||||
From the repository:
|
||||
|
||||
```powershell
|
||||
python tools/package_mod.py sdk/examples/your-mod mods/your-mod.zip
|
||||
cargo run --locked -p skate-mods --example check_mod -- mods/your-mod.zip
|
||||
```
|
||||
|
||||
Requires Python 3.9+ and Rust/Cargo. The packer uses Python's standard ZIP library and
|
||||
runs the authoritative Rust checker on both the source and ZIP before replacing the
|
||||
output. It writes a temporary non-ZIP file first so discovery cannot see a half-written
|
||||
archive. Optional `--target-dir .local/vehicle-tests` selects the Cargo cache.
|
||||
The checker does not execute Lua callbacks. Passing it is not a gameplay test.
|
||||
|
||||
`Build.ps1` builds the host, packages both example sources and stages a
|
||||
standalone copy with docs. Older mod folders in its private staging directory are
|
||||
preserved under hidden .legacy-* names to avoid duplicate IDs. It does not delete
|
||||
personal mod edits. Both installable example ZIPs are committed, along with the
|
||||
Mario Kart model/rider source assets, so a fresh checkout can use or rebuild both mods.
|
||||
ZIP metadata is deterministic; an unchanged source folder produces identical package bytes.
|
||||
|
||||
## Limits and extraction rules
|
||||
|
||||
- At most 128 immediate packages; 512 ZIP entries and 512 extracted files/directories.
|
||||
- At most 64 MiB compressed and 64 MiB expanded per ZIP/package.
|
||||
- ZIP Store or Deflate compression. No encryption, links or special file entries.
|
||||
- Relative forward-slash paths only, at most 240 bytes for ZIP paths. No traversal,
|
||||
absolute/drive paths, Windows device names, forbidden characters or trailing dots/spaces.
|
||||
- Duplicate paths, including case collisions, are rejected. No overwrite extraction.
|
||||
- mod.json ≤64 KiB; Lua entry ≤256 KiB of UTF-8 source, no bytecode.
|
||||
- Vehicle definition ≤128 KiB; model ≤32 MiB; vehicle clips ≤16 MiB.
|
||||
- Text reads ≤256 KiB; authored vanilla body replacements ≤4 MiB. Other Lua quotas
|
||||
and native validation rules are described in lua-modding.md and vehicle-sdk.md.
|
||||
|
||||
The loader checks/decompresses within bounds, then writes to a new session directory
|
||||
under mods/.cache. Assets use this private directory transparently. Never edit or package
|
||||
.cache. Changed ZIPs get new cache paths, so assets cannot silently reuse the old model.
|
||||
Session caches are removed on normal shutdown; a crash may leave cache directories,
|
||||
which may be deleted with the game closed. Do not delete live session caches.
|
||||
A rejected ZIP shows diagnostics in the Mods UI/log and does not continue running an
|
||||
outdated copy indefinitely. Cache contents do not become independent installed mods.
|
||||
|
||||
## Further examples
|
||||
|
||||
- [Native Trainer](../sdk/examples/native-trainer/README.md): tuning, HUD, markers, timers.
|
||||
- [Vehicle SDK](vehicle-sdk.md): physics, controller input, ramp tuning and engine audio.
|
||||
- [Mixamo workflow](mixamo-vehicle-workflow.md): calibrated native-rig animation authoring.
|
||||
|
||||
## 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](multiplayer-mods.md) for ownership, collisions,
|
||||
late joins and shared-state examples.
|
||||
@@ -1,139 +0,0 @@
|
||||
# Multiplayer mod SDK (protocol 5)
|
||||
|
||||
Use the same game build and map on every PC. Install and enable the same mod ZIP
|
||||
on each PC for that mod's shared objects. Launch `PLAY.bat`, then use
|
||||
**Escape > Multiplayer** to host or join. The build includes the optional Steam
|
||||
relay; Steam must be running for Steam lobbies. Direct local testing remains
|
||||
available. `Build.ps1` builds and stages both executables without launching them.
|
||||
|
||||
## What synchronizes automatically
|
||||
|
||||
| SDK feature | Multiplayer behavior |
|
||||
| --- | --- |
|
||||
| `scene.cube` / `scene.remove` | Owner-scoped shared visual objects; updates, removal and late joins supported. Cubes remain non-colliding. |
|
||||
| `vehicle.spawn/remove/reset/tune/control` | Matching package supplies the model and definition. Owner sends chassis pose, velocities, wheel state, controls, tuning and occupancy. Other peers render and collide with replicas. |
|
||||
| Vehicle entry/exit/steering | The final displayed native skeleton, including stance blending and arm steering, is replicated. Seated riders follow the same car render sample. |
|
||||
| Vehicle engine audio | Each occupied car has a smoothed engine voice; other players' cars attenuate with distance. |
|
||||
| Vehicle crash/inversion | The owning player decides the ejection, preserving pre-impact momentum. Their native wipeout/ragdoll then uses existing player replication. |
|
||||
| Trainer / animation replacements / teleport | Run on the owning player's game. Their resulting movement, pose and ragdoll replicate through native player networking. Remote tuning never overwrites your local controls. |
|
||||
| UI, mod settings windows, input, timers and logs | Local to each player. No duplicate remote input execution. |
|
||||
| Custom Lua scores, checkpoints or game rules | Use `sdk.net.publish/read`; Lua variables alone are not shared. |
|
||||
|
||||
This applies to the SDK, not a special Mario Kart network script. Existing trainer
|
||||
beacons/trails and vehicle mods use these paths without additional networking code.
|
||||
The imported multiplayer renderer still displays remote players using the stock
|
||||
skater; custom character appearance transfer is a separate feature.
|
||||
|
||||
## Matching and ownership
|
||||
|
||||
A world/state record carries its mod ID and a portable fingerprint of package-relative
|
||||
file names and file contents, including scripts and assets. Absolute installation
|
||||
paths and ZIP timestamps do not participate. Different assets or scripts count as
|
||||
a different package even if the version text is unchanged. The receiver must have
|
||||
the matching package enabled and running. Mismatches appear in the multiplayer HUD.
|
||||
No ZIP download, remote Lua execution, or arbitrary remote asset path is allowed.
|
||||
|
||||
Object identity is `(player ID, mod ID, key)`. Both players may spawn `kart` without
|
||||
replacing each other's car. The spawning player remains the simulation owner and
|
||||
driver; transferring a vehicle to another player or sharing a passenger seat is not
|
||||
implemented. Disconnecting, disabling a mod or removing an object removes its
|
||||
replicas. Resetting a car updates its existing replica. Joining later receives the
|
||||
current state rather than replaying callbacks. Stable keys should be reused.
|
||||
|
||||
Settings are personal. Shared vehicle tuning follows its owner's settings. For a
|
||||
single shared course or scoreboard, have one designated peer (usually the lobby
|
||||
host) own it; do not run the same authoritative rule independently on every peer.
|
||||
Check `local_id`, `active` and `is_host` each update because connections and hosts
|
||||
can change. Objects owned by a departed peer disappear; a replacement host must
|
||||
recreate host-owned game rules/objects if your mod needs that behavior.
|
||||
|
||||
## Shared-state API
|
||||
|
||||
```lua
|
||||
local previous_id
|
||||
return {
|
||||
on_update = function(e)
|
||||
local net = sdk.net.info()
|
||||
-- Keep player IDs as strings; they are 64-bit and can exceed Lua's exact range.
|
||||
if previous_id ~= net.local_id then
|
||||
previous_id = net.local_id
|
||||
sdk.net.publish("ready", {ready=true})
|
||||
end
|
||||
local mine = sdk.net.read(net.local_id, "ready")
|
||||
-- net.states[sdk.mod_id] maps peer ID strings to that peer's state keys.
|
||||
for peer, values in pairs((net.states or {})[sdk.mod_id] or {}) do
|
||||
local ready = sdk.net.read(peer, "ready")
|
||||
-- Display results locally; only mutate your own authoritative state.
|
||||
end
|
||||
end
|
||||
}
|
||||
```
|
||||
|
||||
- `sdk.net.info()` returns `{active, local_id, is_host, states, status}`. Offline ID
|
||||
is `"0"` and `is_host=true`. State is still readable locally while offline.
|
||||
- `sdk.net.publish(key, value)` stores this mod's latest value under the local
|
||||
player. Values can be JSON-compatible scalars/tables, at most 512 encoded bytes.
|
||||
Keys follow normal SDK key rules (1..64 characters). `nil` clears a value.
|
||||
- `sdk.net.read(peer_id, key)` reads only this mod's value for that peer. Missing
|
||||
peers/keys return `nil`. Poll this from an update callback; no remote callback
|
||||
runs and there is no exactly-once event API.
|
||||
- Publish durable state, such as a counter or `{sequence=12, checkpoint=3}`, rather
|
||||
than a one-frame pulse. Fast updates may coalesce into the newest value.
|
||||
- A Lua callback's commands still commit only after it succeeds. Failing callbacks
|
||||
do not send their queued state changes.
|
||||
|
||||
## Collision and authority contract
|
||||
|
||||
Native players already use owner simulation. Cars follow the same model: your
|
||||
Rapier world owns your cars, and remote cars use kinematic collision proxies.
|
||||
Chassis collisions affect your local simulation and can trigger a local rider bail;
|
||||
the resulting motion is sent back. Two locally owned cars use ordinary dynamic
|
||||
Rapier contacts. Remote cars include the occupied rider capsule.
|
||||
|
||||
Native skating/board collision queries include nearby car chassis. Rapier also
|
||||
receives nearby native player/board proxies (small spheres around active physical
|
||||
parts). This bridges the two physics engines; it is an approximation of the native
|
||||
articulated hitboxes. A seated driver's native standing hitboxes are disabled in
|
||||
network snapshots so they cannot collide at their old skating position.
|
||||
|
||||
Remote positions use bounded extrapolation (150 ms), limited correction velocity,
|
||||
and fixed-step render interpolation. Rider attachment uses the car's displayed
|
||||
transform. Large resets snap instead of drawing a car through the map. This is
|
||||
not a host-authoritative shared Rapier world: delayed impacts can differ across
|
||||
peers and are corrected from each owner's next state. It does not promise exactly
|
||||
conserved cross-client momentum or competitive anti-cheat.
|
||||
|
||||
## Bounds and failure behavior
|
||||
|
||||
Application datagrams share the native lobby's byte budget and MTU (1200 bytes).
|
||||
Native body/pose traffic has priority. Latest records are acknowledged, resent
|
||||
with varied retry intervals after loss, and relayed only by authenticated owners.
|
||||
Records contain at most 1024 value bytes and 128 key bytes; 256 wire keys per player
|
||||
per session includes deletion tombstones. SDK data has a 128-live-record limit;
|
||||
vehicle state is additional. Reuse keys rather than generating unlimited unique
|
||||
IDs. A full network record includes metadata, so oversized world descriptors are
|
||||
reported in the multiplayer HUD instead of being truncated. Typical kart and
|
||||
trainer records fit. Heavy object loads lower effective update frequency.
|
||||
|
||||
The existing limits remain 8 vehicles per mod and 32 local vehicles. Up to 288
|
||||
remote vehicle replicas are admitted across the other nine players. No peer may
|
||||
write another peer's state; stale revisions and oversized packets are rejected.
|
||||
Leaving a lobby clears remote replicas. Map changes leave the lobby and reset the
|
||||
native collision schema.
|
||||
|
||||
## Verification
|
||||
|
||||
Run headless checks (these do not start the game):
|
||||
|
||||
```powershell
|
||||
cargo test --locked -p skate-net -p skate-mods -p skate-vehicles
|
||||
cargo check --locked -p skate-game --bin skate3rust --no-default-features
|
||||
```
|
||||
|
||||
Coverage includes loss/reordering, late joins, tombstones/disconnects, ten-player
|
||||
record replication, spoofed owners and bounds, portable asset fingerprints, Lua
|
||||
state transaction rollback, remote chassis collisions and crash ejection. Manual
|
||||
follow-up: two PCs with matching enabled ZIPs; spawn separate cars, drive into one
|
||||
another, hit a standing player, bail, reset, join late, disable/re-enable a mod, and
|
||||
check trainer markers and mod mismatch messages. No game was launched as part of
|
||||
these automated checks.
|
||||
@@ -0,0 +1,112 @@
|
||||
# Physics + graphics modding layer (API 2)
|
||||
|
||||
Low-level Rapier + BoardWorld primitives. **No vehicle type** — assemble cars, carts,
|
||||
and props from bodies, joints, meshes, and `sdk.net` rules in Lua.
|
||||
|
||||
## Primitives
|
||||
|
||||
| Area | Surface |
|
||||
| --- | --- |
|
||||
| Physics | `spawn/remove`, `add_collider`, forces/impulses/torques, pose/velocity, `read()` |
|
||||
| Queries | `raycast` with `filter="all"` or `"ground"`, `velocity_at`, `effective_inv_mass`, `spring_ray`, `local_ang_accel_impulse` |
|
||||
| Contacts | **`touching()`** (steady pairs incl. `ground`), `contacts()` (begin/end edges) |
|
||||
| Joints | `revolute`, `prismatic`, `joint_motor`, `joint_spring`, `remove_joint` |
|
||||
| Graphics | `sdk.graphics.mesh`, visibility, remove; `sdk.ui.text` |
|
||||
| Player | `read`, `attach`, `detach` — attach pauses native skate for that player |
|
||||
| Network | `sdk.net.publish/read` — JSON rules (driver seat, scores); **not** physics replication |
|
||||
|
||||
Manifest `"api": 2`.
|
||||
|
||||
## Fixed tick (owner client)
|
||||
|
||||
1. Map ground injected into `DynamicsWorld` (`ensure_ground`)
|
||||
2. Per mod: physics snapshot from **last** Rapier step → `on_fixed_update`
|
||||
3. Lua queues commands (forces, spawns, motors, …)
|
||||
4. `begin_force_frame` → apply commands (removes before spawns) → skater proxies → `step`
|
||||
5. Contacts drained; meshes/attach synced
|
||||
|
||||
`read()`, `touching()`, and `contacts()` during `on_fixed_update` reflect the **previous**
|
||||
step. Commands you issue this tick take effect on the next step.
|
||||
|
||||
## Spawn height
|
||||
|
||||
Use the same raycast API with `filter="ground"` for terrain height. The default
|
||||
`filter="all"` still hits dynamic bodies, mod bodies, and skater proxies.
|
||||
|
||||
```lua
|
||||
local hit = sdk.physics.raycast(
|
||||
{x, 500, z},
|
||||
{0, -1, 0},
|
||||
{max_distance=1000, filter="ground"}
|
||||
)
|
||||
local ground_y = hit and hit.point[2] or 0
|
||||
```
|
||||
|
||||
Exclude one or more owned bodies without changing the filter:
|
||||
|
||||
```lua
|
||||
local hit = sdk.physics.raycast(origin, direction, {
|
||||
max_distance=100,
|
||||
filter="all",
|
||||
exclude={"chassis", "wheel_fl"},
|
||||
})
|
||||
```
|
||||
|
||||
Respawn pattern (multiplayer-safe): queue `remove` on frame *N*, `spawn` on frame *N+1*
|
||||
so teardown is applied before placement queries.
|
||||
|
||||
## Contacts
|
||||
|
||||
```lua
|
||||
for _, t in ipairs(sdk.physics.touching()) do
|
||||
if t.a == "wheel_rl" and t.b == "ground" then ... end
|
||||
end
|
||||
```
|
||||
|
||||
`contacts()` fires `started=true/false` for pair changes only; do not use it as a ground
|
||||
counter.
|
||||
|
||||
## Drive assembly (reference pattern)
|
||||
|
||||
Built from primitives on the **owner** sim; remotes follow `dyn:{mod}:{body}` poses.
|
||||
|
||||
1. **Sensor chassis** — `sensor=true`, no terrain contact; carries mass/inertia
|
||||
2. **Wheel bodies** — separate dynamic bodies with mesh/sphere hulls, `friction≈1.3`
|
||||
3. **Prismatic suspension** — chassis↔wheel, axis down, `joint_spring` target = rest length along axis
|
||||
4. **Revolute axle** — same anchors; rear `joint_motor` for drive
|
||||
5. **No** scripted tire impulses on wheel contact points
|
||||
|
||||
Validate with `crates/skate-mods/tests/physics_api.rs` (`wheel_motor_on_ground_moves_chassis`).
|
||||
|
||||
## Example mod
|
||||
|
||||
`sdk/examples/physics-sandbox/` — crate, WASD forces, ground contact HUD, E attach, R reset.
|
||||
|
||||
```powershell
|
||||
cargo run --locked -p skate-mods --example check_mod -- sdk/examples/physics-sandbox
|
||||
cargo test --locked -p skate-mods physics_api
|
||||
```
|
||||
|
||||
Copy the folder into `mods/` (or set `SKATE3_MODS`).
|
||||
|
||||
## Multiplayer
|
||||
|
||||
| Replicates | Does not replicate |
|
||||
| --- | --- |
|
||||
| Body poses (`dyn:…` APPLICATION) | Joints, motors, forces, spawns |
|
||||
| Matching mod fingerprint | Per-tick command stream |
|
||||
|
||||
Use `sdk.net.publish` for shared rules (who drives, scores). Owner simulates physics;
|
||||
late joiners see poses, not a full rigid-body replay.
|
||||
|
||||
## Dual-world collision
|
||||
|
||||
Skater BoardWorld volumes ↔ Rapier kinematic proxies each fixed tick. Dynamics shapes
|
||||
export into `network_proxies` for native skate collision. Player hit / wipeout bridging
|
||||
from Rapier contacts is still planned.
|
||||
|
||||
## Deferred
|
||||
|
||||
- `sdk.player.hit`, play-state masks, portal render targets
|
||||
- Shapecast / multi-ray helpers
|
||||
- Packaged ZIP helper (use loose folders + `check_mod`)
|
||||
@@ -81,7 +81,7 @@ additional worker memory or contention.
|
||||
- Owned-output checks verified geometry bounds, finite buffers, skin weights,
|
||||
all 22 live morph targets, secondary UVs, hair opacity and referenced textures.
|
||||
- Synthetic recovery tests cover default.xex validation, failed finalization,
|
||||
preservation of maps/user profiles/mods, crash-released locking, incomplete
|
||||
preservation of maps/user profiles, crash-released locking, incomplete
|
||||
stages, same-size corruption, dependency invalidation, and a crash between
|
||||
publishing a generation and deleting its pending record.
|
||||
- `cargo check --locked -p skate-game --bin skate3rust --no-default-features`
|
||||
|
||||
@@ -177,8 +177,8 @@ current snapshots and records replay. `animation.rs` interpolates bone transform
|
||||
for display. Snapshot copying and hierarchy propagation are candidates if those
|
||||
systems dominate, not grounds to reduce bone update rate or interpolation quality.
|
||||
Static map transforms are separate from these moving character hierarchies.
|
||||
Multiplayer/mod/vehicle systems are additional workload; profiles should state
|
||||
whether mods or online play were active rather than attributing all time to University.
|
||||
Multiplayer systems are additional workload; profiles should state
|
||||
whether online play was active rather than attributing all time to University.
|
||||
|
||||
`graphics_menu.rs::pace` sleeps in `Last` when capped; window presentation may add
|
||||
another wait. The new frame-interval counter and CPU spans preserve both behaviors.
|
||||
@@ -191,7 +191,7 @@ Use [the capture workflow](performance-tracing.md). Record 30 seconds after load
|
||||
stand briefly, rotate the camera, then skate through a representative University
|
||||
area. Repeat the same settings/route with `--trace-gpu`; record a separate startup
|
||||
trace if the problem is loading or first-use hitching. Include where the slowdown
|
||||
occurred, approximate time in the capture, and whether mods/multiplayer were active.
|
||||
occurred, approximate time in the capture, and whether multiplayer was active.
|
||||
Do not change quality settings between captures.
|
||||
|
||||
Analyse steady-state frame-interval p50/p95/p99, fixed iteration counts, dominant
|
||||
|
||||
+7
-5
@@ -18,13 +18,15 @@ eligible startup, or through the menu.
|
||||
The release manifest owns every shipped program file: executables, helper tools,
|
||||
DLLs, documentation and licenses. Updates add new components, replace changed
|
||||
files and remove components owned by the previous release that are no longer
|
||||
shipped. Assets, maps, saves, settings, user mods and character libraries remain
|
||||
outside this manifest. Bundled mod archives are user-editable defaults and are
|
||||
not overwritten by program updates. The same
|
||||
shipped. Assets, maps, saves, settings and character libraries remain
|
||||
outside this manifest. The same
|
||||
working directory and arguments are used for restart; existing asset selection
|
||||
continues to work. Each package owns its `data` installation. On the next normal
|
||||
launch, changed extractor fingerprints trigger an update of affected asset groups;
|
||||
unchanged groups are retained. Freshly unpacked copies run setup independently.
|
||||
launch, matching extractor fingerprints (including explicit equivalence pairs)
|
||||
keep the existing assets; only real extractor or character-preparation changes
|
||||
trigger an update of affected asset groups, and refreshes reuse the saved Xbox
|
||||
source when it is still available. Unchanged groups are retained. Freshly
|
||||
unpacked copies run setup independently.
|
||||
Explicit `--assets` developer launches bypass this management. Packaged releases are required; developer binaries do not
|
||||
silently adopt a downloaded build.
|
||||
|
||||
|
||||
@@ -1,358 +0,0 @@
|
||||
# Vehicle SDK v1
|
||||
Arcade assist additions (kart package 1.5): optional `ground_stability` (0..1,
|
||||
default 0) adds road-normal roll stabilization while at least two wheels touch;
|
||||
optional `air_control` (0..10 rad/s², default 0) enables occupied-aircraft-style
|
||||
pitch/roll torque control when no wheels touch. These are authored gameplay
|
||||
assists inspired by the requested Burnout Paradise feel, not recovered constants.
|
||||
`VehicleControls.pitch` is optional, -1..1 (positive nose down), mapped from left
|
||||
stick vertical or Up/Down arrows. Existing steering maps to airborne roll.
|
||||
Neutral air input damps pitch/roll without adding linear acceleration.
|
||||
The kart uses ground_stability=0.8, air_control=6, broader inertia dimensions,
|
||||
crash_delta_v=12 m/s, rider hit_impulse=1200 N·s and a 1-second grounded inversion
|
||||
timer. Inversion without chassis/rider contact no longer causes an ejection.
|
||||
Default safety settings on other vehicle definitions are unchanged.
|
||||
|
||||
|
||||
`skate-vehicles` pins Rapier 3D 0.35.3 and owns an independent, fixed-step rigid-body
|
||||
world. The existing skating solver remains authoritative outside vehicles. Rapier
|
||||
uses the installed map's collision triangles and resolves chassis collisions with
|
||||
the map and other vehicles. Wheels use Rapier raycast suspension. Each host tick
|
||||
is split into substeps of approximately 1/120 second or smaller.
|
||||
|
||||
### Force-based handling (September 2026)
|
||||
|
||||
The shared `skate-vehicles/src/handling.rs` now owns tire forces. Rapier's built-in
|
||||
arcade tire impulses are disabled; its suspension queries and chassis collision
|
||||
solver remain active. Steering has a 2.5 rad/s travel rate, speed-sensitive lock
|
||||
and Ackermann front-wheel angles. Wheel torque integrates angular momentum;
|
||||
longitudinal slip and lateral slip angle produce contact impulses constrained by
|
||||
one load-dependent friction circle. Contact-point application produces chassis
|
||||
pitch/roll, and dynamic supports receive opposing tire and suspension impulses.
|
||||
|
||||
`tire_grip` is now the tire friction coefficient, multiplied by the contacted
|
||||
collider's friction. The kart uses 1.3 for a dry setup. Its new `tire_friction`
|
||||
setting intentionally replaces the old arcade `grip` setting so a saved value of
|
||||
3 is not reused as a physical coefficient. Other saved settings remain compatible.
|
||||
Older third-party definitions should review their grip values with this build.
|
||||
|
||||
Service brake overrides throttle; an opposing throttle request brakes before
|
||||
engaging the other direction near rest. Handbrake locks non-steering wheels.
|
||||
Brakes scale with simulation time (the legacy `brake_impulse` setting retains its
|
||||
120 Hz reference scale). Torque tapers with wheel speed rather than switching off
|
||||
at a chassis-speed threshold. Reverse is limited by torque taper at up to 8 m/s.
|
||||
Aerodynamic drag and axle rolling resistance slow coasting. Published speed is
|
||||
the post-solve longitudinal velocity; falling or sliding sideways does not count
|
||||
as forward speed. Reset clears steering, wheel angular momentum and displayed spin.
|
||||
|
||||
This is a simplified physical kart model, not a measured tire/engine dataset:
|
||||
wheel inertia is estimated from chassis mass, lateral stiffness is a fixed model
|
||||
coefficient, and suspension still uses raycasts. There is no gearbox, tire heat,
|
||||
deformation, ABS, or validated moving-platform suspension damping. Engine sound
|
||||
still follows speed/throttle. Automated physics tests do not establish rendered
|
||||
handling quality; steering feel and unusual map surfaces require playtesting.
|
||||
|
||||
Chassis contacts use the minimum friction combine rule: the kart's 0.05 body
|
||||
friction against a 1.0 map stays 0.05 instead of averaging to 0.525. Tire friction
|
||||
still reads the map material independently. The imported collision mesh welds
|
||||
identical vertices and enables internal-edge normal correction while retaining
|
||||
all authored triangles. Crash delta-v is sampled immediately before collision
|
||||
integration, after tire/suspension impulses, so ordinary driving forces are not
|
||||
included in that impact measurement. Rider contact and inversion tests remain active.
|
||||
|
||||
## First mod: Mario Kart
|
||||
|
||||
Run PLAY-MARIO-KART.bat, enable **Mario Kart** in Mods, resume and press **F10**.
|
||||
Walk/skate within four metres and press **E** to enter. **W/S** accelerate/reverse,
|
||||
**A/D** steer, **Space** brakes, **left Shift** handbrakes, **R / right-stick click** rights/resets.
|
||||
**E** exits once speed is below 3 m/s. F10 replaces a parked kart or resets an
|
||||
occupied kart. Live engine, speed, brake, steering and grip controls appear in its
|
||||
own draggable, resizable, scrollable mod window.
|
||||
|
||||
Controller: **Y** enter/exit, **RT/LT** accelerate/reverse, **left stick** steer,
|
||||
**A** brake, **B** handbrake, **right-stick click** reset while driving. The reset bind can be changed to left-stick click in the mod settings. F10 remains the keyboard spawn shortcut.
|
||||
Release controls before switching between driving and skating.
|
||||
|
||||
`sdk/examples/mario-kart/kart.glb` is generated from the user-supplied ZIP. It is excluded
|
||||
from Git. The importer preserves meshes/materials/embedded textures, normalizes
|
||||
the model to metres and creates named wheel pivots. To regenerate:
|
||||
|
||||
```powershell
|
||||
python tools/prepare_mario_kart.py <kart-source.zip> sdk/examples/mario-kart
|
||||
```
|
||||
|
||||
NumPy is required by the model importer. Editable sources live in `sdk/examples/`;
|
||||
`Build.ps1` packages them into top-level `mods/*.zip` and stages a release.
|
||||
The project launcher loads top-level mods/; a standalone executable defaults to its
|
||||
adjacent mods directory. See [mod-packages.md](mod-packages.md) for the ZIP layout,
|
||||
validation, update rules and development-folder support. No game assets are committed.
|
||||
|
||||
## Lua API
|
||||
|
||||
All keys belong to the calling mod. Commands cannot control another mod's vehicle.
|
||||
The host allows eight vehicles per mod and 32 total. Definitions are validated
|
||||
before allocation. Commands from failed Lua callbacks are discarded. Disabling,
|
||||
removing or reloading a mod removes its vehicles and releases its driver.
|
||||
|
||||
```lua
|
||||
sdk.vehicle.spawn('kart', 'vehicle.json', {x, y, z}, heading_radians)
|
||||
sdk.vehicle.enter('kart')
|
||||
sdk.vehicle.control('kart', {
|
||||
throttle = 1, -- -1 reverse through +1 forward
|
||||
steering = 0.25, -- -1 right through +1 left
|
||||
brake = 0, -- 0..1
|
||||
handbrake = false,
|
||||
})
|
||||
sdk.vehicle.tune('kart', {engine_force=1800, max_speed=25, tire_grip=3})
|
||||
local car = sdk.vehicle.read('kart')
|
||||
sdk.vehicle.exit('kart')
|
||||
sdk.vehicle.reset('kart', {x, y, z}, heading_radians)
|
||||
sdk.vehicle.remove('kart')
|
||||
```
|
||||
|
||||
`read` returns nil for an absent vehicle, otherwise position, quaternion rotation
|
||||
(x/y/z/w), heading, signed speed in m/s, occupied, ready and phase. Phases are
|
||||
`parked`, `entering`, `driving`, `exiting`. Snapshots update at host callback boundaries;
|
||||
a spawn command is visible on a later callback. `input()` returns normalized
|
||||
keyboard/controller throttle, steering, brake, handbrake, interact and pad_buttons.
|
||||
Do not pass the full input table to `control`; copy only its four control fields.
|
||||
`control` must be refreshed; after 0.25 seconds without a command, throttle clears
|
||||
and a parking brake is applied. Controls are ignored during enter/exit animations.
|
||||
|
||||
`tune` supports optional engine_force (0..100000 N), max_speed (1..100 m/s),
|
||||
engine_volume (0..1), brake_impulse (0..10000), steering_angle (0.01..1.2 radians), tire_grip (0.1..20).
|
||||
Maximum speed limits engine application; it is not an absolute downhill speed cap.
|
||||
|
||||
Events passed to `on_event`: `vehicle_spawned`, `vehicle_entering`, `vehicle_entered`,
|
||||
`vehicle_exited`, `vehicle_exit_blocked`, `vehicle_reset`, `vehicle_removed`,
|
||||
`vehicle_bailed`.
|
||||
Each includes owner and key. Map changes use the existing `world_changed` event;
|
||||
spawn new vehicles in the new world as needed. Invalid assets/commands appear as
|
||||
mod errors; they retire the mod's vehicles instead of crashing the game.
|
||||
|
||||
## Vehicle definition
|
||||
|
||||
See `sdk/examples/mario-kart/vehicle.json` for a complete working definition. Coordinates
|
||||
are metres, +Y up, +Z forward, +X driver-left, with heading in radians around +Y.
|
||||
`half_extents` describes the outer chassis collision bounds. `mass` is kilograms. `model_scale`,
|
||||
`model_offset` and `model_yaw` affect the model only, not its collider.
|
||||
|
||||
Each wheel defines a chassis-local suspension mounting `position`, radius,
|
||||
steering/driven flags and optional unique model node name. Support is 2..8 wheels
|
||||
with at least one driven wheel. Wheel nodes should have local +Y steering and +X
|
||||
axle axes. Pivots must be correctly positioned; the host adds suspension travel,
|
||||
steering and spin to their rest transforms. The supplied kart importer does this.
|
||||
|
||||
Seat and exit are chassis-local offsets. Exit checks the nearby fixed ground and
|
||||
standing clearance; if unavailable, it returns the rider to the saved entry spot.
|
||||
The camera follows using camera_distance and camera_height. Model GLBs must embed
|
||||
all textures and buffers; filesystem/network references inside GLBs are rejected.
|
||||
A package may total 64 MiB; a model is limited to 32 MiB and an animation file to
|
||||
16 MiB. Paths stay within the owning mod folder.
|
||||
|
||||
## Rider animation support
|
||||
|
||||
The local kart package supplies fitted clips. Other definitions default their animation slots to null. Without a matching
|
||||
clip, the skater is hidden and entering/exiting completes immediately. This avoids
|
||||
showing an unrelated standing/skating pose on the kart.
|
||||
|
||||
Set `animations.file` to a package-relative JSON file and set slot names:
|
||||
|
||||
```json
|
||||
"animations": {
|
||||
"file": "rider.json",
|
||||
"enter": "get_in",
|
||||
"exit": "get_out",
|
||||
"drive": "drive_loop",
|
||||
"idle": "seated_idle",
|
||||
"reverse": "look_back",
|
||||
"brake": "braking",
|
||||
"steer_left": "turn_left",
|
||||
"steer_right": "turn_right"
|
||||
}
|
||||
```
|
||||
|
||||
The file is:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 1,
|
||||
"bone_names": ["EXACT_NAMES_FROM_sdk.animation.info().bone_names"],
|
||||
"clips": {
|
||||
"drive_loop": {
|
||||
"fps": 30,
|
||||
"frames": [["ONE_16_NUMBER_MATRIX_PER_BONE"]]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The strings in the example frames/bone list are explanatory placeholders.
|
||||
Actual frames must contain one 16-number, column-major, native model-space matrix
|
||||
per bone, in the exact native skeleton order. Matrices use metres and +Y up,
|
||||
matching the existing authored animation convention. They are full poses, not
|
||||
additive deltas. Obtain names from `sdk.animation.info()`; do not invent bone names.
|
||||
The renderer performs the existing native-to-GLB skin-basis conversion. Author the
|
||||
pose relative to the seat offset; enter/exit root motion is visual and does not
|
||||
move the Rapier chassis. The native skater is restored through its teleport path
|
||||
when exiting. Files with a mismatched skeleton, invalid matrices or missing named
|
||||
clips are rejected. Maximum 32 clips, 3600 frames per clip, 1..120 fps.
|
||||
|
||||
Enter/exit clips play once and their frame count/fps determines transition duration.
|
||||
Driving slots loop; brake and reverse select their respective base clips, with drive
|
||||
as fallback. Steering continuously blends that base towards the left/right pose.
|
||||
Vehicle phase changes blend with native bone-local interpolation; brake/reverse
|
||||
base-clip changes currently cut, so author compatible seated poses.
|
||||
Edit vehicle.json or rider.json in the authoring folder, rebuild its ZIP, then reload
|
||||
the mod (or wait for automatic rescan) to install new clips. Never edit .cache.
|
||||
|
||||
## Current boundaries
|
||||
|
||||
The native skating simulation is suspended while driving and resumes on exit.
|
||||
Offline vehicle time pauses with Escape; online physics continues while menus are
|
||||
open and unattended controls fall back to braking. Replay entry and session-marker
|
||||
controls are blocked while driving. Vehicle motion is not recorded in skating replays.
|
||||
|
||||
Vehicles collide with the map, other cars and nearby native player/board proxies.
|
||||
Native skating collision queries include car chassis. Matching enabled packages
|
||||
replicate vehicles, tuning, wheel/rider poses, occupancy and engine audio. Crash bails
|
||||
return to normal native ragdoll networking. See [Multiplayer mod SDK](multiplayer-mods.md)
|
||||
for owner simulation, latency limits, hitbox approximations and late joins. Driving
|
||||
another player's vehicle, passengers, weapons, damage and race rules are not built in.
|
||||
|
||||
Verification uses headless Rapier tests and window-free Lua tests. The game is
|
||||
not launched automatically; rendering, entry/exit and handling need manual playtesting.
|
||||
|
||||
### Rider transitions and steering
|
||||
The host blends vehicle phase changes over 0.4 seconds and the return to vanilla over
|
||||
0.5 seconds, using the game's bone-local translation/scale lerp and quaternion slerp.
|
||||
Steering input is smoothed and blends the current driving pose towards steer_left or
|
||||
steer_right continuously, so these slots should contain compatible seated poses.
|
||||
Vehicle camera hand-offs ease over 0.5 seconds. While driving, chassis, wheels, rider
|
||||
and camera share the same interpolated fixed-step motion sample, avoiding relative
|
||||
jitter from separate camera damping. Resets discard the old motion sample.
|
||||
The board root is scaled away during vehicle playback and restored by the vanilla blend.
|
||||
The local Mario Kart package supplies fitted entry/exit, seated and steering clips;
|
||||
its fitted rider.json is included in source control with the prepared kart model.
|
||||
|
||||
## Ramp clearance and mass distribution
|
||||
These are shared definition fields, available to any mod, not Mario-specific code:
|
||||
|
||||
| Field | Meaning | Default |
|
||||
| --- | --- | --- |
|
||||
| `collider_offset` | Chassis-local collision centre, metres | `[0,0,0]` |
|
||||
| `collider_rounding` | Rounded edge radius; below 95% of smallest half extent | `0` |
|
||||
| `chassis_friction` | Body contact friction, 0..2 | `0.3` |
|
||||
| `center_of_mass` | Chassis-local mass centre, metres | `[0,0,0]` |
|
||||
| `inertia_half_extents` | Box dimensions used for mass distribution, independent of contact shape | null: use half_extents |
|
||||
|
||||
Rounding stays inside half_extents. Shorten low front/rear overhangs so wheel contact
|
||||
can lift the chassis before the body catches the ramp. Keep the collider large enough
|
||||
to protect the cockpit; the visual model does not define collision. Inertia dimensions
|
||||
let a shorter collision shape retain stable pitching/rolling behaviour. A lower centre
|
||||
of mass helps resist nose-diving under braking. These fields require respawning.
|
||||
The example passes headless 20/30/40-degree incline tests plus braking and steering tests;
|
||||
this does not guarantee every map seam or vertical ledge is traversable. Wheels remain
|
||||
raycasts, and vertical walls remain obstacles. No teleporting or artificial ramp boost is used.
|
||||
|
||||
## Engine sound
|
||||
Opt in per definition:
|
||||
|
||||
```json
|
||||
"engine_audio": { "enabled": true, "volume": 0.45, "idle_pitch": 0.7, "max_pitch": 2.8 }
|
||||
```
|
||||
|
||||
The host synthesizes an original layered exhaust pulse with filtered noise. No downloaded
|
||||
sound asset or extra file is required. It plays for occupied local and remote vehicles, with distance attenuation for remote engines; parked engines
|
||||
are silent. Pitch follows speed and absolute throttle, including reverse and free revving
|
||||
while stopped. Full throttle increases volume; releasing it smoothly drops the engine back
|
||||
towards idle. Pitch/volume changes are smoothed, Escape/replay fades it silent, and exiting
|
||||
or disabling the mod fades/removes the voice. This is a driver-focused mono sound, not a
|
||||
spatial multi-car mixer or a simulated gearbox. There is currently no custom sample slot.
|
||||
|
||||
Volume is 0..1, idle_pitch 0.25..2, max_pitch idle_pitch..5 (multipliers of the 80 Hz
|
||||
synth fundamental). All are validated. `sdk.vehicle.tune(key,{engine_volume=0.5})`
|
||||
changes volume live; use zero to mute. Mario Kart exposes this in its mod settings.
|
||||
|
||||
## Authoring and reuse
|
||||
See [Mixamo vehicle animation workflow](mixamo-vehicle-workflow.md) for the full sequence,
|
||||
calibration, Blender fitting, native export and packaging commands. The host's animation
|
||||
slots, steering blend, stance hand-offs, board hiding and shared motion interpolation
|
||||
apply automatically to every vehicle definition. The example fitting scripts contain
|
||||
kart geometry targets; adjust those targets for another vehicle without changing the host.
|
||||
|
||||
## Rider hitbox and crash ejection
|
||||
|
||||
Every vehicle can use the shared `rider_safety` definition. It is enabled by default;
|
||||
explicit example values are shown below. Changes require respawning the vehicle.
|
||||
|
||||
```json
|
||||
"rider_safety": {
|
||||
"enabled": true,
|
||||
"offset": [0, 0.5, 0],
|
||||
"radius": 0.25,
|
||||
"half_height": 0.25,
|
||||
"crash_delta_v": 6,
|
||||
"hit_impulse": 180,
|
||||
"inverted_up_y": -0.2,
|
||||
"inverted_seconds": 0.2,
|
||||
"eject_up_speed": 2
|
||||
}
|
||||
```
|
||||
|
||||
A solid capsule is attached to the occupied chassis, centred at `seat + offset`.
|
||||
It covers the seated torso/head, including roof/ground/overhang contact when rolled.
|
||||
It is not a sensor: contacts affect the vehicle. It adds no mass, so existing mass and
|
||||
inertia tuning remains authoritative. Empty vehicles have no active rider collider.
|
||||
The capsule is an approximation, not separate animated hand/foot hitboxes; adjust it
|
||||
for your cockpit and expected character proportions. It is active through entry/exit
|
||||
as well as driving, and is removed from collision when ownership ends. Disabling
|
||||
rider_safety disables both this collider and automatic ejection.
|
||||
|
||||
Ejection triggers on any of:
|
||||
|
||||
- A chassis collision changing linear velocity by at least crash_delta_v in a physics
|
||||
substep. This is delta speed in m/s, not total driving speed; braking/ordinary ramps
|
||||
should not meet the default threshold.
|
||||
- A rider-capsule contact impulse at least hit_impulse, in N·s. This is an impulse
|
||||
threshold, not force in newtons, and is evaluated on actual Rapier contacts.
|
||||
- Chassis-local up having world Y below inverted_up_y continuously for inverted_seconds.
|
||||
The timer clears when upright again. Reset clears pending impacts/inversion history.
|
||||
|
||||
The host stops driver controls, releases occupancy and fades engine audio. It queues
|
||||
an actor reset just above the seat, enters the native Wipeout ragdoll after normal reset
|
||||
initialization, and seeds the native body and board velocities. Seat point velocity
|
||||
includes the vehicle's angular motion. Pre-impact velocity is retained when the car
|
||||
stops abruptly; if a hit accelerates the car from rest, the stronger post-impact point
|
||||
velocity is used instead. A small world-up launch speed helps clear the seat. Output
|
||||
linear/angular speeds are bounded to 60 m/s and 15 rad/s. This is a momentum hand-off,
|
||||
not a fully coupled passenger rigid-body simulation or an exact seated ragdoll pose.
|
||||
The crash visual hand-off lasts 0.12 seconds; normal entry/exit blends remain unchanged.
|
||||
Native map collision and ordinary bail recovery take over. After release, native rider
|
||||
collision against the Rapier car itself is still not bridged, as described above.
|
||||
|
||||
Lua receives **vehicle_bailed** instead of the normal vehicle_exited event:
|
||||
|
||||
```lua
|
||||
on_event = function(event)
|
||||
if event.name == 'vehicle_bailed' then
|
||||
-- event.key is this mod's vehicle key; reason is crash/rider_impact/inverted.
|
||||
sdk.log('Ejected: ' .. event.reason)
|
||||
-- event.position: world seat position at detection, metres
|
||||
-- event.velocity: carried world velocity including launch lift, m/s
|
||||
-- event.angular_velocity: world angular velocity, rad/s
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
The vehicle remains spawned and can be reset or entered again after bail recovery.
|
||||
Use vehicle.read().occupied/phase for ownership; do not keep sending driving input
|
||||
while unoccupied. Physics events are delivered through the existing mod callback queue.
|
||||
The shared Rust simulation also exposes set_occupied and take_ejection for host tests;
|
||||
Lua cannot directly write native ragdoll bodies or bypass the hand-off lifecycle.
|
||||
|
||||
Bounds: offset components ±5 m, radius 0.1..1 m, half_height 0.05..1 m (capsule cylinder
|
||||
half-length; total height is 2*(half_height+radius)), crash_delta_v 1..50 m/s,
|
||||
hit_impulse 10..10000 N·s, inverted_up_y -1..0.5, inverted_seconds 0.05..3 s,
|
||||
eject_up_speed 0..10 m/s. Nonfinite values are rejected. Headless tests cover wall
|
||||
momentum retention, overhang rider hits and sustained inversion versus empty vehicles.
|
||||
Native rendering/recovery and unusual map geometry still need manual playtesting.
|
||||
Reference in new issue
Block a user