mirror of
https://github.com/SK8-ENGINE/skate-3-rust-engine.git
synced 2026-10-26 19:47:44 +00:00
Replace examples with native trainer and expose enabled settings beside pause menu
This commit is contained in:
1 parent
72152f4b50
commit
b809fc0701
30 files changed
+626
-129
No files matched your search
@@ -0,0 +1,44 @@
|
||||
param([switch]$StageOnly, [string]$DependencyTargetDirectory = (Join-Path $PSScriptRoot 'target'))
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$privateDirectory = Join-Path $PSScriptRoot '.local/native-trainer'
|
||||
$buildDirectory = Join-Path $privateDirectory 'build'
|
||||
$artifact = Join-Path $buildDirectory 'skate3rust-native-trainer.exe'
|
||||
$destination = Join-Path $privateDirectory 'skate3-native-trainer.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=-native-trainer `
|
||||
-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', 'ENABLED MOD SETTINGS', 'Native trainer controls are already owned by another mod')) {
|
||||
if (-not $text.Contains($required)) { throw "Wrong native-trainer artifact: missing $required" }
|
||||
}
|
||||
Copy-Item -LiteralPath $artifact -Destination $destination -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\native-trainer\assets.path" set /p "SKATE3_ASSETS="<"%~dp0.local\native-trainer\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\native-trainer\skate3-native-trainer.exe" (
|
||||
echo Build missing. Run Build-NativeTrainer.ps1 first.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
set "SKATE3_MODS=%~dp0mods"
|
||||
set "SKATE3_MOD_SETTINGS=%~dp0.local\native-trainer\settings"
|
||||
if exist "%~dp0.local\native-trainer\overlay.path" set /p "SKATE3_SESSION_MARKER_OVERLAY="<"%~dp0.local\native-trainer\overlay.path"
|
||||
echo Open Mods and ENABLE Native Trainer. Escape shows its settings on the left.
|
||||
echo Trainer log: %~dp0.local\native-trainer\session.log
|
||||
"%~dp0.local\native-trainer\skate3-native-trainer.exe" --assets "%SKATE3_ASSETS%" --start-paused %* > "%~dp0.local\native-trainer\session.log" 2>&1
|
||||
if errorlevel 1 pause
|
||||
@@ -1,6 +1,7 @@
|
||||
//! Complete TU3 Toolkit_AntiFlipTorque 82D94190, distinct from heading/manual.
|
||||
use super::vector::{cross, dot3, normalize};
|
||||
use crate::{point_graph::PointGraph, trigonometry::acos};
|
||||
#[derive(Clone)]
|
||||
pub struct AntiFlipSettings {
|
||||
pub axis_96_response: PointGraph<8>, // +1832/+1864
|
||||
pub axis_64_response: PointGraph<8>, // +1912/+1944
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
//! The research index has not recovered its original name. Keep its four
|
||||
//! caller-supplied scalar arguments explicit instead of inventing units.
|
||||
use super::vector::{clamp, dot3, normalize};
|
||||
#[derive(Clone)]
|
||||
pub struct GroundForceSettings {
|
||||
pub range_1220: f32,
|
||||
pub speed_scale_1224: f32,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
//! AntiFlipTorque; the two are distinct. The caller owns board+284 history.
|
||||
use crate::{physics::reciprocal_sqrt::estimate, point_graph::PointGraph, trigonometry::acos};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct HeadingSettings {
|
||||
pub manual_wrong_wheel_scalar: f32, // +904
|
||||
pub manual_damping: f32, // +908
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
use super::vector::{clamp, dot3};
|
||||
use crate::{point_graph::PointGraph, trigonometry::acos};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SlideFrictionSettings {
|
||||
pub angle_response: PointGraph<16>, // +196/+260
|
||||
pub speed_response: PointGraph<8>, // +324/+356
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
use super::vector::{clamp, dot3, normalize};
|
||||
use crate::point_graph::PointGraph;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SpeedModelSettings {
|
||||
/// physics_speedmodel layout +0,+8,+16,+20,+24.
|
||||
pub negative_gain: f32,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! Complete TU3 Toolkit_CalcStraightenOutTorque 82D92B60.
|
||||
use super::vector::{clamp, cross, dot3};
|
||||
use crate::point_graph::PointGraph;
|
||||
#[derive(Clone)]
|
||||
pub struct StraightenSettings {
|
||||
pub time_response: PointGraph<16>, // settings +768/+832
|
||||
pub opposite_turn_limit: f32, // +896
|
||||
|
||||
@@ -252,6 +252,7 @@ pub(crate) fn interact(
|
||||
mut transition: ResMut<crate::map_transition::MapTransition>,
|
||||
mut customiser: ResMut<crate::customiser::Customiser>,
|
||||
mut mods: ResMut<crate::modding::ModMenu>,
|
||||
panel: Res<crate::modding::EnabledPanel>,
|
||||
nav: Res<crate::customiser::Navigation>,
|
||||
mut physics: ResMut<crate::physics::GamePhysics>,
|
||||
keys: Res<ButtonInput<KeyCode>>,
|
||||
@@ -269,6 +270,7 @@ pub(crate) fn interact(
|
||||
if keys.just_pressed(KeyCode::Escape) || nav.pressed & 0x10 != 0 {
|
||||
menu.open = !menu.open;
|
||||
}
|
||||
if panel.focused && menu.open { return; }
|
||||
let mut action = None;
|
||||
if menu.open {
|
||||
if keys.just_pressed(KeyCode::ArrowUp) || nav.pressed & 1 != 0 {
|
||||
@@ -407,12 +409,16 @@ fn labels(
|
||||
time: Res<Time<Real>>,
|
||||
customiser: Res<crate::customiser::Customiser>,
|
||||
mods: Res<crate::modding::ModMenu>,
|
||||
enabled_mods: Res<crate::modding::Mods>,
|
||||
window: Single<&Window, With<PrimaryWindow>>,
|
||||
mut root: Single<&mut Node, With<MenuRoot>>,
|
||||
mut labels: Query<(&MenuLabel, &mut Text), Without<StatusLabel>>,
|
||||
mut status: Single<&mut Text, With<StatusLabel>>,
|
||||
mut buttons: Query<(&MenuRow, &Interaction, &mut BackgroundColor)>,
|
||||
) {
|
||||
let has_enabled=enabled_mods.manager.packages.values().any(|p|p.running());
|
||||
root.justify_content=if has_enabled {JustifyContent::FlexEnd}else{JustifyContent::Center};
|
||||
root.padding.right=if has_enabled {percent(3)}else{px(0.)};
|
||||
root.display = if menu.open && !customiser.open && !mods.open {
|
||||
Display::Flex
|
||||
} else {
|
||||
|
||||
@@ -10,6 +10,10 @@ pub(crate) struct ModMenu {
|
||||
status: String,
|
||||
}
|
||||
impl ModMenu {
|
||||
pub fn configure(&mut self, id: String) {
|
||||
self.begin();
|
||||
self.id = Some(id);
|
||||
}
|
||||
pub fn begin(&mut self) {
|
||||
self.open = true;
|
||||
self.just_opened = true;
|
||||
@@ -24,6 +28,8 @@ struct Row(usize);
|
||||
#[derive(Component)]
|
||||
struct Label(usize);
|
||||
#[derive(Component)]
|
||||
struct Badge(usize);
|
||||
#[derive(Component)]
|
||||
struct Detail;
|
||||
#[derive(Clone)]
|
||||
enum Action {
|
||||
@@ -41,17 +47,11 @@ fn rows(menu: &ModMenu, mods: &Mods) -> Vec<(String, Action)> {
|
||||
if let Some(p) = manager.packages.get(id) {
|
||||
let mut rows = vec![
|
||||
(
|
||||
format!(
|
||||
"{} — {}",
|
||||
if p.enabled { "Disable" } else { "Enable" },
|
||||
if p.running() {
|
||||
"Running"
|
||||
} else if p.error.is_some() {
|
||||
"Error"
|
||||
} else {
|
||||
"Disabled"
|
||||
}
|
||||
),
|
||||
if p.running() {
|
||||
"DISABLE MOD".into()
|
||||
} else {
|
||||
"ENABLE MOD".into()
|
||||
},
|
||||
Action::Enable(id.clone()),
|
||||
),
|
||||
("Reload from disk".into(), Action::Reload(id.clone())),
|
||||
@@ -73,22 +73,7 @@ fn rows(menu: &ModMenu, mods: &Mods) -> Vec<(String, Action)> {
|
||||
let mut rows: Vec<_> = manager
|
||||
.packages
|
||||
.iter()
|
||||
.map(|(id, p)| {
|
||||
(
|
||||
format!(
|
||||
"{} [{}]",
|
||||
p.manifest.name,
|
||||
if p.running() {
|
||||
"Running"
|
||||
} else if p.error.is_some() {
|
||||
"Error"
|
||||
} else {
|
||||
"Disabled"
|
||||
}
|
||||
),
|
||||
Action::Select(id.clone()),
|
||||
)
|
||||
})
|
||||
.map(|(id, p)| (p.manifest.name.clone(), Action::Select(id.clone())))
|
||||
.collect();
|
||||
rows.push(("Rescan packages".into(), Action::Scan));
|
||||
rows.push(("Back to pause menu".into(), Action::Back));
|
||||
@@ -108,7 +93,7 @@ fn setup(mut commands: Commands) {
|
||||
commands.spawn((Root,GlobalZIndex(20),Node{display:Display::None,width:percent(100),height:percent(100),position_type:PositionType::Absolute,align_items:AlignItems::Center,justify_content:JustifyContent::Center,..default()},BackgroundColor(Color::srgba(0.015,0.025,0.04,0.98)))).with_children(|root| {
|
||||
root.spawn((Node{width:px(760),max_width:percent(95),padding:UiRect::all(px(18)),flex_direction:FlexDirection::Column,row_gap:px(5),..default()},BackgroundColor(Color::srgb(0.035,0.055,0.08)))).with_children(|panel| {
|
||||
panel.spawn((Text::new("MODS — Lua SDK 1"),TextFont{font_size:28.,..default()},TextColor(Color::WHITE)));
|
||||
for i in 0..8 { panel.spawn((Button,Row(i),Node{min_height:px(32),padding:UiRect::all(px(6)),..default()},BackgroundColor(Color::srgb(0.08,0.11,0.15)))).with_children(|row| {row.spawn((Label(i),Text::new(""),TextFont{font_size:17.,..default()},TextColor(Color::WHITE)));}); }
|
||||
for i in 0..8 { panel.spawn((Button,Row(i),Node{min_height:px(32),padding:UiRect::all(px(6)),justify_content:JustifyContent::SpaceBetween,..default()},BackgroundColor(Color::srgb(0.08,0.11,0.15)))).with_children(|row| {row.spawn((Label(i),Text::new(""),TextFont{font_size:17.,..default()},TextColor(Color::WHITE)));row.spawn((Badge(i),Text::new(""),TextFont{font_size:16.,..default()},TextColor(Color::WHITE)));}); }
|
||||
panel.spawn((Detail,Text::new(""),TextFont{font_size:15.,..default()},TextColor(Color::srgb(0.65,0.85,0.85))));
|
||||
panel.spawn((Text::new("Arrows / D-pad: select & adjust | Enter / A: choose | Esc / B: back\nStrings: Enter then type; Enter saves, Esc cancels. More rows scroll automatically."),TextFont{font_size:14.,..default()},TextColor(Color::WHITE)));
|
||||
});
|
||||
@@ -220,7 +205,7 @@ fn input(
|
||||
Ok(())
|
||||
}
|
||||
Action::Enable(id) => {
|
||||
let enabled = !mods.manager.packages[&id].enabled;
|
||||
let enabled = !mods.manager.packages[&id].running();
|
||||
mods.manager.enable(&id, enabled)
|
||||
}
|
||||
Action::Reload(id) => {
|
||||
@@ -269,9 +254,10 @@ fn draw(
|
||||
menu: Res<ModMenu>,
|
||||
mods: Res<Mods>,
|
||||
mut root: Single<&mut Node, With<Root>>,
|
||||
mut labels: Query<(&Label, &mut Text)>,
|
||||
mut labels: Query<(&Label, &mut Text), Without<Badge>>,
|
||||
mut badges: Query<(&Badge, &mut Text, &mut TextColor), Without<Label>>,
|
||||
mut buttons: Query<(&Row, &mut Node, &mut BackgroundColor), Without<Root>>,
|
||||
mut detail: Single<&mut Text, (With<Detail>, Without<Label>)>,
|
||||
mut detail: Single<&mut Text, (With<Detail>, Without<Label>, Without<Badge>)>,
|
||||
) {
|
||||
root.display = if menu.open {
|
||||
Display::Flex
|
||||
@@ -302,6 +288,21 @@ fn draw(
|
||||
Color::srgb(0.08, 0.11, 0.15)
|
||||
};
|
||||
}
|
||||
for (badge, mut text, mut color) in &mut badges {
|
||||
let package = entries.get(offset + badge.0).and_then(|(_, a)| match a {
|
||||
Action::Select(id) | Action::Enable(id) => mods.manager.packages.get(id),
|
||||
_ => None,
|
||||
});
|
||||
**text = package
|
||||
.map(|p| if p.running() { "ENABLED" } else { "DISABLED" })
|
||||
.unwrap_or("")
|
||||
.into();
|
||||
color.0 = if package.is_some_and(|p| p.running()) {
|
||||
Color::srgb(0.25, 1., 0.4)
|
||||
} else {
|
||||
Color::srgb(1., 0.25, 0.25)
|
||||
};
|
||||
}
|
||||
let mut description = format!("{} / {}\n{}", selected + 1, entries.len(), menu.status);
|
||||
if let Some(p) = menu
|
||||
.id
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
//! Main-thread SDK adapter. Lua never receives World, entity IDs, or asset handles.
|
||||
mod menu;
|
||||
mod panel;
|
||||
use bevy::prelude::*;
|
||||
pub(crate) use menu::ModMenu;
|
||||
pub(crate) use panel::EnabledPanel;
|
||||
use serde_json::json;
|
||||
use skate_mods::{Command, Manager};
|
||||
use std::collections::BTreeMap;
|
||||
@@ -10,6 +12,7 @@ use std::collections::BTreeMap;
|
||||
pub(crate) struct Mods {
|
||||
pub manager: Manager,
|
||||
animation_info: serde_json::Value,
|
||||
trainer: Option<(String, skate_mods::TrainerTuning)>,
|
||||
owned: BTreeMap<(String, String), Owned>,
|
||||
generation: u64,
|
||||
last_bail: bool,
|
||||
@@ -65,6 +68,7 @@ impl Plugin for ModdingPlugin {
|
||||
let animation_info = json!({"bone_names":frames.bone_names,"slots":slots});
|
||||
app.insert_resource(Mods {
|
||||
animation_info,
|
||||
trainer: None,
|
||||
manager: Manager::new(root, settings),
|
||||
owned: BTreeMap::new(),
|
||||
generation: u64::MAX,
|
||||
@@ -85,6 +89,7 @@ impl Plugin for ModdingPlugin {
|
||||
)
|
||||
.add_systems(Update, update.after(crate::app::FrameSet::Animation));
|
||||
menu::install(app);
|
||||
panel::install(app);
|
||||
}
|
||||
}
|
||||
fn snapshot(world: &World) -> serde_json::Value {
|
||||
@@ -205,6 +210,15 @@ fn retire(world: &mut World, mods: &mut Mods, key: &(String, String)) {
|
||||
}
|
||||
fn apply(world: &mut World, mods: &mut Mods) {
|
||||
let retired = std::mem::take(&mut mods.manager.retired);
|
||||
if mods
|
||||
.trainer
|
||||
.as_ref()
|
||||
.is_some_and(|(id, _)| retired.contains(id))
|
||||
{
|
||||
mods.trainer = None;
|
||||
}
|
||||
world.resource_mut::<crate::physics::GamePhysics>().trainer =
|
||||
mods.trainer.as_ref().map(|(_, t)| *t).unwrap_or_default();
|
||||
for id in &retired {
|
||||
world
|
||||
.resource::<crate::physics::SkaterRuntime>()
|
||||
@@ -273,6 +287,10 @@ fn apply(world: &mut World, mods: &mut Mods) {
|
||||
if let Err(e) = result {
|
||||
warn!("Lua mod {id}: {e}");
|
||||
mods.manager.fail(&id, e);
|
||||
if mods.trainer.as_ref().is_some_and(|(owner, _)| owner == &id) {
|
||||
mods.trainer = None;
|
||||
world.resource_mut::<crate::physics::GamePhysics>().trainer = Default::default();
|
||||
}
|
||||
world
|
||||
.resource::<crate::physics::SkaterRuntime>()
|
||||
.animation
|
||||
@@ -331,6 +349,13 @@ fn apply_one(world: &mut World, mods: &mut Mods, id: &str, command: Command) ->
|
||||
}
|
||||
}
|
||||
match command {
|
||||
Command::Trainer { tuning } => {
|
||||
if mods.trainer.as_ref().is_some_and(|(owner, _)| owner != id) {
|
||||
return Err("Native trainer controls are already owned by another mod".into());
|
||||
}
|
||||
mods.trainer = Some((id.to_owned(), tuning));
|
||||
world.resource_mut::<crate::physics::GamePhysics>().trainer = tuning;
|
||||
}
|
||||
Command::Animation { path } => {
|
||||
let root = &mods.manager.packages[id].root;
|
||||
let bytes = skate_mods::read_bounded(root, &path, 4 * 1024 * 1024)?;
|
||||
|
||||
@@ -0,0 +1,271 @@
|
||||
//! Enabled mod settings beside the normal pause menu, with independent focus.
|
||||
use super::{ModMenu, Mods};
|
||||
use bevy::prelude::*;
|
||||
#[derive(Resource, Default)]
|
||||
pub(crate) struct EnabledPanel {
|
||||
pub focused: bool,
|
||||
selected: usize,
|
||||
status: String,
|
||||
}
|
||||
#[derive(Component)]
|
||||
struct Root;
|
||||
#[derive(Component)]
|
||||
struct Row(usize);
|
||||
#[derive(Component)]
|
||||
struct Label(usize);
|
||||
#[derive(Component)]
|
||||
struct Hint;
|
||||
#[derive(Clone)]
|
||||
enum Entry {
|
||||
Mod(String),
|
||||
Setting(String, String),
|
||||
}
|
||||
fn entries(mods: &Mods) -> Vec<(String, Entry)> {
|
||||
let mut rows = vec![];
|
||||
for (id, p) in &mods.manager.packages {
|
||||
if p.running() {
|
||||
rows.push((
|
||||
format!("{} - ENABLED", p.manifest.name),
|
||||
Entry::Mod(id.clone()),
|
||||
));
|
||||
for (key, s) in &p.manifest.settings {
|
||||
let value = &p.settings[key];
|
||||
let display = value
|
||||
.as_f64()
|
||||
.map(|v| format!("{v:.2}"))
|
||||
.unwrap_or_else(|| {
|
||||
value
|
||||
.as_str()
|
||||
.map(str::to_owned)
|
||||
.unwrap_or_else(|| value.to_string())
|
||||
});
|
||||
rows.push((
|
||||
format!("{} {}", s.label, display),
|
||||
Entry::Setting(id.clone(), key.clone()),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
rows
|
||||
}
|
||||
pub(super) fn install(app: &mut App) {
|
||||
app.init_resource::<EnabledPanel>()
|
||||
.add_systems(PostStartup, setup)
|
||||
.add_systems(
|
||||
PreUpdate,
|
||||
input
|
||||
.after(crate::graphics_menu::MenuInput)
|
||||
.before(crate::map_transition::MapTransitionSet),
|
||||
)
|
||||
.add_systems(Update, draw);
|
||||
}
|
||||
fn setup(mut commands: Commands) {
|
||||
commands
|
||||
.spawn((
|
||||
Root,
|
||||
GlobalZIndex(12),
|
||||
Node {
|
||||
display: Display::None,
|
||||
position_type: PositionType::Absolute,
|
||||
left: percent(3),
|
||||
top: percent(8),
|
||||
width: percent(39),
|
||||
padding: UiRect::all(px(16)),
|
||||
row_gap: px(7),
|
||||
flex_direction: FlexDirection::Column,
|
||||
border_radius: BorderRadius::all(px(12)),
|
||||
..default()
|
||||
},
|
||||
BackgroundColor(Color::srgb(0.035, 0.055, 0.08)),
|
||||
))
|
||||
.with_children(|panel| {
|
||||
panel.spawn((
|
||||
Text::new("ENABLED MOD SETTINGS"),
|
||||
TextFont {
|
||||
font_size: 23.,
|
||||
..default()
|
||||
},
|
||||
TextColor(Color::srgb(0.25, 1., 0.4)),
|
||||
));
|
||||
for i in 0..10 {
|
||||
panel
|
||||
.spawn((
|
||||
Row(i),
|
||||
Button,
|
||||
Node {
|
||||
padding: UiRect::all(px(7)),
|
||||
min_height: px(32),
|
||||
..default()
|
||||
},
|
||||
BackgroundColor(Color::srgb(0.08, 0.11, 0.15)),
|
||||
))
|
||||
.with_children(|row| {
|
||||
row.spawn((
|
||||
Label(i),
|
||||
Text::new(""),
|
||||
TextFont {
|
||||
font_size: 16.,
|
||||
..default()
|
||||
},
|
||||
TextColor(Color::WHITE),
|
||||
));
|
||||
});
|
||||
}
|
||||
panel.spawn((
|
||||
Hint,
|
||||
Text::new(""),
|
||||
TextFont {
|
||||
font_size: 14.,
|
||||
..default()
|
||||
},
|
||||
TextColor(Color::srgb(0.65, 0.85, 0.85)),
|
||||
));
|
||||
});
|
||||
}
|
||||
fn visible(
|
||||
pause: &crate::graphics_menu::Menu,
|
||||
menu: &ModMenu,
|
||||
custom: &crate::customiser::Customiser,
|
||||
) -> bool {
|
||||
pause.open && !menu.open && !custom.open
|
||||
}
|
||||
fn input(
|
||||
mut panel: ResMut<EnabledPanel>,
|
||||
pause: Res<crate::graphics_menu::Menu>,
|
||||
mut menu: ResMut<ModMenu>,
|
||||
custom: Res<crate::customiser::Customiser>,
|
||||
mut mods: ResMut<Mods>,
|
||||
keys: Res<ButtonInput<KeyCode>>,
|
||||
nav: Res<crate::customiser::Navigation>,
|
||||
buttons: Query<(&Interaction, &Row), Changed<Interaction>>,
|
||||
) {
|
||||
let rows = entries(&mods);
|
||||
if !visible(&pause, &menu, &custom) || rows.is_empty() {
|
||||
panel.focused = false;
|
||||
return;
|
||||
}
|
||||
if keys.just_pressed(KeyCode::Tab) || nav.pressed & 0x4000 != 0 {
|
||||
panel.focused = !panel.focused;
|
||||
}
|
||||
panel.selected = panel.selected.min(rows.len() - 1);
|
||||
let mut direction = 0;
|
||||
if panel.focused {
|
||||
if keys.just_pressed(KeyCode::ArrowUp) || nav.pressed & 1 != 0 {
|
||||
panel.selected = (panel.selected + rows.len() - 1) % rows.len();
|
||||
}
|
||||
if keys.just_pressed(KeyCode::ArrowDown) || nav.pressed & 2 != 0 {
|
||||
panel.selected = (panel.selected + 1) % rows.len();
|
||||
}
|
||||
if keys.just_pressed(KeyCode::ArrowLeft) || nav.pressed & 4 != 0 {
|
||||
direction = -1;
|
||||
}
|
||||
if keys.just_pressed(KeyCode::ArrowRight)
|
||||
|| keys.just_pressed(KeyCode::Enter)
|
||||
|| nav.pressed & (8 | 0x1000) != 0
|
||||
{
|
||||
direction = 1;
|
||||
}
|
||||
}
|
||||
let offset = panel.selected / 10 * 10;
|
||||
for (interaction, row) in &buttons {
|
||||
if *interaction == Interaction::Pressed && offset + row.0 < rows.len() {
|
||||
panel.focused = true;
|
||||
panel.selected = offset + row.0;
|
||||
direction = 1;
|
||||
}
|
||||
}
|
||||
if direction == 0 {
|
||||
return;
|
||||
}
|
||||
match &rows[panel.selected].1 {
|
||||
Entry::Mod(id) => menu.configure(id.clone()),
|
||||
Entry::Setting(id, key) => {
|
||||
let p = &mods.manager.packages[id];
|
||||
let s = &p.manifest.settings[key];
|
||||
let v = &p.settings[key];
|
||||
let next = match s.kind.as_str() {
|
||||
"number" => Some(serde_json::json!(
|
||||
(v.as_f64().unwrap() + direction as f64 * s.step.unwrap())
|
||||
.clamp(s.min.unwrap(), s.max.unwrap())
|
||||
)),
|
||||
"boolean" => Some(serde_json::json!(!v.as_bool().unwrap())),
|
||||
"choice" => {
|
||||
let i = s
|
||||
.choices
|
||||
.iter()
|
||||
.position(|s| Some(s.as_str()) == v.as_str())
|
||||
.unwrap_or(0);
|
||||
Some(serde_json::json!(
|
||||
s.choices
|
||||
[(i as i32 + direction).rem_euclid(s.choices.len() as i32) as usize]
|
||||
))
|
||||
}
|
||||
_ => {
|
||||
menu.configure(id.clone());
|
||||
None
|
||||
}
|
||||
};
|
||||
if let Some(v) = next {
|
||||
panel.status = mods.manager.setting(id, key, v).err().unwrap_or_default();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fn draw(
|
||||
panel: Res<EnabledPanel>,
|
||||
pause: Res<crate::graphics_menu::Menu>,
|
||||
menu: Res<ModMenu>,
|
||||
custom: Res<crate::customiser::Customiser>,
|
||||
mods: Res<Mods>,
|
||||
mut root: Single<&mut Node, With<Root>>,
|
||||
mut rows: Query<(&Row, &mut Node, &mut BackgroundColor), Without<Root>>,
|
||||
mut labels: Query<(&Label, &mut Text), Without<Hint>>,
|
||||
mut hint: Single<&mut Text, (With<Hint>, Without<Label>)>,
|
||||
) {
|
||||
let list = entries(&mods);
|
||||
root.display = if visible(&pause, &menu, &custom) && !list.is_empty() {
|
||||
Display::Flex
|
||||
} else {
|
||||
Display::None
|
||||
};
|
||||
let offset = panel.selected / 10 * 10;
|
||||
for (label, mut text) in &mut labels {
|
||||
**text = list
|
||||
.get(offset + label.0)
|
||||
.map(|e| e.0.clone())
|
||||
.unwrap_or_default();
|
||||
}
|
||||
for (row, mut node, mut color) in &mut rows {
|
||||
node.display = if offset + row.0 < list.len() {
|
||||
Display::Flex
|
||||
} else {
|
||||
Display::None
|
||||
};
|
||||
color.0 = if panel.focused && offset + row.0 == panel.selected {
|
||||
Color::srgb(0.10, 0.30, 0.34)
|
||||
} else {
|
||||
Color::srgb(0.08, 0.11, 0.15)
|
||||
};
|
||||
}
|
||||
let description = list
|
||||
.get(panel.selected)
|
||||
.and_then(|(_, entry)| match entry {
|
||||
Entry::Setting(id, key) => Some(
|
||||
mods.manager.packages[id].manifest.settings[key]
|
||||
.description
|
||||
.as_str(),
|
||||
),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or("Select a mod name for its enable/disable controls.");
|
||||
***hint = format!(
|
||||
"Tab / X: {}\nUp/Down select | Left/Right adjust | Click increases\n{}\n{}",
|
||||
if panel.focused {
|
||||
"return to pause controls"
|
||||
} else {
|
||||
"focus mod settings"
|
||||
},
|
||||
description,
|
||||
panel.status
|
||||
);
|
||||
}
|
||||
@@ -88,6 +88,7 @@ pub(crate) struct GamePhysics {
|
||||
pub processed_flags_2468: u32,
|
||||
/// Toolkit ctor82C0680C clears8384bit7; wipeout entry/exit owns changes.
|
||||
pub board_wiping_out: bool,
|
||||
pub trainer: skate_mods::TrainerTuning,
|
||||
}
|
||||
|
||||
/// Cross-phase records for the current fixed tick. Subsystems retain their
|
||||
@@ -258,6 +259,7 @@ impl GamePhysics {
|
||||
exchange: SimulationExchange::new(0),
|
||||
processed_flags_2468,
|
||||
board_wiping_out: false,
|
||||
trainer: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -75,6 +75,9 @@ pub(super) fn advance(
|
||||
skater.player_input.processed.state_variant_index_2528,
|
||||
skater.player_input.processed.surface_mode_2540,
|
||||
)?;
|
||||
if physics.trainer != skate_mods::TrainerTuning::default() {
|
||||
skater.ground_settings = std::sync::Arc::new(skater.ground_settings.tuned(physics.trainer));
|
||||
}
|
||||
if teleported {
|
||||
skater.respawn.reset_measurements();
|
||||
skater.player_state.reset_for_teleport();
|
||||
|
||||
@@ -575,7 +575,7 @@ pub(super) fn update(physics: &mut GamePhysics, skater: &mut SkaterRuntime) -> R
|
||||
c.direction,
|
||||
board[3],
|
||||
c.centre,
|
||||
(1.0 - strength).mul_add(min, max * strength),
|
||||
(1.0 - strength).mul_add(min, max * strength) * physics.trainer.grind_pop,
|
||||
skater.animation_input.extra.grind_stability_nudge,
|
||||
physics.grind.manager_age,
|
||||
);
|
||||
|
||||
@@ -33,7 +33,7 @@ pub(super) fn advance(physics: &mut GamePhysics, skater: &mut SkaterRuntime) ->
|
||||
p.flags_2472,
|
||||
);
|
||||
physics.settings.wheel_material = s.wheel_material;
|
||||
let mode = *skater
|
||||
let mut mode = *skater
|
||||
.ground_animation_settings
|
||||
.modes
|
||||
.get(p.state_variant_index_2528 as usize)
|
||||
@@ -43,6 +43,9 @@ pub(super) fn advance(physics: &mut GamePhysics, skater: &mut SkaterRuntime) ->
|
||||
p.state_variant_index_2528
|
||||
)
|
||||
})?;
|
||||
mode.minimum_height_64 *= physics.trainer.pop;
|
||||
mode.minimum_height_68 *= physics.trainer.pop;
|
||||
mode.maximum_height *= physics.trainer.pop;
|
||||
skater.ground_animation.jump = ground_jump::calculate(
|
||||
GroundJumpInput {
|
||||
flags_2468: p.flags_2468,
|
||||
|
||||
@@ -118,7 +118,7 @@ impl GroundSettings {
|
||||
propulsion: GroundPropulsionInput {
|
||||
flags_2468: p.flags_2468,
|
||||
flags_2472: p.flags_2472,
|
||||
target_speed: a.contacts.push_speed,
|
||||
target_speed: a.contacts.push_speed * self.push_target_multiplier,
|
||||
signed_speed: p.scalar_2612,
|
||||
absolute_body_speed: t.absolute_speed,
|
||||
scalar_2660: t.total_mass,
|
||||
|
||||
@@ -39,6 +39,7 @@ impl GroundProfiles {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct GroundSettings {
|
||||
steering: SteeringSettings,
|
||||
wobble: SpeedWobbleSettings,
|
||||
@@ -57,6 +58,7 @@ pub(crate) struct GroundSettings {
|
||||
contact_force_time: f32,
|
||||
pub wheel_material: RetailContactMaterial,
|
||||
pub foot_force_offset: f32,
|
||||
pub push_target_multiplier: f32,
|
||||
pub absorption_front: f32,
|
||||
pub absorption_rear: f32,
|
||||
pub surface_braking_factor: f32,
|
||||
@@ -74,6 +76,7 @@ impl GroundSettings {
|
||||
//broadcasts82165A10 (zero) into830BD380 for speed override direction.
|
||||
let threshold = [f32::from_bits(0x3586_37bd); 4];
|
||||
Ok(Self {
|
||||
push_target_multiplier: 1.,
|
||||
foot_force_offset: f("physics_feet", "FootForceOffset")?,
|
||||
absorption_front: f("physics_feet", "AbsorptionFootForceScalar")?,
|
||||
absorption_rear: f("physics_feet", "AbsorptionFootForceRearScalar")?,
|
||||
@@ -203,6 +206,17 @@ impl GroundSettings {
|
||||
wobble_amplitude: m("Hash_5B57F2CCCCEEF430")?,
|
||||
})
|
||||
}
|
||||
pub fn tuned(&self, tuning: skate_mods::TrainerTuning) -> Self {
|
||||
let mut result=self.clone();
|
||||
result.push_target_multiplier = tuning.push_speed;
|
||||
result.propulsion.maximum_pushable_speed *= tuning.push_speed;
|
||||
for dv in &mut result.propulsion.mode_speed_changes { *dv *= tuning.push_power; }
|
||||
result.propulsion.braking.input_force *= tuning.braking;
|
||||
result.propulsion.braking.override_force *= tuning.braking;
|
||||
result.steering.general_scalar *= tuning.steering;
|
||||
result.wobble_amplitude *= tuning.wobble;
|
||||
result
|
||||
}
|
||||
pub fn board(&self) -> GroundBoardSettings<'_> {
|
||||
GroundBoardSettings {
|
||||
steering: &self.steering,
|
||||
|
||||
@@ -45,3 +45,6 @@ end
|
||||
sdk.animation = {}
|
||||
function sdk.animation.replace(path) submit{kind="animation",path=path} end
|
||||
function sdk.animation.info() return sdk.snapshot.animation end
|
||||
|
||||
sdk.trainer = {}
|
||||
function sdk.trainer.apply(tuning) submit{kind="trainer",tuning=tuning} end
|
||||
@@ -132,3 +132,45 @@ impl Setting {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Whitelisted native trainer multipliers. 1.0 preserves the installed stock value.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize)]
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
pub struct TrainerTuning {
|
||||
pub pop: f32,
|
||||
pub grind_pop: f32,
|
||||
pub push_speed: f32,
|
||||
pub push_power: f32,
|
||||
pub braking: f32,
|
||||
pub steering: f32,
|
||||
pub wobble: f32,
|
||||
}
|
||||
impl Default for TrainerTuning {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
pop: 1.,
|
||||
grind_pop: 1.,
|
||||
push_speed: 1.,
|
||||
push_power: 1.,
|
||||
braking: 1.,
|
||||
steering: 1.,
|
||||
wobble: 1.,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl TrainerTuning {
|
||||
pub fn valid(&self) -> bool {
|
||||
[
|
||||
self.pop,
|
||||
self.grind_pop,
|
||||
self.push_speed,
|
||||
self.push_power,
|
||||
self.braking,
|
||||
self.steering,
|
||||
]
|
||||
.into_iter()
|
||||
.all(|v| v.is_finite() && (0.25..=4.).contains(&v))
|
||||
&& self.wobble.is_finite()
|
||||
&& (0. ..=2.).contains(&self.wobble)
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,9 @@ use std::{
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
|
||||
pub enum Command {
|
||||
Trainer {
|
||||
tuning: crate::TrainerTuning,
|
||||
},
|
||||
Log {
|
||||
text: String,
|
||||
},
|
||||
@@ -42,6 +45,7 @@ impl Command {
|
||||
fn validate(&self) -> bool {
|
||||
let point = |p: &[f32; 3]| p.iter().all(|v| v.is_finite() && v.abs() <= 100_000.);
|
||||
match self {
|
||||
Self::Trainer { tuning } => tuning.valid(),
|
||||
Self::Animation { path } => !path.is_empty() && path.len() <= 256,
|
||||
Self::Log { text } => text.len() <= 2048,
|
||||
Self::Overlay { key, text } => crate::schema::valid_id(key) && text.len() <= 1024,
|
||||
|
||||
@@ -152,7 +152,7 @@ fn examples_load_and_run() {
|
||||
m.scan(true);
|
||||
assert!(m.diagnostics.is_empty(), "{:?}", m.diagnostics);
|
||||
let ids: Vec<_> = m.packages.keys().cloned().collect();
|
||||
assert!(ids.len() >= 2);
|
||||
assert_eq!(ids, vec!["community.native-trainer"]);
|
||||
for id in ids {
|
||||
m.enable(&id, true).unwrap();
|
||||
for _ in 0..5 {
|
||||
@@ -271,3 +271,29 @@ fn changed_source_waits_for_debounce() {
|
||||
m.scan(false);
|
||||
assert_eq!(m.commands.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trainer_commands_validate_and_follow_settings() {
|
||||
let f = Fixture::new(
|
||||
"return {on_load=function() sdk.trainer.apply{pop=2,push_speed=3,wobble=0} end, on_settings=function() sdk.trainer.apply{pop=sdk.settings.count} end}",
|
||||
);
|
||||
let mut m = f.manager();
|
||||
m.enable("example", true).unwrap();
|
||||
assert!(
|
||||
matches!(&m.commands[0].1,Command::Trainer{tuning} if tuning.pop==2. && tuning.push_speed==3. && tuning.wobble==0. && tuning.braking==1.)
|
||||
);
|
||||
m.setting("example", "count", json!(4)).unwrap();
|
||||
assert!(matches!(&m.commands.last().unwrap().1,Command::Trainer{tuning} if tuning.pop==4.));
|
||||
m.setting("example", "count", json!(5)).unwrap();
|
||||
assert!(!m.packages["example"].running());
|
||||
assert!(m.commands.is_empty());
|
||||
assert!(
|
||||
!TrainerTuning {
|
||||
pop: f32::NAN,
|
||||
..Default::default()
|
||||
}
|
||||
.valid()
|
||||
);
|
||||
m.enable("example", false).unwrap();
|
||||
assert!(m.retired.contains(&"example".into()));
|
||||
}
|
||||
+22
-8
@@ -2,11 +2,11 @@
|
||||
|
||||
This is an implemented, in-process Lua 5.4 SDK for this engine. Open **Escape/Start → Mods**. New packages are disabled. Select a package to enable/disable, reload, edit settings or reset defaults. Up/down and D-pad scroll eight visible rows; left/right changes booleans, numbers and choices; Enter/A chooses. Strings use keyboard text entry, Backspace, Enter to commit and Escape to cancel. B/Escape returns one level without resuming behind the menu. The existing map, marker, bail and character menus remain the owners of their native behavior.
|
||||
|
||||
Run `PLAY-LUA-SDK.bat` for the task-specific build. It starts paused, uses isolated mod preferences, and writes `.local/lua-sdk/session.log`. It requires prepared engine assets and the executable generated by `Build-LuaSDK.ps1`. Set `SKATE3_ASSETS` or the private `.local/lua-sdk/assets.path`; the checked-in launcher contains no personal installation path. No Lua installation is needed.
|
||||
Run `PLAY-NATIVE-TRAINER.bat` for the current trainer build (`Build-NativeTrainer.ps1` builds it); `PLAY-LUA-SDK.bat` retains the earlier SDK build. It starts paused, uses isolated mod preferences, and writes `.local/lua-sdk/session.log`. It requires prepared engine assets and the executable generated by `Build-LuaSDK.ps1`. Set `SKATE3_ASSETS` or the private `.local/lua-sdk/assets.path`; the checked-in launcher contains no personal installation path. No Lua installation is needed.
|
||||
|
||||
## Quick start
|
||||
|
||||
Copy `mods/speedometer` to another immediate child of `mods`, change its `id` and `name`, then choose Rescan packages. A package consists of:
|
||||
Copy `mods/native-trainer` to another immediate child of `mods`, change its `id` and `name`, then choose Rescan packages. A package consists of:
|
||||
|
||||
```text
|
||||
mods/my-mod/
|
||||
@@ -69,7 +69,7 @@ All manifest fields in the minimal example except `settings` are required. `api`
|
||||
|
||||
`min`, `max`, `step` and `choices` are optional on other types and have no effect there. Number step controls the menu increment; values need not lie on that increment grid. Saved values are type/range validated. Missing, invalid or renamed values use defaults; removed keys are discarded on loading the current schema. Same-key compatible values survive version changes. Renaming a key intentionally resets it. There is no arbitrary Lua migration callback or persisted mutable Lua state in API 1. Changing the package ID creates a distinct preference identity. Reset delivers one `on_settings` notification per setting in key order. Preferences store `{ "enabled": boolean, "values": {...} }` as `<id>.json` through a sibling temporary file and rename. Save errors appear in the menu; the live edit may already have applied. Never distribute these preference files with a mod. Enable preference and actual running status differ after a fault: the error remains visible and the mod stays stopped until reload, a settled edit, or a future launch.
|
||||
|
||||
There is no generic gamepad rebinding setting yet. A choice containing supported key names, as in Route Trainer, is an ordinary validated setting; it does not remap native input.
|
||||
There is no generic gamepad rebinding setting yet. A choice containing supported key names, as an ordinary key-choice schema, is an ordinary validated setting; it does not remap native input.
|
||||
|
||||
## Lifecycle, ordering and scheduling
|
||||
|
||||
@@ -92,7 +92,7 @@ No callbacks accumulate across reload: there is one returned lifecycle table and
|
||||
|
||||
Polling occurs at most every 500 ms and hashes bounded package contents (not just timestamps). Changed valid files must retain the same hash for at least 750 ms, so usual effective latency is 1–1.5 seconds. Invalid manifests get a 750 ms grace interval before the old package is retired and diagnostics replace its listing. Deleted directories retire immediately at the next poll. New packages are discovered disabled unless their ID already has a saved enable preference. Explicit Rescan/Reload bypasses debounce: finish saving before using it. A failed reload does not retain the previous version; it remains visibly stopped. Fix/save or explicitly reload to recover. Polling runs on the main thread, so large packages or slow storage can hitch; no zero-hitch guarantee is made. A root-directory I/O error reports diagnostics; a missing root retires its packages.
|
||||
|
||||
Successful world changes remove **all** mod objects and animation overrides before `world_changed` is delivered. Lua locals, settings and timers survive this event. Rebuild world-specific objects in the callback, cancel any world-specific timers, and clear stored positions (Route Trainer does this). `sdk.snapshot.map.generation` identifies the current world, including reloading the same map. Failed map preparation retains the previous world and emits no world-change event.
|
||||
Successful world changes remove **all** mod objects and animation overrides before `world_changed` is delivered. Lua locals, settings and timers survive this event. Rebuild world-specific objects in the callback, cancel any world-specific timers, and clear stored positions when retaining world-specific positions. `sdk.snapshot.map.generation` identifies the current world, including reloading the same map. Failed map preparation retains the previous world and emits no world-change event.
|
||||
|
||||
## Complete SDK API
|
||||
|
||||
@@ -145,7 +145,7 @@ Cube positions and teleport positions must be finite, each component within ±10
|
||||
}
|
||||
```
|
||||
|
||||
Keyboard names follow Bevy 0.18 `KeyCode` debug names, e.g. `KeyA`, `KeyJ`, `F6`, `Space`, `ArrowUp`, `ShiftLeft`. Held-state reads do not consume input or inject native actions. Route Trainer demonstrates edge detection. Native actions expose the current published mapping: 64/65 left stick axes, 66 left trigger, 67/68 right stick axes, 69 right trigger, 70/71 stick clicks, 72/73 shoulders, 74–77 face buttons, 78/79 Start/Back, 80/81 D-pad horizontal/vertical as provided by the existing expression map. These are observations, not a binding editor. Consult `crates/skate-core/src/input/gameplay_map.rs` for the retained source mapping; no new native input semantics are introduced.
|
||||
Keyboard names follow Bevy 0.18 `KeyCode` debug names, e.g. `KeyA`, `KeyJ`, `F6`, `Space`, `ArrowUp`, `ShiftLeft`. Held-state reads do not consume input or inject native actions. Track the previous held state in a Lua local for edge detection. Native actions expose the current published mapping: 64/65 left stick axes, 66 left trigger, 67/68 right stick axes, 69 right trigger, 70/71 stick clicks, 72/73 shoulders, 74–77 face buttons, 78/79 Start/Back, 80/81 D-pad horizontal/vertical as provided by the existing expression map. These are observations, not a binding editor. Consult `crates/skate-core/src/input/gameplay_map.rs` for the retained source mapping; no new native input semantics are introduced.
|
||||
|
||||
`on_event` receives one of:
|
||||
|
||||
@@ -193,16 +193,30 @@ Distribute your package directory with source, original permitted data, a licens
|
||||
|
||||
## Validation and manual test checklist
|
||||
|
||||
`cargo test --locked -p skate-mods --target x86_64-pc-windows-msvc` runs the standalone window-free Lua/schema suite. It covers both example programs, reload state reset, timers/replacement/order, settings persistence and validation, duplicate IDs, removal/invalid manifests, path restrictions, failure isolation, unload failure, instruction and memory limits. This is not rendered-gameplay validation.
|
||||
`cargo test --locked -p skate-mods --target x86_64-pc-windows-msvc` runs the standalone window-free Lua/schema suite. It covers the bundled Native Trainer, reload state reset, timers/replacement/order, settings persistence and validation, duplicate IDs, removal/invalid manifests, path restrictions, failure isolation, unload failure, instruction and memory limits. This is not rendered-gameplay validation.
|
||||
|
||||
Use `Build-LuaSDK.ps1 -DependencyTargetDirectory <shared cargo cache>` to produce a uniquely named direct linker output, check SDK/CLI diagnostic strings without executing it, and record a staged SHA-256. Do not copy the generic executable from a shared Cargo target. `PLAY-LUA-SDK.bat` is provided exclusively for the user's manual run. No game, recomp, game asset-check mode, automated controller input, screenshot capture or game-based validation was executed for this SDK task.
|
||||
|
||||
Manual checks still required: open/scroll/back with keyboard/controller; enable both examples; edit all setting types; hot-edit and reload; deliberately cause a Lua error then recover; add/remove/duplicate a package; disable and verify HUD/cubes disappear; switch maps and check marker/customiser/bail behavior; record and run a route; inspect original authored animation replacement if available. Teleport destinations and animation visuals require deliberate author testing. Compilation and window-free runtime tests do not establish these gameplay results.
|
||||
Manual checks still required: open/scroll/back with keyboard/controller; enable Native Trainer; edit all setting types; hot-edit and reload; deliberately cause a Lua error then recover; add/remove/duplicate a package; disable and verify HUD/cubes disappear; switch maps and check marker/customiser/bail behavior; adjust pop/push settings, then disable and check stock behavior; inspect original authored animation replacement if available. Teleport destinations and animation visuals require deliberate author testing. Compilation and window-free runtime tests do not establish these gameplay results.
|
||||
|
||||
Integration footprint: `skate-mods` owns VM/schema/discovery/preferences; `skate-game/modding` owns menu and command adapter. Shared-file changes are plugin wiring (`main.rs`, `app.rs`), one pause-menu row, a read-only grind observation method and the existing animation evaluator/parser's owner-scoped overrides. Other owners should retain these narrow hooks when merging. Baseline is local merge `26f056c`; no original checkout, private asset tree or other task configuration was modified.
|
||||
|
||||
### Recorded non-game results
|
||||
|
||||
On 2026-09-08, all 13 standalone SDK tests passed. The Windows release build and direct-output string/SHA-256 identity checks passed. The first release attempt diagnosed a stale shared `skate-core` artifact missing the merged respawn module; the helper now refreshes only this worktree's local crate source timestamps before compiling, preserving all dependency cache contents. Existing engine warnings remain. No interactive menu, native teleport or authored animation visual result is claimed.
|
||||
On 2026-09-08, all 13 original standalone SDK tests passed. The Windows release build and direct-output string/SHA-256 identity checks passed. The first release attempt diagnosed a stale shared `skate-core` artifact missing the merged respawn module; the helper now refreshes only this worktree's local crate source timestamps before compiling, preserving all dependency cache contents. Existing engine warnings remain. No interactive menu, native teleport or authored animation visual result is claimed.
|
||||
|
||||
Private manual-test setup uses this task's own real asset-root directory with a read-only junction to the prepared `private` data, and its own settings directory. Maps are referenced through a junction. The launcher therefore does not use another task's writable settings root. None of these references, prepared assets, personal paths or binaries are tracked.
|
||||
|
||||
## Native Trainer and pause-panel update
|
||||
|
||||
The bundled Speedometer and Route Trainer examples have been removed. `mods/native-trainer` is the single bundled mod, disabled by default. Its seven settings control pop height, grind pop height, push speed, push power, braking, steering and wobble. All start at **1.00x stock**. Pop/grind pop/push speed/push power/braking/steering range from 0.25x to 4x; wobble ranges from 0x to 2x, in 0.25 increments. There is no direct board-velocity write, invincibility or global gravity cheat.
|
||||
|
||||
The Mods list now has a right-aligned **ENABLED** badge in green or **DISABLED** in red; the selected mod has an explicit **ENABLE MOD / DISABLE MOD** action. A faulted mod is DISABLED with its error in the detail area. Whenever a mod is running, Escape shows its settings in a separate left-hand panel beside the normal pause controls. Click a value to increase/cycle it, or press **Tab / controller X** to focus that panel, then **Up/Down** to select and **Left/Right** to adjust. Tab/X returns focus to normal pause controls. Select the mod name to open its full enable/disable/settings page. Text settings open the full editor. Lists longer than ten rows page as selection advances.
|
||||
|
||||
`sdk.trainer.apply(tuning: table) -> nil` queues an owner-scoped native tuning override. Optional fields `pop`, `grind_pop`, `push_speed`, `push_power`, `braking`, `steering`, `wobble` have the ranges above and default to 1 on **each call**. Unknown fields and nonfinite/out-of-range values raise errors. Only one mod can own trainer tuning at a time; another owner is rejected and stopped. Disabling, reloading or faulting the owner restores default multipliers. Active tuning survives map replacement and is reapplied before further simulation. Existing momentum and an already-launched jump are not rewound; subsequent physics calculations use current tuning.
|
||||
|
||||
Source bindings: ground pop scales the current `GroundJumpMode` minimum/maximum heights before the existing ground-jump calculation. Grind pop scales the existing interpolated launch-height argument. Push speed scales the animation-provided push target plus `maximum_pushable_speed`; push power scales difficulty-specific `mode_speed_changes`. Brake strength scales the native foot/tail brake force and steering scales `general_scalar`. Wobble scales the existing mode amplitude multiplier. Ground profiles are derived anew from the stock selected difficulty/surface, so edits never compound and do not overwrite the customiser or installed data. Native floors, contact conditions, speed conservation and state transitions still apply: a 2x setting is a parameter multiplier, not a guaranteed 2x measured jump/speed.
|
||||
|
||||
Trainer integration is compile-checked and the standalone Lua/schema suite covers command validation and setting delivery. Native gameplay effects, controller focus and two-panel layout still require the user-run launcher. No game was launched for this update.
|
||||
|
||||
The trainer update passes 14 standalone Lua/schema tests and a static-CRT release build. Use **PLAY-NATIVE-TRAINER.bat** for this update; its executable and preferences are isolated from the previous SDK test build.
|
||||
@@ -0,0 +1,10 @@
|
||||
local function apply()
|
||||
sdk.trainer.apply(sdk.settings)
|
||||
end
|
||||
return {
|
||||
on_load = apply,
|
||||
on_settings = apply,
|
||||
on_event = function(event)
|
||||
if event.name == 'world_changed' then apply() end
|
||||
end
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
{
|
||||
"id": "community.native-trainer",
|
||||
"api": 1,
|
||||
"name": "Native Trainer",
|
||||
"version": "1.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.",
|
||||
"entry": "main.lua",
|
||||
"settings": {
|
||||
"pop": {
|
||||
"type": "number",
|
||||
"label": "Pop height",
|
||||
"description": "Multiplies native ground-jump minimum and maximum heights. 1.00x is stock.",
|
||||
"default": 1,
|
||||
"min": 0.25,
|
||||
"max": 4,
|
||||
"step": 0.25
|
||||
},
|
||||
"grind_pop": {
|
||||
"type": "number",
|
||||
"label": "Grind pop height",
|
||||
"description": "Multiplies the existing grind-launch height. 1.00x is stock.",
|
||||
"default": 1,
|
||||
"min": 0.25,
|
||||
"max": 4,
|
||||
"step": 0.25
|
||||
},
|
||||
"push_speed": {
|
||||
"type": "number",
|
||||
"label": "Maximum push speed",
|
||||
"description": "Multiplies the speed up to which pushing works. This is not a downhill speed cap. 1.00x is stock.",
|
||||
"default": 1,
|
||||
"min": 0.25,
|
||||
"max": 4,
|
||||
"step": 0.25
|
||||
},
|
||||
"push_power": {
|
||||
"type": "number",
|
||||
"label": "Push power",
|
||||
"description": "Multiplies native difficulty-specific push velocity changes. 1.00x is stock.",
|
||||
"default": 1,
|
||||
"min": 0.25,
|
||||
"max": 4,
|
||||
"step": 0.25
|
||||
},
|
||||
"braking": {
|
||||
"type": "number",
|
||||
"label": "Brake strength",
|
||||
"description": "Multiplies foot and tail braking forces. 1.00x is stock.",
|
||||
"default": 1,
|
||||
"min": 0.25,
|
||||
"max": 4,
|
||||
"step": 0.25
|
||||
},
|
||||
"steering": {
|
||||
"type": "number",
|
||||
"label": "Steering response",
|
||||
"description": "Multiplies the native general steering scalar. 1.00x is stock.",
|
||||
"default": 1,
|
||||
"min": 0.25,
|
||||
"max": 4,
|
||||
"step": 0.25
|
||||
},
|
||||
"wobble": {
|
||||
"type": "number",
|
||||
"label": "Speed wobble",
|
||||
"description": "Multiplies native wobble amplitude. Zero removes wobble. 1.00x is stock.",
|
||||
"default": 1,
|
||||
"min": 0,
|
||||
"max": 2,
|
||||
"step": 0.25
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
local points, previous, next_point, elapsed, running = {}, {}, 1, 0, false
|
||||
local function pressed(key)
|
||||
local down=sdk.input.down(key); local edge=down and not previous[key]; previous[key]=down; return edge
|
||||
end
|
||||
local function clear()
|
||||
sdk.time.cancel('result')
|
||||
for i=1,#points do sdk.scene.remove('gate'..i) end
|
||||
points={}; next_point=1; elapsed=0; running=false
|
||||
end
|
||||
local function status(text) sdk.ui.text('route',text) end
|
||||
local function update(e)
|
||||
local p=sdk.player.read().position
|
||||
if pressed(sdk.settings.record_key) and #points<32 and not running then
|
||||
points[#points+1]={p[1],p[2],p[3]}
|
||||
sdk.scene.cube('gate'..#points,{p[1],p[2]+0.5,p[3]},{0.2,1,0.2},{0.1,0.8,0.6})
|
||||
end
|
||||
if pressed('F8') then clear() end
|
||||
if pressed('F7') and #points>1 then sdk.time.cancel('result'); next_point=1; elapsed=0; running=true end
|
||||
if running then
|
||||
elapsed=elapsed+e.dt
|
||||
local q=points[next_point]
|
||||
local distance=math.sqrt((p[1]-q[1])^2+(p[2]-q[2])^2+(p[3]-q[3])^2)
|
||||
if distance<=sdk.settings.radius then
|
||||
next_point=next_point+1
|
||||
if next_point>#points then
|
||||
running=false
|
||||
status(string.format('Finished! %.2f s',elapsed))
|
||||
sdk.time.after('result',5,function() status('F7: run again | F8: clear route') end)
|
||||
return
|
||||
end
|
||||
end
|
||||
status(string.format('Checkpoint %d/%d | %.2f s',next_point,#points,elapsed))
|
||||
else -- Retain finished result until the one-shot timer replaces it.
|
||||
if elapsed==0 then status(string.format('%d checkpoints | %s add | F7 start | F8 clear',#points,sdk.settings.record_key)) end
|
||||
end
|
||||
end
|
||||
return {
|
||||
on_load=function() status('Record a route with '..sdk.settings.record_key) end,
|
||||
on_update=update,
|
||||
on_event=function(e)
|
||||
if e.name=='world_changed' then clear(); sdk.time.cancel('result'); status('New world: route cleared') end
|
||||
if e.name=='bail_changed' and e.bailing and sdk.settings.reset_on_bail then running=false; status('Bailed. F7 restarts the run.') end
|
||||
end,
|
||||
on_settings=function() previous={} end
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"id":"community.route-trainer","api":1,"name":"Route Trainer","version":"1.0.0",
|
||||
"author":"Skate Rust SDK examples",
|
||||
"description":"Record your own route: F6 adds a visual checkpoint at your feet; F7 starts a timed run; F8 clears. Visit each checkpoint in order. Routes reset on reload and map change. Cubes have no collision.",
|
||||
"entry":"main.lua",
|
||||
"settings":{
|
||||
"radius":{"type":"number","label":"Checkpoint radius","description":"Distance in metres that counts as reaching the next checkpoint.","default":2,"min":0.5,"max":10,"step":0.5},
|
||||
"reset_on_bail":{"type":"boolean","label":"Stop run on bail","description":"Stop timing when the existing physics system enters a bail.","default":true},
|
||||
"record_key":{"type":"choice","label":"Record key","description":"Keyboard shortcut, sampled during Update.","default":"F6","choices":["F6","KeyJ","KeyK"]}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
local function draw()
|
||||
if not sdk.settings.visible then sdk.scene.remove('speed'); return end
|
||||
local v=sdk.player.read().velocity
|
||||
local speed=math.sqrt(v[1]^2+v[2]^2+v[3]^2)
|
||||
local units=sdk.settings.units
|
||||
speed=speed*(units=='km/h' and 3.6 or units=='mph' and 2.236936 or 1)
|
||||
sdk.ui.text('speed',string.format('%s: %.1f %s',sdk.settings.label,speed,units))
|
||||
end
|
||||
return {
|
||||
on_load=function() sdk.log('Speedometer loaded; mutable state starts fresh on reload.'); draw() end,
|
||||
on_update=draw,
|
||||
on_settings=draw,
|
||||
on_event=function(e) if e.name=='world_changed' then draw() end end,
|
||||
on_unload=function() -- Owned text is removed by the host, even if this callback fails.
|
||||
end
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"id": "community.speedometer",
|
||||
"api": 1,
|
||||
"name": "Speedometer",
|
||||
"version": "1.0.0",
|
||||
"author": "Skate Rust SDK examples",
|
||||
"description": "Original speed HUD. Change settings or edit main.lua while enabled to try hot reload. Disabled by default.",
|
||||
"entry": "main.lua",
|
||||
"settings": {
|
||||
"visible": {"type":"boolean","label":"Show HUD","description":"Display the board speed readout.","default":true},
|
||||
"units": {"type":"choice","label":"Units","description":"Speed conversion only; does not change physics.","default":"km/h","choices":["km/h","mph","m/s"]},
|
||||
"label": {"type":"string","label":"Label","description":"Your HUD label. Enter to edit with the keyboard.","default":"Speed"}
|
||||
}
|
||||
}
|
||||
@@ -91,3 +91,15 @@ function sdk.time.cancel(key) end
|
||||
function sdk.animation.info() end
|
||||
---@param path string Package-relative original body-clip JSON, <=4 MiB; host validates supported slots and exact timing.
|
||||
function sdk.animation.replace(path) end
|
||||
|
||||
---@class TrainerTuning All fields default to 1; multipliers of current stock settings.
|
||||
---@field pop? number 0.25..4, native ground pop heights.
|
||||
---@field grind_pop? number 0.25..4, native grind launch height.
|
||||
---@field push_speed? number 0.25..4, push target and maximum pushable speed.
|
||||
---@field push_power? number 0.25..4, native push velocity-change limits.
|
||||
---@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.
|
||||
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