mirror of
https://github.com/SK8-ENGINE/skate-3-rust-engine.git
synced 2026-10-26 19:47:44 +00:00
Expand native trainer showcase and discover mods beside the executable
This commit is contained in:
1 parent
49caf4d40d
commit
f73dc0f5d2
18 files changed
+597
-16
No files matched your search
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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(())
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
Reference in new issue
Block a user