Add resizable trainer windows and optional fakie turn hold

This commit is contained in:
chazmm committed 2026-09-08 19:32:11 +01:00
1 parent a2366128f4
commit 9fa682a0c4
13 files changed
+242 -15

No files matched your search

+56
View File
@@ -0,0 +1,56 @@
param([switch]$StageOnly, [string]$DependencyTargetDirectory = (Join-Path $PSScriptRoot 'target'))
$ErrorActionPreference = 'Stop'
$privateDirectory = Join-Path $PSScriptRoot '.local/fakie-trainer'
$buildDirectory = Join-Path $privateDirectory 'build'
$artifact = Join-Path $buildDirectory 'skate3rust-fakie-trainer.exe'
$destination = Join-Path $privateDirectory 'skate3-fakie-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=-fakie-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', 'Scroll wheel: more settings', 'Drag // to resize', 'hold_fakie', 'Native trainer controls are already owned by another mod')) {
if (-not $text.Contains($required)) { throw "Wrong fakie-trainer artifact: missing $required" }
}
Copy-Item -LiteralPath $artifact -Destination $destination -Force
# Only original SDK example sources are staged. Never copy extracted assets.
$modDestination = Join-Path $privateDirectory 'mods/native-trainer'
New-Item -ItemType Directory -Force -Path $modDestination | Out-Null
foreach ($name in @('mod.json','main.lua','help.txt','README.md')) {
$source = Join-Path $PSScriptRoot "mods/native-trainer/$name"
$target = Join-Path $modDestination $name
if (-not (Test-Path -LiteralPath $target)) { Copy-Item -LiteralPath $source -Destination $target }
elseif ((Get-FileHash -LiteralPath $source).Hash -ne (Get-FileHash -LiteralPath $target).Hash) {
Write-Warning "Preserving existing mod file: $target. Copy the updated source there when ready."
}
}
Copy-Item -LiteralPath (Join-Path $PSScriptRoot 'mods/README.md') -Destination (Join-Path $privateDirectory 'mods/README.md') -Force
$hash = (Get-FileHash -LiteralPath $destination -Algorithm SHA256).Hash
if ($hash -ne (Get-FileHash -LiteralPath $artifact -Algorithm SHA256).Hash) {
throw 'Staged executable hash mismatch.'
}
@{ executable = $destination; sha256 = $hash; source = $PSScriptRoot;
revision = (& git rev-parse HEAD); staticChecks = 'CLI, Mods UI, Lua quota diagnostics' } |
ConvertTo-Json | Set-Content -LiteralPath (Join-Path $privateDirectory 'build-manifest.json') -Encoding UTF8
Write-Host "Ready for manual launch: $destination"
Write-Host "SHA256: $hash"
} finally {
$env:CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_RUSTFLAGS = $previousFlags
Pop-Location
}
+24
View File
@@ -0,0 +1,24 @@
@echo off
setlocal
cd /d "%~dp0"
if not defined SKATE3_ASSETS (
if exist "%~dp0.local\fakie-trainer\assets.path" set /p "SKATE3_ASSETS="<"%~dp0.local\fakie-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\fakie-trainer\skate3-fakie-trainer.exe" (
echo Build missing. Run Build-FakieTrainer.ps1 first.
pause
exit /b 1
)
set "SKATE3_MOD_SETTINGS=%~dp0.local\fakie-trainer\settings"
if exist "%~dp0.local\fakie-trainer\overlay.path" set /p "SKATE3_SESSION_MARKER_OVERLAY="<"%~dp0.local\fakie-trainer\overlay.path"
echo Open Mods and ENABLE Native Trainer. Escape shows draggable settings windows. Drag the title to move, drag // to resize, or scroll for more settings.
echo Enable Hold fakie stance in the trainer settings to stop automatic fakie correction.
echo Mods folder: %~dp0.local\fakie-trainer\mods
echo Trainer log: %~dp0.local\fakie-trainer\session.log
"%~dp0.local\fakie-trainer\skate3-fakie-trainer.exe" --assets "%SKATE3_ASSETS%" --start-paused %* > "%~dp0.local\fakie-trainer\session.log" 2>&1
if errorlevel 1 pause
@@ -177,6 +177,7 @@ pub struct MotionHost {
pub applying_body_tilt: bool,
pub crouching_physical: Option<skate_core::animation::crouching::Physical>,
pub body_tilt_physical: Option<skate_core::animation::body_tilt::Physical>,
pub hold_fakie: bool,
pub fakie_physical: Option<skate_core::animation::riding_fakie::Physical>,
/// PhysOutAnimation158/157, used directly by IsRidingGoofy82BA5AA8.
pub physical_stance: Option<(bool, bool)>,
@@ -298,6 +299,7 @@ impl MotionHost {
applying_body_tilt: false,
crouching_physical: None,
body_tilt_physical: None,
hold_fakie: false,
fakie_physical: None,
physical_stance: None,
flags: Default::default(),
@@ -553,7 +553,9 @@ impl MotionHost {
.physical
.ok_or("SetTurning requires actual PhysOutAnimation and stance outputs")?;
let intents = set_turning::Intents {
fakie_turn: self.animation.motion_intent("FakieTurn"),
// Trainer suppresses only the automatic fakie correction.
// Slide branches and ordinary steering retain their native inputs.
fakie_turn: if self.hold_fakie { None } else { self.animation.motion_intent("FakieTurn") },
mode_0_slide: self.animation.motion_intent("LeftSlide"),
mode_1_slide: self.animation.motion_intent("RightSlide"),
};
+108 -11
View File
@@ -7,6 +7,7 @@ use std::collections::BTreeMap;
#[derive(Default)]
struct Layout {
position: Vec2,
size: Vec2,
collapsed: bool,
selected: usize,
status: String,
@@ -18,11 +19,12 @@ pub(crate) struct EnabledPanel {
active: Option<String>,
layouts: BTreeMap<String, Layout>,
drag: Option<(String, Vec2)>,
resize: Option<(String, Vec2)>,
signature: Vec<(String, Vec<String>)>,
}
impl EnabledPanel {
pub fn dragging(&self) -> bool {
self.drag.is_some()
self.drag.is_some() || self.resize.is_some()
}
}
#[derive(Component)]
@@ -34,6 +36,8 @@ struct Viewport(String);
#[derive(Component)]
struct Header(String);
#[derive(Component)]
struct ResizeGrip(String);
#[derive(Component)]
struct Row(String, usize);
#[derive(Component)]
struct Label(String, usize);
@@ -113,6 +117,7 @@ fn sync(
}
panel.signature = signature.clone();
panel.drag = None;
panel.resize = None;
if !signature
.iter()
.any(|(id, _)| panel.active.as_ref() == Some(id))
@@ -122,6 +127,7 @@ fn sync(
}
for (index, (id, settings)) in signature.iter().enumerate() {
panel.layouts.entry(id.clone()).or_insert_with(|| Layout {
size: Vec2::new(360., 760.),
position: Vec2::new(16. + index as f32 * 22., 64. + index as f32 * 34.),
..default()
});
@@ -133,7 +139,8 @@ fn sync(
Node {
display: Display::None,
position_type: PositionType::Absolute,
width: px(320.),
width: px(360.),
height: px(760.),
max_width: percent(95),
padding: UiRect::all(px(10.)),
row_gap: px(8.),
@@ -199,6 +206,8 @@ fn sync(
.spawn((
Body(id.clone()),
Node {
flex_grow: 1.,
min_height: px(0.),
flex_direction: FlexDirection::Column,
row_gap: px(6.),
..default()
@@ -209,8 +218,9 @@ fn sync(
Viewport(id.clone()),
ScrollPosition::default(),
Node {
height: px(280.),
max_height: Val::Vh(45.),
flex_grow: 1.,
flex_basis: px(0.),
min_height: px(0.),
overflow: Overflow::scroll_y(),
flex_direction: FlexDirection::Column,
row_gap: px(6.),
@@ -279,6 +289,30 @@ fn sync(
},
TextColor(Color::srgb(0.65, 0.85, 0.85)),
));
body.spawn((
Button,
ResizeGrip(id.clone()),
Node {
align_self: AlignSelf::FlexEnd,
width: px(28.),
height: px(22.),
flex_shrink: 0.,
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
..default()
},
BackgroundColor(Color::srgb(0.12, 0.20, 0.26)),
))
.with_children(|grip| {
grip.spawn((
Text::new("//"),
TextFont {
font_size: 18.,
..default()
},
TextColor(Color::WHITE),
));
});
});
});
}
@@ -293,12 +327,14 @@ fn input(
mouse: Res<ButtonInput<MouseButton>>,
nav: Res<crate::customiser::Navigation>,
window: Single<&Window, With<bevy::window::PrimaryWindow>>,
grips: Query<(&Interaction, &ResizeGrip), Changed<Interaction>>,
headers: Query<(&Interaction, &Header), Changed<Interaction>>,
buttons: Query<(&Interaction, &Action), Changed<Interaction>>,
) {
if !pause.open || menu.open || custom.open {
panel.focused = false;
panel.drag = None;
panel.resize = None;
return;
}
let ids: Vec<_> = mods
@@ -311,6 +347,7 @@ fn input(
if ids.is_empty() {
panel.focused = false;
panel.drag = None;
panel.resize = None;
return;
}
if keys.just_pressed(KeyCode::Tab) || nav.pressed & 0x4000 != 0 {
@@ -331,25 +368,53 @@ fn input(
}
if !mouse.pressed(MouseButton::Left) {
panel.drag = None;
panel.resize = None;
}
for (interaction, header) in &headers {
if *interaction == Interaction::Pressed && mouse.pressed(MouseButton::Left) {
if let (Some(cursor), Some(layout)) =
(window.cursor_position(), panel.layouts.get(&header.0))
{
panel.drag = Some((header.0.clone(), cursor - layout.position));
panel.drag = Some((header.0.clone(), cursor - fitted_position(layout, &window)));
panel.active = Some(header.0.clone());
panel.focused = true;
}
}
}
for (interaction, grip) in &grips {
if *interaction == Interaction::Pressed && mouse.pressed(MouseButton::Left) {
if let (Some(cursor), Some(layout)) =
(window.cursor_position(), panel.layouts.get(&grip.0))
{
let size = fit_size(layout.size, &window);
let position = fitted_position(layout, &window);
panel.layouts.get_mut(&grip.0).unwrap().position = position;
panel.resize = Some((grip.0.clone(), size - cursor));
panel.drag = None;
panel.active = Some(grip.0.clone());
panel.focused = true;
}
}
}
if let (Some((id, offset)), Some(cursor)) = (panel.resize.clone(), window.cursor_position()) {
if let Some(layout) = panel.layouts.get_mut(&id) {
layout.size = fit_size(cursor + offset, &window)
.min(Vec2::new(window.width(), window.height()) - layout.position);
}
}
if let (Some((id, offset)), Some(cursor)) = (panel.drag.clone(), window.cursor_position()) {
if let Some(layout) = panel.layouts.get_mut(&id) {
layout.position = (cursor - offset).clamp(
Vec2::ZERO,
Vec2::new(
(window.width() - 320.).max(0.),
(window.height() - 48.).max(0.),
(window.width() - fit_size(layout.size, &window).x).max(0.),
(window.height()
- if layout.collapsed {
48.
} else {
fit_size(layout.size, &window).y
})
.max(0.),
),
);
}
@@ -479,8 +544,21 @@ fn draw(
} else {
Display::None
};
node.left = px(layout.position.x.clamp(0., (window.width() - 320.).max(0.)));
node.top = px(layout.position.y.clamp(0., (window.height() - 48.).max(0.)));
let size = fit_size(layout.size, &window);
node.width = px(size.x);
node.height = if layout.collapsed {
Val::Auto
} else {
px(size.y)
};
node.left = px(layout
.position
.x
.clamp(0., (window.width() - size.x).max(0.)));
node.top = px(layout.position.y.clamp(
0.,
(window.height() - if layout.collapsed { 48. } else { size.y }).max(0.),
));
z.0 = if panel.active.as_ref() == Some(&root.0) {
14
} else {
@@ -526,7 +604,7 @@ fn draw(
if let Some(h) = hint {
if let Some(l) = panel.layouts.get(&h.0) {
**t = format!(
"Drag title to move | +/- change values\nScroll wheel: more settings | Tab / X: focus\n{}",
"Drag title to move | Drag // to resize\nScroll wheel: more settings | Tab / X: focus\n{}",
l.status
);
}
@@ -583,7 +661,7 @@ fn scroll(
MouseScrollUnit::Pixel => event.y,
})
.sum();
if !pause.open || menu.open || custom.open || panel.drag.is_some() {
if !pause.open || menu.open || custom.open || panel.dragging() {
return;
}
let hovered = window.physical_cursor_position().and_then(|cursor| {
@@ -629,3 +707,22 @@ fn scroll(
position.0.y = position.0.y.clamp(0., max);
}
}
fn fit_size(size: Vec2, window: &Window) -> Vec2 {
let maximum = Vec2::new(
(window.width() - 16.).max(1.),
(window.height() - 32.).max(1.),
);
size.clamp(Vec2::new(320., 260.).min(maximum), maximum)
}
fn fitted_position(layout: &Layout, window: &Window) -> Vec2 {
let mut size = fit_size(layout.size, window);
if layout.collapsed {
size.y = 48.;
}
layout.position.clamp(
Vec2::ZERO,
(Vec2::new(window.width(), window.height()) - size).max(Vec2::ZERO),
)
}
@@ -247,6 +247,7 @@ pub(crate) fn advance(
surface_up_y: physics.riding.ground.wheel_normal.y,
},
);
skater.animation.motion.hold_fakie = physics.trainer.hold_fakie;
let observations = AnimationPhysical {
conditions,
feedback,
+2
View File
@@ -148,6 +148,7 @@ pub struct TrainerTuning {
pub grip: f32,
pub turn_power: f32,
pub manual_drag: f32,
pub hold_fakie: bool,
}
impl Default for TrainerTuning {
fn default() -> Self {
@@ -163,6 +164,7 @@ impl Default for TrainerTuning {
grip: 1.,
turn_power: 1.,
manual_drag: 1.,
hold_fakie: false,
}
}
}
+23
View File
@@ -383,3 +383,26 @@ fn showcase_reuses_trail_keys_and_updates_hud_settings() {
.any(|(_, c)| matches!(c,Command::Trainer{tuning} if tuning.grip==2.))
);
}
#[test]
fn showcase_hold_fakie_is_optional_live_and_reversible() {
let f = Fixture::new("return {}");
let mut m = showcase_manager(&f);
let id = "community.native-trainer";
assert!(!TrainerTuning::default().hold_fakie);
assert_eq!(m.packages[id].settings["hold_fakie"], json!(false));
for enabled in [true, false] {
m.commands.clear();
m.setting(id, "hold_fakie", json!(enabled)).unwrap();
let tuning = m.commands.iter().find_map(|(_, command)| match command {
Command::Trainer { tuning } => Some(tuning),
_ => None,
}).expect("setting update must emit native trainer tuning");
assert_eq!(tuning.hold_fakie, enabled);
assert_eq!(tuning.pop, 1.);
assert!(tuning.valid());
}
m.setting(id, "hold_fakie", json!(true)).unwrap();
m.enable(id, false).unwrap();
assert!(m.retired.iter().any(|owner| owner == id));
}
+11
View File
@@ -265,3 +265,14 @@ package-relative text loading. No private game assets are shipped.
Standalone Lua tests exercise save/return command payloads, stopwatch creation,
world-change cleanup, bounded trail keys and immediate HUD/tuning settings. Actual
native gameplay effects and visuals remain user-run; no game process was launched.
Trainer 2.1 adds `sdk.trainer.apply { hold_fakie = true }` (default false).
It suppresses the native `FakieTurn` correction input in SetTurning; normal
steering, slide overrides and trick inputs remain native. Turning the setting
off, disabling the mod or retiring its owner restores stock behavior.
Enabled-mod windows default to 360 by 760 pixels, bounded by the display.
Drag the bottom-right `//` grip to resize (minimum 320 by 260); the settings
viewport grows with the window and keeps mouse-wheel scrolling. Window sizes
are retained in memory while the game runs, alongside position/collapse state.
+2
View File
@@ -46,3 +46,5 @@ HUD, visual objects), on_event (map/bail/grind observations), and timer cleanup.
Use this as a practical starting point for training tools, navigation helpers,
telemetry overlays and original visual extensions. The engine's full API reference
is docs/lua-modding.md and language-server annotations are sdk/skate.lua.
Hold fakie stance disables automatic fakie turn correction; normal steering and slides still work. Off by default. Escape settings windows can be resized by dragging the bottom-right // grip.
+1 -1
View File
@@ -1,5 +1,5 @@
-- Native Trainer Showcase: original Lua, no game assets.
local tuning_keys={'pop','grind_pop','push_speed','push_power','braking','steering','wobble','offboard_jump','grip','turn_power','manual_drag'}
local tuning_keys={'pop','grind_pop','push_speed','push_power','braking','steering','wobble','offboard_jump','grip','turn_power','manual_drag','hold_fakie'}
local previous, checkpoint, last_position, last_trail = {}, nil, nil, nil
local trail_index, beacon_index = 0, 0
local stats = {distance=0,peak=0,bails=0,grinds=0,grind_time=0}
+7 -1
View File
@@ -2,7 +2,7 @@
"id": "community.native-trainer",
"api": 1,
"name": "Native Trainer Showcase",
"version": "2.0.0",
"version": "2.1.0",
"author": "Skate Rust Engine",
"description": "Native tuning, telemetry, checkpoint return, stopwatch, breadcrumbs and beacons in one original Lua mod. Manage options here; default keyboard shortcuts F5 save, F6 return, F7 stopwatch, F8 reset, F9 beacon. Stock tuning is 1x.",
"entry": "main.lua",
@@ -225,6 +225,12 @@
"KeyL"
],
"description": "Choose distinct shortcuts. If keys coincide, save, return, timer, reset, beacon run in that order."
},
"hold_fakie": {
"type": "boolean",
"label": "Hold fakie stance",
"description": "Disables automatic FakieTurn correction. Normal steering, slides and tricks remain available. Turn off to restore stock correction.",
"default": false
}
}
}
+2 -1
View File
@@ -93,7 +93,7 @@ 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.
---@class TrainerTuning Numeric 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.
@@ -104,6 +104,7 @@ function sdk.animation.replace(path) end
---@field offboard_jump? number 0.25..4, native biped launch height.
---@field grip? number 0.25..4, wheel static/dynamic and sideways slide friction.
---@field turn_power? number 0.25..4, native heading turn strength.
---@field hold_fakie? boolean Defaults false; suppress automatic fakie turn correction.
---@field manual_drag? number 0.25..4, manual balance linear drag.
sdk.trainer = {}
---@param tuning TrainerTuning Owned, reversible native tuning. Conflicting owner is rejected.