From f73dc0f5d273c2de6f5eae391172254338d591e0 Mon Sep 17 00:00:00 2001 From: chazmm Date: Tue, 8 Sep 2026 19:05:17 +0100 Subject: [PATCH] Expand native trainer showcase and discover mods beside the executable --- Build-Showcase.ps1 | 56 ++++++ PLAY-SHOWCASE.bat | 23 +++ crates/skate-game/src/modding/menu.rs | 15 ++ crates/skate-game/src/modding/mod.rs | 13 +- .../src/physics/ground_runtime/settings.rs | 5 + .../src/physics/offboard/air_state.rs | 4 +- .../src/physics/offboard/air_trajectory.rs | 4 +- .../src/physics/offboard/ground_state.rs | 2 +- crates/skate-mods/src/lib.rs | 3 + crates/skate-mods/src/schema.rs | 12 ++ crates/skate-mods/tests/runtime.rs | 86 ++++++++++ docs/lua-modding.md | 41 ++++- mods/README.md | 20 +++ mods/native-trainer/README.md | 48 ++++++ mods/native-trainer/help.txt | 4 + mods/native-trainer/main.lua | 110 +++++++++++- mods/native-trainer/mod.json | 162 +++++++++++++++++- sdk/skate.lua | 5 + 18 files changed, 597 insertions(+), 16 deletions(-) create mode 100644 Build-Showcase.ps1 create mode 100644 PLAY-SHOWCASE.bat create mode 100644 mods/README.md create mode 100644 mods/native-trainer/README.md create mode 100644 mods/native-trainer/help.txt diff --git a/Build-Showcase.ps1 b/Build-Showcase.ps1 new file mode 100644 index 00000000..1fb9efac --- /dev/null +++ b/Build-Showcase.ps1 @@ -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 +} diff --git a/PLAY-SHOWCASE.bat b/PLAY-SHOWCASE.bat new file mode 100644 index 00000000..6ff065b3 --- /dev/null +++ b/PLAY-SHOWCASE.bat @@ -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 diff --git a/crates/skate-game/src/modding/menu.rs b/crates/skate-game/src/modding/menu.rs index 5e18f246..deaeac82 100644 --- a/crates/skate-game/src/modding/menu.rs +++ b/crates/skate-game/src/modding/menu.rs @@ -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(()) diff --git a/crates/skate-game/src/modding/mod.rs b/crates/skate-game/src/modding/mod.rs index 57e4ec16..662ee4be 100644 --- a/crates/skate-game/src/modding/mod.rs +++ b/crates/skate-game/src/modding/mod.rs @@ -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::().animation_info, "map":{"name":map.name,"generation":map.generation},"tick":physics.ticks,"keys":keys,"actions":actions, "paused":world.resource::().open,"replay":world.resource::().active}) diff --git a/crates/skate-game/src/physics/ground_runtime/settings.rs b/crates/skate-game/src/physics/ground_runtime/settings.rs index aa1c3850..452a2694 100644 --- a/crates/skate-game/src/physics/ground_runtime/settings.rs +++ b/crates/skate-game/src/physics/ground_runtime/settings.rs @@ -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<'_> { diff --git a/crates/skate-game/src/physics/offboard/air_state.rs b/crates/skate-game/src/physics/offboard/air_state.rs index b3e9694a..0abb8ca1 100644 --- a/crates/skate-game/src/physics/offboard/air_state.rs +++ b/crates/skate-game/src/physics/offboard/air_state.rs @@ -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); diff --git a/crates/skate-game/src/physics/offboard/air_trajectory.rs b/crates/skate-game/src/physics/offboard/air_trajectory.rs index 0039fd70..58341fba 100644 --- a/crates/skate-game/src/physics/offboard/air_trajectory.rs +++ b/crates/skate-game/src/physics/offboard/air_trajectory.rs @@ -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 { +pub(crate) fn prepare(skater: &SkaterRuntime, current: bool, height_multiplier: f32) -> Result { let p = &skater.player_input.processed; let toolkit = skater .player_input @@ -39,7 +39,7 @@ pub(crate) fn prepare(skater: &SkaterRuntime, current: bool) -> Result { } } 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 diff --git a/crates/skate-mods/src/lib.rs b/crates/skate-mods/src/lib.rs index d5a36d08..f6200b34 100644 --- a/crates/skate-mods/src/lib.rs +++ b/crates/skate-mods/src/lib.rs @@ -47,6 +47,9 @@ pub struct Manager { invalid_since: BTreeMap, } impl Manager { + pub fn root(&self) -> &Path { + &self.root + } pub fn new(root: PathBuf, preferences: PathBuf) -> Self { Self { packages: BTreeMap::new(), diff --git a/crates/skate-mods/src/schema.rs b/crates/skate-mods/src/schema.rs index f08b34fc..cb7329fe 100644 --- a/crates/skate-mods/src/schema.rs +++ b/crates/skate-mods/src/schema.rs @@ -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)) diff --git a/crates/skate-mods/tests/runtime.rs b/crates/skate-mods/tests/runtime.rs index 7296b8cb..570a335e 100644 --- a/crates/skate-mods/tests/runtime.rs +++ b/crates/skate-mods/tests/runtime.rs @@ -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.)) + ); +} diff --git a/docs/lua-modding.md b/docs/lua-modding.md index 109cfddc..8f776a45 100644 --- a/docs/lua-modding.md +++ b/docs/lua-modding.md @@ -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. diff --git a/mods/README.md b/mods/README.md new file mode 100644 index 00000000..e53e08d2 --- /dev/null +++ b/mods/README.md @@ -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. diff --git a/mods/native-trainer/README.md b/mods/native-trainer/README.md new file mode 100644 index 00000000..2e898a9a --- /dev/null +++ b/mods/native-trainer/README.md @@ -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. diff --git a/mods/native-trainer/help.txt b/mods/native-trainer/help.txt new file mode 100644 index 00000000..4a8ce437 --- /dev/null +++ b/mods/native-trainer/help.txt @@ -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. diff --git a/mods/native-trainer/main.lua b/mods/native-trainer/main.lua index 3c218e77..0847a20b 100644 --- a/mods/native-trainer/main.lua +++ b/mods/native-trainer/main.lua @@ -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 } diff --git a/mods/native-trainer/mod.json b/mods/native-trainer/mod.json index 110ff04f..5e285d69 100644 --- a/mods/native-trainer/mod.json +++ b/mods/native-trainer/mod.json @@ -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." } } } diff --git a/sdk/skate.lua b/sdk/skate.lua index b2d9907a..87642eba 100644 --- a/sdk/skate.lua +++ b/sdk/skate.lua @@ -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