Replace examples with native trainer and expose enabled settings beside pause menu

This commit is contained in:
chazmm committed 2026-09-08 18:46:21 +01:00
1 parent 72152f4b50
commit b809fc0701
30 files changed
+626 -129

No files matched your search

@@ -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,
+1
View File
@@ -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
+6
View File
@@ -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 {
+32 -31
View File
@@ -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
+25
View File
@@ -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)?;
+271
View File
@@ -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
);
}
+2
View File
@@ -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(),
})
}
+3
View File
@@ -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();
+1 -1
View File
@@ -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,
+3
View File
@@ -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
+42
View File
@@ -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)
}
}
+4
View File
@@ -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,
+27 -1
View File
@@ -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()));
}