Expand native trainer showcase and discover mods beside the executable

This commit is contained in:
chazmm committed 2026-09-08 19:05:17 +01:00
1 parent 49caf4d40d
commit f73dc0f5d2
18 files changed
+597 -16

No files matched your search

+56
View File
@@ -0,0 +1,56 @@
param([switch]$StageOnly, [string]$DependencyTargetDirectory = (Join-Path $PSScriptRoot 'target'))
$ErrorActionPreference = 'Stop'
$privateDirectory = Join-Path $PSScriptRoot '.local/showcase'
$buildDirectory = Join-Path $privateDirectory 'build'
$artifact = Join-Path $buildDirectory 'skate3rust-showcase.exe'
$destination = Join-Path $privateDirectory 'skate3-showcase.exe'
Push-Location $PSScriptRoot
$previousFlags = $env:CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_RUSTFLAGS
try {
New-Item -ItemType Directory -Force -Path $buildDirectory | Out-Null
if (-not $StageOnly) {
$env:CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_RUSTFLAGS = '-C target-feature=+crt-static'
# Dependencies may use a shared cache. The linker writes this task's
# artifact directly, never staging the cache's generic skate3rust.exe.
# Shared target fingerprints can refer to another worktree's local crates.
# Refresh only our local source timestamps; never clean the shared cache.
foreach ($crate in @('skate-core', 'skate-data', 'skate-mods')) {
(Get-Item -LiteralPath (Join-Path $PSScriptRoot "crates/$crate/src/lib.rs")).LastWriteTime = Get-Date
}
& cargo rustc --release --locked --target x86_64-pc-windows-msvc `
--target-dir $DependencyTargetDirectory -p skate-game --no-default-features `
--bin skate3rust -- -C extra-filename=-showcase `
-o (Join-Path $buildDirectory 'skate3rust.exe')
if ($LASTEXITCODE -ne 0) { throw 'Lua SDK build failed.' }
}
$bytes = [System.IO.File]::ReadAllBytes($artifact)
$text = [System.Text.Encoding]::ASCII.GetString($bytes)
foreach ($required in @('--start-paused', 'Drag title to move', 'Native trainer controls are already owned by another mod')) {
if (-not $text.Contains($required)) { throw "Wrong showcase artifact: missing $required" }
}
Copy-Item -LiteralPath $artifact -Destination $destination -Force
# Only original SDK example sources are staged. Never copy extracted assets.
$modDestination = Join-Path $privateDirectory 'mods/native-trainer'
New-Item -ItemType Directory -Force -Path $modDestination | Out-Null
foreach ($name in @('mod.json','main.lua','help.txt','README.md')) {
$source = Join-Path $PSScriptRoot "mods/native-trainer/$name"
$target = Join-Path $modDestination $name
if (-not (Test-Path -LiteralPath $target)) { Copy-Item -LiteralPath $source -Destination $target }
elseif ((Get-FileHash -LiteralPath $source).Hash -ne (Get-FileHash -LiteralPath $target).Hash) {
Write-Warning "Preserving existing mod file: $target. Copy the updated source there when ready."
}
}
Copy-Item -LiteralPath (Join-Path $PSScriptRoot 'mods/README.md') -Destination (Join-Path $privateDirectory 'mods/README.md') -Force
$hash = (Get-FileHash -LiteralPath $destination -Algorithm SHA256).Hash
if ($hash -ne (Get-FileHash -LiteralPath $artifact -Algorithm SHA256).Hash) {
throw 'Staged executable hash mismatch.'
}
@{ executable = $destination; sha256 = $hash; source = $PSScriptRoot;
revision = (& git rev-parse HEAD); staticChecks = 'CLI, Mods UI, Lua quota diagnostics' } |
ConvertTo-Json | Set-Content -LiteralPath (Join-Path $privateDirectory 'build-manifest.json') -Encoding UTF8
Write-Host "Ready for manual launch: $destination"
Write-Host "SHA256: $hash"
} finally {
$env:CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_RUSTFLAGS = $previousFlags
Pop-Location
}
+23
View File
@@ -0,0 +1,23 @@
@echo off
setlocal
cd /d "%~dp0"
if not defined SKATE3_ASSETS (
if exist "%~dp0.local\showcase\assets.path" set /p "SKATE3_ASSETS="<"%~dp0.local\showcase\assets.path"
)
if not defined SKATE3_ASSETS (
echo Set SKATE3_ASSETS to your prepared assets directory.
pause
exit /b 1
)
if not exist "%~dp0.local\showcase\skate3-showcase.exe" (
echo Build missing. Run Build-Showcase.ps1 first.
pause
exit /b 1
)
set "SKATE3_MOD_SETTINGS=%~dp0.local\showcase\settings"
if exist "%~dp0.local\showcase\overlay.path" set /p "SKATE3_SESSION_MARKER_OVERLAY="<"%~dp0.local\showcase\overlay.path"
echo Open Mods and ENABLE Native Trainer. Escape shows draggable settings windows. Drag their title bars.
echo Mods folder: %~dp0.local\showcase\mods
echo Trainer log: %~dp0.local\showcase\session.log
"%~dp0.local\showcase\skate3-showcase.exe" --assets "%SKATE3_ASSETS%" --start-paused %* > "%~dp0.local\showcase\session.log" 2>&1
if errorlevel 1 pause
+15
View File
@@ -39,6 +39,7 @@ enum Action {
Reset(String),
Setting(String, String),
Scan,
OpenFolder,
Back,
}
fn rows(menu: &ModMenu, mods: &Mods) -> Vec<(String, Action)> {
@@ -75,6 +76,7 @@ fn rows(menu: &ModMenu, mods: &Mods) -> Vec<(String, Action)> {
.iter()
.map(|(id, p)| (p.manifest.name.clone(), Action::Select(id.clone())))
.collect();
rows.push(("Open mods folder".into(), Action::OpenFolder));
rows.push(("Rescan packages".into(), Action::Scan));
rows.push(("Back to pause menu".into(), Action::Back));
rows
@@ -200,6 +202,19 @@ fn input(
menu.selected = 0;
Ok(())
}
Action::OpenFolder => {
#[cfg(target_os = "windows")]
let program = "explorer.exe";
#[cfg(target_os = "macos")]
let program = "open";
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
let program = "xdg-open";
std::process::Command::new(program)
.arg(mods.manager.root())
.spawn()
.map(|_| ())
.map_err(|e| format!("Could not open mods folder: {e}"))
}
Action::Scan => {
mods.manager.scan(true);
Ok(())
+11 -2
View File
@@ -29,7 +29,16 @@ impl Plugin for ModdingPlugin {
fn build(&self, app: &mut App) {
let root = std::env::var_os("SKATE3_MODS")
.map(std::path::PathBuf::from)
.unwrap_or_else(|| std::path::PathBuf::from("mods"));
.unwrap_or_else(|| {
std::env::current_exe()
.ok()
.and_then(|exe| exe.parent().map(|p| p.join("mods")))
.unwrap_or_else(|| std::path::PathBuf::from("mods"))
});
if let Err(e) = std::fs::create_dir_all(&root) {
warn!("Cannot create mods folder {}: {e}", root.display());
}
info!("Mod packages folder: {}", root.display());
let settings = std::env::var_os("SKATE3_MOD_SETTINGS")
.map(std::path::PathBuf::from)
.unwrap_or_else(|| {
@@ -108,7 +117,7 @@ fn snapshot(world: &World) -> serde_json::Value {
.0
.actions()
.values();
json!({"player":{"position":&s.animated_skeleton.roots.animation_to_world[3][..3],"velocity": &p.skateboard.vector_80.map(f32::from_bits)[..3],"on_board":p.state.category_12!=500,"state":p.state.state_16,"category":p.state.category_12,"bailing":physics.board_wiping_out,"grind":{"active":grinding,"name":grind_name,"kind":grind_kind,"distance":grind_distance}},
json!({"player":{"position":&s.animated_skeleton.roots.animation_to_world[3][..3],"velocity": &p.skateboard.vector_80.map(f32::from_bits)[..3],"heading":s.animated_skeleton.roots.animation_to_world[2][0].atan2(s.animated_skeleton.roots.animation_to_world[2][2]),"on_board":p.state.category_12!=500,"state":p.state.state_16,"category":p.state.category_12,"bailing":physics.board_wiping_out,"grind":{"active":grinding,"name":grind_name,"kind":grind_kind,"distance":grind_distance}},
"animation":world.resource::<Mods>().animation_info,
"map":{"name":map.name,"generation":map.generation},"tick":physics.ticks,"keys":keys,"actions":actions,
"paused":world.resource::<crate::graphics_menu::Menu>().open,"replay":world.resource::<crate::replay::Replay>().active})
@@ -215,6 +215,11 @@ impl GroundSettings {
result.propulsion.braking.override_force *= tuning.braking;
result.steering.general_scalar *= tuning.steering;
result.wobble_amplitude *= tuning.wobble;
result.slide.friction *= tuning.grip;
result.wheel_material.static_friction *= tuning.grip;
result.wheel_material.dynamic_friction *= tuning.grip;
result.heading.turn_strength *= tuning.turn_power;
result.drag.balance_drag *= tuning.manual_drag;
result
}
pub fn board(&self) -> GroundBoardSettings<'_> {
@@ -43,7 +43,7 @@ pub(crate) fn enter(physics: &mut GamePhysics, skater: &mut SkaterRuntime) -> Re
.skeleton_controller
.request(4, &mut skater.skeleton_collision)?;
if skater.offboard.air_prediction.is_none() {
let launch = super::air_trajectory::prepare(skater, false)?;
let launch = super::air_trajectory::prepare(skater, false, physics.trainer.offboard_jump)?;
super::air_trajectory::launch(physics, skater, launch)?;
}
let p = &skater.player_input.processed;
@@ -136,7 +136,7 @@ pub(crate) fn update(physics: &mut GamePhysics, skater: &mut SkaterRuntime) -> R
can_requery,
&mut skater.wipeout.state,
) {
let mut launch = super::air_trajectory::prepare(skater, false)?;
let mut launch = super::air_trajectory::prepare(skater, false, physics.trainer.offboard_jump)?;
launch.velocity = relaunch.velocity;
launch.has_target = true;
let feet = feet_world(skater);
@@ -4,7 +4,7 @@ use ground_query::GroundQueryScene;
use skate_core::player::offboard::{
air_completion, air_launch, air_prediction, air_queries, ground_query,
};
pub(crate) fn prepare(skater: &SkaterRuntime, current: bool) -> Result<air_launch::Launch, String> {
pub(crate) fn prepare(skater: &SkaterRuntime, current: bool, height_multiplier: f32) -> Result<air_launch::Launch, String> {
let p = &skater.player_input.processed;
let toolkit = skater
.player_input
@@ -39,7 +39,7 @@ pub(crate) fn prepare(skater: &SkaterRuntime, current: bool) -> Result<air_launc
&air_launch::JumpInput {
reference_up_144: c.state.frame_output.frame[1],
contact_active_708: c.state.contact.active,
height_792: skater.offboard.jump_height,
height_792: skater.offboard.jump_height * height_multiplier,
velocity_scalar_796: skater.offboard.jump_speed_scalar,
speed_704: c.state.motion.speed_704,
steering_760: c.state.intent.steering,
@@ -180,7 +180,7 @@ impl ground_sync::Services for Services<'_> {
}
}
fn prepare_biped_launch(&mut self) -> Self::Launch {
super::air_trajectory::prepare(self.skater, false)
super::air_trajectory::prepare(self.skater, false, self.physics.trainer.offboard_jump)
}
fn skeleton_point_10960(&self) -> Vector {
self.skater.animated_skeleton.record.centre_of_mass
+3
View File
@@ -47,6 +47,9 @@ pub struct Manager {
invalid_since: BTreeMap<String, Instant>,
}
impl Manager {
pub fn root(&self) -> &Path {
&self.root
}
pub fn new(root: PathBuf, preferences: PathBuf) -> Self {
Self {
packages: BTreeMap::new(),
+12
View File
@@ -144,6 +144,10 @@ pub struct TrainerTuning {
pub braking: f32,
pub steering: f32,
pub wobble: f32,
pub offboard_jump: f32,
pub grip: f32,
pub turn_power: f32,
pub manual_drag: f32,
}
impl Default for TrainerTuning {
fn default() -> Self {
@@ -155,6 +159,10 @@ impl Default for TrainerTuning {
braking: 1.,
steering: 1.,
wobble: 1.,
offboard_jump: 1.,
grip: 1.,
turn_power: 1.,
manual_drag: 1.,
}
}
}
@@ -167,6 +175,10 @@ impl TrainerTuning {
self.push_power,
self.braking,
self.steering,
self.offboard_jump,
self.grip,
self.turn_power,
self.manual_drag,
]
.into_iter()
.all(|v| v.is_finite() && (0.25..=4.).contains(&v))
+86
View File
@@ -297,3 +297,89 @@ fn trainer_commands_validate_and_follow_settings() {
m.enable("example", false).unwrap();
assert!(m.retired.contains(&"example".into()));
}
fn showcase_manager(f: &Fixture) -> Manager {
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../mods");
let mut m = Manager::new(root, f.0.join("showcase-settings"));
m.snapshot = json!({"player":{"position":[0,0,0],"velocity":[3,0,0],"heading":0.5,"state":100,"on_board":true,"bailing":false,"grind":{"active":false}},"keys":{},"map":{"name":"test","generation":0},"actions":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]});
m.scan(true);
m.enable("community.native-trainer", true).unwrap();
m.commands.clear();
m
}
#[test]
fn showcase_checkpoint_stopwatch_and_world_cleanup() {
let f = Fixture::new("return {}");
let mut m = showcase_manager(&f);
m.snapshot["keys"] = json!({"F5":true});
m.dispatch("on_update", json!({"dt":0.1}));
assert!(
m.commands
.iter()
.any(|(_, c)| matches!(c,Command::Cube{key,..} if key=="checkpoint"))
);
m.commands.clear();
m.snapshot["keys"] = json!({"F6":true,"F7":true});
m.dispatch("on_update", json!({"dt":0.1}));
assert!(
m.commands
.iter()
.any(|(_, c)| matches!(c,Command::Teleport{heading,..} if *heading==0.5))
);
assert!(
m.commands
.iter()
.any(|(_, c)| matches!(c,Command::Overlay{key,..} if key=="timer"))
);
m.commands.clear();
m.snapshot["keys"] = json!({});
m.dispatch("on_event", json!({"name":"world_changed"}));
assert!(
m.commands
.iter()
.any(|(_, c)| matches!(c,Command::Remove{key} if key=="checkpoint"))
);
m.commands.clear();
m.snapshot["keys"] = json!({"F6":true});
m.dispatch("on_update", json!({"dt":0.1}));
assert!(
!m.commands
.iter()
.any(|(_, c)| matches!(c, Command::Teleport { .. }))
);
assert!(m.packages["community.native-trainer"].running());
}
#[test]
fn showcase_reuses_trail_keys_and_updates_hud_settings() {
let f = Fixture::new("return {}");
let mut m = showcase_manager(&f);
let id = "community.native-trainer";
m.setting(id, "trail", json!(true)).unwrap();
m.commands.clear();
let mut trail = std::collections::BTreeSet::new();
for i in 0..100 {
m.snapshot["player"]["position"] = json!([i * 10, 0, 0]);
m.dispatch("on_update", json!({"dt":0.1}));
for (_, c) in &m.commands {
if let Command::Cube { key, .. } = c {
trail.insert(key.clone());
}
}
m.commands.clear();
}
assert_eq!(trail.len(), 24);
assert!(m.packages[id].running());
m.setting(id, "hud", json!("off")).unwrap();
assert!(
m.commands
.iter()
.any(|(_, c)| matches!(c,Command::Remove{key} if key=="hud"))
);
m.commands.clear();
m.setting(id, "grip", json!(2)).unwrap();
assert!(
m.commands
.iter()
.any(|(_, c)| matches!(c,Command::Trainer{tuning} if tuning.grip==2.))
);
}
+40 -1
View File
@@ -15,7 +15,7 @@ mods/my-mod/
assets/ # optional original text data or authored body clip JSON
```
`SKATE3_MODS` overrides the discovery directory; its default is `mods` relative to the process working directory. 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.
`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:
@@ -226,3 +226,42 @@ The trainer update passes 14 standalone Lua/schema tests and a static-CRT releas
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 pages seven settings with its own previous/next buttons. 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.
+20
View File
@@ -0,0 +1,20 @@
# Mods
This folder lives beside the game executable:
```
skate3rust.exe
mods/
native-trainer/
mod.json
main.lua
```
Drop each community mod's folder here, then open Escape -> Mods. New mods start
disabled. Use Open mods folder to return here from the game. Added/changed files
are discovered automatically, or choose Rescan packages. Keep personal settings
out of this folder. SKATE3_MODS is an optional explicit directory override.
Native Trainer Showcase is original Lua source, with stock tuning defaults.
Read its README for controls and examples of supported SDK capabilities.
Do not distribute private Skate assets with mods.
+48
View File
@@ -0,0 +1,48 @@
# Native Trainer Showcase 2.0
One original mod demonstrating the actual SDK, with no game assets. Enable it in
Escape -> Mods. Its independent draggable settings window opens on Escape.
The eleven physics controls start at 1x stock. Disable the mod to restore stock
parameters and remove all its HUD and world objects.
## Native controls
Pop height, grind pop, push speed, push power, brake strength, steering response,
speed wobble, off-board jump height, ground wheel/slide grip, heading turn force,
and manual balance drag. These scale existing inputs/settings, not simulation
speed or arbitrary body velocities. Most controls range 0.25x-4x; wobble is 0x-2x.
Current difficulty, native state gates and surface rules still apply.
## Showcase features
- Compact/full/off telemetry: board speed, peak, approximate travelled distance,
root position, bail count, grind entries and active grind time.
- F5 saves a trainer checkpoint; F6 requests return through the native reset path.
This does not replace the game's own session marker. Save on valid clear ground.
- F7 starts/stops the active-play stopwatch; its clock pauses with Escape/replay.
- F8 clears session statistics, stopwatch, checkpoint and all visual markers.
- F9 places a coloured visual beacon. Eight keyed beacons are reused in a ring.
- Optional breadcrumb trail reuses 24 tiny cubes at configurable distance spacing.
- Colour choices, units, HUD label, help hints and configurable keyboard shortcuts.
- Temporary notifications expire using owned timers. The log reads help.txt using
the package-relative file API. Edit source to see debounced hot reload in action.
Markers have no collision and are not grind rails. Statistics are local sampled
observations, not native career scores: travel rejects single-frame jumps of 50m
or more but is not replay-grade odometry. Board velocity is not walking speed.
The full HUD uses separate owned lines to avoid overlapping other notifications.
All transient state resets on script reload/disable or map change. Persisted
settings survive. The shortcut choice lists let authors try rebinding; use distinct
keys. If bindings coincide, actions execute in save/return/timer/clear/beacon order.
A native teleport rejection stops this mod and reports the error, as with any
SDK host-command failure; recover via Reload. No invincibility, general gravity
edit, physics collision insertion, arbitrary animation slot or score API is claimed.
## Source tour
main.lua deliberately separates apply (validated native tuning), update (input,
HUD, visual objects), on_event (map/bail/grind observations), and timer cleanup.
Use this as a practical starting point for training tools, navigation helpers,
telemetry overlays and original visual extensions. The engine's full API reference
is docs/lua-modding.md and language-server annotations are sdk/skate.lua.
+4
View File
@@ -0,0 +1,4 @@
Native Trainer Showcase 2.0
F5 save checkpoint; F6 return; F7 start/stop stopwatch; F8 clear session; F9 place beacon.
All bindings and visual options are configurable. All physics multipliers default to 1.
Telemetry reads the existing engine; cubes are visual only. No game assets are bundled.
+105 -5
View File
@@ -1,10 +1,110 @@
-- Native Trainer Showcase: original Lua, no game assets.
local tuning_keys={'pop','grind_pop','push_speed','push_power','braking','steering','wobble','offboard_jump','grip','turn_power','manual_drag'}
local previous, checkpoint, last_position, last_trail = {}, nil, nil, nil
local trail_index, beacon_index = 0, 0
local stats = {distance=0,peak=0,bails=0,grinds=0,grind_time=0}
local stopwatch, running, elapsed = 0, false, 0
local function copy(p) return {p[1],p[2],p[3]} end
local function distance(a,b) return math.sqrt((a[1]-b[1])^2+(a[2]-b[2])^2+(a[3]-b[3])^2) end
local function color()
return sdk.settings.marker_color=='orange' and {1,0.45,0.1} or sdk.settings.marker_color=='purple' and {0.7,0.25,1} or {0.1,0.85,0.85}
end
local function notice(text)
sdk.ui.text('notice',text)
sdk.time.after('notice',3,function() sdk.scene.remove('notice') end)
end
local function clear_trail()
for i=1,24 do sdk.scene.remove('trail'..i) end
last_trail=nil; trail_index=0
end
local function clear_session()
clear_trail()
for i=1,8 do sdk.scene.remove('beacon'..i) end
sdk.scene.remove('checkpoint'); sdk.scene.remove('timer'); sdk.scene.remove('notice')
sdk.time.cancel('notice')
checkpoint=nil; beacon_index=0; last_position=nil
stopwatch=0; running=false; elapsed=0
stats={distance=0,peak=0,bails=0,grinds=0,grind_time=0}
end
local function apply()
sdk.trainer.apply(sdk.settings)
local tuning={}
for _,key in ipairs(tuning_keys) do tuning[key]=sdk.settings[key] end
sdk.trainer.apply(tuning)
if not sdk.settings.trail then clear_trail() end
if sdk.settings.hud=='off' then sdk.scene.remove('hud'); sdk.scene.remove('hud_stats'); sdk.scene.remove('hud_help') end
end
local function draw()
local s=sdk.settings
if s.hud=='off' then sdk.scene.remove('hud'); sdk.scene.remove('hud_stats'); sdk.scene.remove('hud_help'); return end
local p=sdk.player.read(); local v=p.velocity
local speed=math.sqrt(v[1]^2+v[2]^2+v[3]^2)
local factor=s.units=='km/h' and 3.6 or s.units=='mph' and 2.236936 or 1
local label=s.hud_label; local cut=utf8.offset(label,25); if cut then label=label:sub(1,cut-1) end
local text=string.format('%s | %.1f %s | peak %.1f | %.0f m',label,speed*factor,s.units,stats.peak*factor,stats.distance)
if s.hud=='full' then
sdk.ui.text('hud_stats',string.format('XYZ %.1f %.1f %.1f | bails %d | grinds %d / %.1fs',p.position[1],p.position[2],p.position[3],stats.bails,stats.grinds,stats.grind_time))
else sdk.scene.remove('hud_stats') end
if s.show_help then sdk.ui.text('hud_help',string.format('%s save %s return %s timer %s clear %s beacon',s.save_key,s.return_key,s.timer_key,s.reset_key,s.beacon_key)) else sdk.scene.remove('hud_help') end
sdk.ui.text('hud',text)
end
local function update(e)
local p=sdk.player.read(); local position=p.position
elapsed=elapsed+e.dt
if last_position then local d=distance(position,last_position); if d<50 then stats.distance=stats.distance+d end end
last_position=copy(position)
local v=p.velocity; stats.peak=math.max(stats.peak,math.sqrt(v[1]^2+v[2]^2+v[3]^2))
if p.grind and p.grind.active then stats.grind_time=stats.grind_time+e.dt end
local edges={}; local down={}
for _,setting in ipairs({'save_key','return_key','timer_key','reset_key','beacon_key'}) do
local key=sdk.settings[setting]; down[key]=sdk.input.down(key); edges[setting]=down[key] and not previous[key]
end
previous=down
if edges.save_key then
if p.bailing or p.state==702 then notice('Wait until the skater is ready to save.')
else
checkpoint={position=copy(position),heading=p.heading or 0,on_board=p.on_board}
sdk.scene.cube('checkpoint',{position[1],position[2]+0.5,position[3]},{0.15,1,0.15},color())
notice('Trainer checkpoint saved (separate from the native session marker).')
end
end
if edges.return_key then
if not checkpoint then notice('Save a trainer checkpoint first.')
elseif p.bailing or p.state==702 then notice('Wait until recovery finishes.')
else
sdk.player.teleport(checkpoint.position,checkpoint.heading,checkpoint.on_board)
last_position=nil; last_trail=nil
notice('Returning to trainer checkpoint.')
end
end
if edges.timer_key then
if running then running=false; notice(string.format('Stopwatch stopped: %.2f seconds',stopwatch))
else stopwatch=0; running=true; notice('Stopwatch started.') end
end
if edges.reset_key then clear_session(); notice('Session stats, checkpoint and visual markers cleared.') end
if edges.beacon_key then
beacon_index=beacon_index%8+1
sdk.scene.cube('beacon'..beacon_index,{position[1],position[2]+1,position[3]},{0.25,2,0.25},color())
notice('Visual beacon placed; it has no collision.')
end
if sdk.settings.trail and (not last_trail or distance(position,last_trail)>=sdk.settings.trail_spacing) then
trail_index=trail_index%24+1; last_trail=copy(position)
sdk.scene.cube('trail'..trail_index,{position[1],position[2]+0.1,position[3]},{0.2,0.2,0.2},color())
end
if running then stopwatch=stopwatch+e.dt end
if running or stopwatch>0 then sdk.ui.text('timer',string.format('STOPWATCH: %.2f s%s',stopwatch,running and '' or ' (stopped)')) end
draw()
end
return {
on_load = apply,
on_settings = apply,
on_event = function(event)
if event.name == 'world_changed' then apply() end
on_load=function()
apply(); draw()
sdk.log(sdk.read_text('help.txt'))
notice('Native Trainer Showcase ready. Open Escape for settings.')
end,
on_settings=function() apply(); draw() end,
on_update=update,
on_event=function(e)
if e.name=='world_changed' then clear_session(); previous={}; apply(); draw(); notice('New map: trainer session cleared.') end
if e.name=='bail_changed' and e.bailing then stats.bails=stats.bails+1 end
if e.name=='grind_changed' and e.grind.active then stats.grinds=stats.grinds+1 end
end
}
+159 -3
View File
@@ -1,10 +1,10 @@
{
"id": "community.native-trainer",
"api": 1,
"name": "Native Trainer",
"version": "1.0.0",
"name": "Native Trainer Showcase",
"version": "2.0.0",
"author": "Skate Rust Engine",
"description": "Live native gameplay tuning. All values are multipliers of the current stock difficulty/surface. Disable to restore stock. Editable from the left Escape panel.",
"description": "Native tuning, telemetry, checkpoint return, stopwatch, breadcrumbs and beacons in one original Lua mod. Manage options here; default keyboard shortcuts F5 save, F6 return, F7 stopwatch, F8 reset, F9 beacon. Stock tuning is 1x.",
"entry": "main.lua",
"settings": {
"pop": {
@@ -69,6 +69,162 @@
"min": 0,
"max": 2,
"step": 0.25
},
"offboard_jump": {
"type": "number",
"label": "Off-board jump",
"description": "Multiplies native biped jump height. 1x preserves stock.",
"default": 1,
"min": 0.25,
"max": 4,
"step": 0.25
},
"grip": {
"type": "number",
"label": "Wheel / slide grip",
"description": "Multiplies ground wheel static/dynamic friction and sideways slide friction. 1x preserves stock.",
"default": 1,
"min": 0.25,
"max": 4,
"step": 0.25
},
"turn_power": {
"type": "number",
"label": "Turning force",
"description": "Multiplies native heading turn torque strength. 1x preserves stock.",
"default": 1,
"min": 0.25,
"max": 4,
"step": 0.25
},
"manual_drag": {
"type": "number",
"label": "Manual drag",
"description": "Multiplies native manual balance linear drag. 1x preserves stock.",
"default": 1,
"min": 0.25,
"max": 4,
"step": 0.25
},
"hud": {
"type": "choice",
"label": "Telemetry HUD",
"default": "full",
"choices": [
"off",
"compact",
"full"
],
"description": "Live board speed and session statistics. Full adds position, altitude and event counts."
},
"units": {
"type": "choice",
"label": "Speed units",
"default": "km/h",
"choices": [
"km/h",
"mph",
"m/s"
],
"description": "Display conversion only; does not alter physics."
},
"trail": {
"type": "boolean",
"label": "Breadcrumb trail",
"default": false,
"description": "Leave up to 24 small visual markers behind you. No collision. Cleared on map change."
},
"trail_spacing": {
"type": "number",
"label": "Trail spacing (m)",
"default": 6,
"min": 1,
"max": 30,
"step": 1,
"description": "Minimum distance between breadcrumb markers."
},
"marker_color": {
"type": "choice",
"label": "Marker colour",
"default": "cyan",
"choices": [
"cyan",
"orange",
"purple"
],
"description": "Colour of newly placed checkpoints, breadcrumbs and beacons."
},
"hud_label": {
"type": "string",
"label": "HUD label",
"default": "TRAINER",
"description": "An editable label demonstrating string settings."
},
"show_help": {
"type": "boolean",
"label": "Show key hints",
"default": true,
"description": "Show current shortcut bindings in the HUD."
},
"save_key": {
"type": "choice",
"label": "Save checkpoint key",
"default": "F5",
"choices": [
"F5",
"KeyJ",
"KeyK",
"KeyL"
],
"description": "Choose distinct shortcuts. If keys coincide, save, return, timer, reset, beacon run in that order."
},
"return_key": {
"type": "choice",
"label": "Return checkpoint key",
"default": "F6",
"choices": [
"F6",
"KeyJ",
"KeyK",
"KeyL"
],
"description": "Choose distinct shortcuts. If keys coincide, save, return, timer, reset, beacon run in that order."
},
"timer_key": {
"type": "choice",
"label": "Stopwatch key",
"default": "F7",
"choices": [
"F7",
"KeyJ",
"KeyK",
"KeyL"
],
"description": "Choose distinct shortcuts. If keys coincide, save, return, timer, reset, beacon run in that order."
},
"reset_key": {
"type": "choice",
"label": "Reset / clear key",
"default": "F8",
"choices": [
"F8",
"KeyJ",
"KeyK",
"KeyL"
],
"description": "Choose distinct shortcuts. If keys coincide, save, return, timer, reset, beacon run in that order."
},
"beacon_key": {
"type": "choice",
"label": "Place beacon key",
"default": "F9",
"choices": [
"F9",
"KeyJ",
"KeyK",
"KeyL"
],
"description": "Choose distinct shortcuts. If keys coincide, save, return, timer, reset, beacon run in that order."
}
}
}
+5
View File
@@ -11,6 +11,7 @@
---@class PlayerSnapshot
---@field position Vec3 Animation root position.
---@field velocity Vec3 Physical board velocity, including when offboard.
---@field heading number Animation-root yaw in radians, zero faces +Z.
---@field on_board boolean
---@field state integer Native state identifier, not a trick name.
---@field category integer
@@ -100,6 +101,10 @@ function sdk.animation.replace(path) end
---@field braking? number 0.25..4, foot and tail brake force.
---@field steering? number 0.25..4, general steering scalar.
---@field wobble? number 0..2, speed wobble amplitude.
---@field offboard_jump? number 0.25..4, native biped launch height.
---@field grip? number 0.25..4, wheel static/dynamic and sideways slide friction.
---@field turn_power? number 0.25..4, native heading turn strength.
---@field manual_drag? number 0.25..4, manual balance linear drag.
sdk.trainer = {}
---@param tuning TrainerTuning Owned, reversible native tuning. Conflicting owner is rejected.
function sdk.trainer.apply(tuning) end