Files
skate-3-rust-engine/docs/lua-modding.md
T

281 lines
33 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Lua modding SDK 1
This is an implemented, in-process Lua 5.4 SDK for this engine. Open **Escape/Start → Mods**. New packages are disabled. Select a package to enable/disable, reload, edit settings or reset defaults. Up/down and D-pad scroll eight visible rows; left/right changes booleans, numbers and choices; Enter/A chooses. Strings use keyboard text entry, Backspace, Enter to commit and Escape to cancel. B/Escape returns one level without resuming behind the menu. The existing map, marker, bail and character menus remain the owners of their native behavior.
Run `PLAY-NATIVE-TRAINER.bat` for the current trainer build (`Build-NativeTrainer.ps1` builds it); `PLAY-LUA-SDK.bat` retains the earlier SDK build. It starts paused, uses isolated mod preferences, and writes `.local/lua-sdk/session.log`. It requires prepared engine assets and the executable generated by `Build-LuaSDK.ps1`. Set `SKATE3_ASSETS` or the private `.local/lua-sdk/assets.path`; the checked-in launcher contains no personal installation path. No Lua installation is needed.
## Quick start
Copy `mods/native-trainer` to another immediate child of `mods`, change its `id` and `name`, then choose Rescan packages. A package consists of:
```text
mods/my-mod/
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. Only immediate child directories are packages. Archives are not loaded. `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.
## Complete SDK API
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, network API, 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 directories; packages over 512 files/directories or 8 MiB of file data are rejected. Keep packages small. Lua allocator quotas do not include Rust/Bevy asset storage; visual count and package/file limits bound those separately. Source and plain UTF-8 data can be loaded with `sdk.read_text`; there is no general JSON decoder/import loader in Lua. Prefer ordinary Lua local functions and source literals for small data. Body JSON has its dedicated engine parser. This release does not load mod GLTF/meshes, textures, fonts, audio banks or general Bevy scenes. Cubes are original generated geometry and text uses the engine's existing font. Do not ship private Skate maps, banks, extracted character assets, sounds or captures; mods can rely on the player's installed engine data for allowed body slots without copying it.
No camera-controller override, native audio command, customiser-profile mutation, physics force injection, collision-geometry insertion, climbing controller installation, map-selection command, score manipulation, networking, 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 manual test checklist
`cargo test --locked -p skate-mods --target x86_64-pc-windows-msvc` runs the standalone window-free Lua/schema suite. It covers the bundled Native Trainer, reload state reset, timers/replacement/order, settings persistence and validation, duplicate IDs, removal/invalid manifests, path restrictions, failure isolation, unload failure, instruction and memory limits. This is not rendered-gameplay validation.
Use `Build-LuaSDK.ps1 -DependencyTargetDirectory <shared cargo cache>` to produce a uniquely named direct linker output, check SDK/CLI diagnostic strings without executing it, and record a staged SHA-256. Do not copy the generic executable from a shared Cargo target. `PLAY-LUA-SDK.bat` is provided exclusively for the user's manual run. No game, recomp, game asset-check mode, automated controller input, screenshot capture or game-based validation was executed for this SDK task.
Manual checks still required: open/scroll/back with keyboard/controller; enable Native Trainer; edit all setting types; hot-edit and reload; deliberately cause a Lua error then recover; add/remove/duplicate a package; disable and verify HUD/cubes disappear; switch maps and check marker/customiser/bail behavior; adjust pop/push settings, then disable and check stock behavior; inspect original authored animation replacement if available. Teleport destinations and animation visuals require deliberate author testing. Compilation and window-free runtime tests do not establish these gameplay results.
Integration footprint: `skate-mods` owns VM/schema/discovery/preferences; `skate-game/modding` owns menu and command adapter. Shared-file changes are plugin wiring (`main.rs`, `app.rs`), one pause-menu row, a read-only grind observation method and the existing animation evaluator/parser's owner-scoped overrides. Other owners should retain these narrow hooks when merging. Baseline is local merge `26f056c`; no original checkout, private asset tree or other task configuration was modified.
### Recorded non-game results
On 2026-09-08, all 13 original standalone SDK tests passed. The Windows release build and direct-output string/SHA-256 identity checks passed. The first release attempt diagnosed a stale shared `skate-core` artifact missing the merged respawn module; the helper now refreshes only this worktree's local crate source timestamps before compiling, preserving all dependency cache contents. Existing engine warnings remain. No interactive menu, native teleport or authored animation visual result is claimed.
Private manual-test setup uses this task's own real asset-root directory with a read-only junction to the prepared `private` data, and its own settings directory. Maps are referenced through a junction. The launcher therefore does not use another task's writable settings root. None of these references, prepared assets, personal paths or binaries are tracked.
## Native Trainer and pause-panel update
The bundled Speedometer and Route Trainer examples have been removed. `mods/native-trainer` is the single bundled mod, disabled by default. Its seven settings control pop height, grind pop height, push speed, push power, braking, steering and wobble. All start at **1.00x stock**. Pop/grind pop/push speed/push power/braking/steering range from 0.25x to 4x; wobble ranges from 0x to 2x, in 0.25 increments. There is no direct board-velocity write, invincibility or global gravity cheat.
The Mods list now has a right-aligned **ENABLED** badge in green or **DISABLED** in red; the selected mod has an explicit **ENABLE MOD / DISABLE MOD** action. A faulted mod is DISABLED with its error in the detail area. Whenever a mod is running, Escape shows its settings in a separate left-hand panel beside the normal pause controls. Click a value to increase/cycle it, or press **Tab / controller X** to focus that panel, then **Up/Down** to select and **Left/Right** to adjust. Tab/X returns focus to normal pause controls. Select the mod name to open its full enable/disable/settings page. Text settings open the full editor. Lists longer than ten rows page as selection advances.
`sdk.trainer.apply(tuning: table) -> nil` queues an owner-scoped native tuning override. Optional fields `pop`, `grind_pop`, `push_speed`, `push_power`, `braking`, `steering`, `wobble` have the ranges above and default to 1 on **each call**. Unknown fields and nonfinite/out-of-range values raise errors. Only one mod can own trainer tuning at a time; another owner is rejected and stopped. Disabling, reloading or faulting the owner restores default multipliers. Active tuning survives map replacement and is reapplied before further simulation. Existing momentum and an already-launched jump are not rewound; subsequent physics calculations use current tuning.
Source bindings: ground pop scales the current `GroundJumpMode` minimum/maximum heights before the existing ground-jump calculation. Grind pop scales the existing interpolated launch-height argument. Push speed scales the animation-provided push target plus `maximum_pushable_speed`; push power scales difficulty-specific `mode_speed_changes`. Brake strength scales the native foot/tail brake force and steering scales `general_scalar`. Wobble scales the existing mode amplitude multiplier. Ground profiles are derived anew from the stock selected difficulty/surface, so edits never compound and do not overwrite the customiser or installed data. Native floors, contact conditions, speed conservation and state transitions still apply: a 2x setting is a parameter multiplier, not a guaranteed 2x measured jump/speed.
Trainer integration is compile-checked and the standalone Lua/schema suite covers command validation and setting delivery. Native gameplay effects, controller focus and two-panel layout still require the user-run launcher. No game was launched for this update.
The trainer update passes 14 standalone Lua/schema tests and a static-CRT release build. Use **PLAY-NATIVE-TRAINER.bat** for this update; its executable and preferences are isolated from the previous SDK test build.
## Draggable per-mod windows
Use `PLAY-MOD-WINDOWS.bat` for the latest settings-window build (`Build-ModWindows.ps1`). The ordinary Escape menu stays in its original centered position. Each enabled mod receives its own compact window, initially at the left, with a title you can drag. The title-bar minus button collapses that window; plus expands it. Each setting has separate minus/plus controls to decrease/increase numbers (within the manifest bounds), cycle choices, or toggle booleans. String controls open the full editor. Values are still saved and delivered to Lua immediately.
Positions and collapse state are retained for the current process, including closing Escape and disabling/re-enabling a mod; they are not saved across game restarts. Clicking a window brings it forward. Tab/controller X cycles focus through enabled mods and then back to the main pause controls; arrows select/adjust within the focused mod. Each window shows a continuous, clipped settings list. Use the mouse wheel over the window to scroll; keyboard/controller selection scrolls the selected setting into view. The value minus/plus buttons edit that row without changing the other rows. Manage mod opens that mod's enable/disable page. The main pause controls remain mouse-clickable when a mod has keyboard focus. Dragging prevents accidental main-menu mouse actions. No game was launched to validate this UI update; its interactive behavior remains for user testing.
## Showcase 2.0 and top-level packages
`PLAY-SHOWCASE.bat` / `Build-Showcase.ps1` provide the current showcase build.
The build directory contains the executable and a real `mods/native-trainer`
folder beside it. Only original Lua/JSON/text documentation is copied there.
Existing edited mod files are preserved on rebuilding, with a diagnostic if they
differ from bundled sources. The launcher relies on executable-relative discovery;
it does not redirect mods to the checkout. `SKATE3_MODS` remains an explicit override.
`mods/README.md` documents this layout for players; use Open mods folder in-game.
Native Trainer 2.0 now has 23 declarative settings: eleven native multipliers plus
HUD/units/label/help, breadcrumbs/spacing/colour and five keyboard shortcut choices.
The four added `sdk.trainer.apply` fields are `offboard_jump`, `grip`, `turn_power`
and `manual_drag` (all default 1, finite 0.25..4). Off-board jump scales the existing
biped `JumpInput.height_792` before native launch preparation. Grip scales ground
wheel static/dynamic friction and slide-friction strength. Turn power scales the
heading controller's turn strength; manual drag scales its balance drag. No raw
velocity, gravity, mass or state-controller replacement was added.
The player snapshot now additionally includes `heading:number`, the animation-root
yaw in radians (atan2 of forward X/Z), for source-backed checkpoint orientation.
The expanded Lua example demonstrates this native teleport request with a separate
trainer checkpoint; F5 saves and F6 returns by default. F7 starts/stops the active-play
stopwatch, F8 clears transient session data/markers, and F9 places a visual beacon.
All shortcuts are configurable choices. Saving is intended on valid clear ground;
a blocked native return reports a mod error rather than bypassing engine gates.
The HUD displays board speed/peak, approximate travel, position and observed bail/
grind statistics. It is not the career scoring system or precise odometry. Optional
breadcrumbs reuse 24 keys; beacons reuse eight; all visual objects have no collision.
Timers remove temporary notices; settings update visuals immediately; map changes
clear checkpoint, stopwatch, markers and statistics. Script reload resets transient
Lua state while preserving compatible saved settings. `help.txt` demonstrates
package-relative text loading. No private game assets are shipped.
Standalone Lua tests exercise save/return command payloads, stopwatch creation,
world-change cleanup, bounded trail keys and immediate HUD/tuning settings. Actual
native gameplay effects and visuals remain user-run; no game process was launched.
Trainer 2.1 adds `sdk.trainer.apply { hold_fakie = true }` (default false).
It blocks the automatic idle/bump transitions into the native stance-switch animation; normal
steering, slide overrides and trick inputs remain native. Turning the setting
off, disabling the mod or retiring its owner restores stock behavior.
Enabled-mod windows default to 360 by 760 pixels, bounded by the display.
Drag the bottom-right `//` grip to resize (minimum 320 by 260); the settings
viewport grows with the window and keeps mouse-wheel scrolling. Window sizes
are retained in memory while the game runs, alongside position/collapse state.
Each enabled-mod settings viewport also has a right-side scrollbar. Drag its thumb or click the track to jump; wheel and keyboard scrolling update the same thumb. It resizes with the window and dims when all settings fit.