mirror of
https://github.com/SK8-ENGINE/skate-3-rust-engine.git
synced 2026-10-27 19:54:40 +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
@@ -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()));
|
||||
}
|
||||
Reference in new issue
Block a user