From 9fa682a0c4bd6ad7ad42700a758ff48719e3c371 Mon Sep 17 00:00:00 2001 From: chazmm Date: Tue, 8 Sep 2026 19:32:11 +0100 Subject: [PATCH] Add resizable trainer windows and optional fakie turn hold --- Build-FakieTrainer.ps1 | 56 +++++++++ PLAY-FAKIE-TRAINER.bat | 24 ++++ crates/skate-game/src/graph_host/motion.rs | 2 + .../src/graph_host/motion_execute.rs | 4 +- crates/skate-game/src/modding/panel.rs | 119 ++++++++++++++++-- .../skate-game/src/physics/animation_phase.rs | 1 + crates/skate-mods/src/schema.rs | 2 + crates/skate-mods/tests/runtime.rs | 23 ++++ docs/lua-modding.md | 11 ++ mods/native-trainer/README.md | 2 + mods/native-trainer/main.lua | 2 +- mods/native-trainer/mod.json | 8 +- sdk/skate.lua | 3 +- 13 files changed, 242 insertions(+), 15 deletions(-) create mode 100644 Build-FakieTrainer.ps1 create mode 100644 PLAY-FAKIE-TRAINER.bat diff --git a/Build-FakieTrainer.ps1 b/Build-FakieTrainer.ps1 new file mode 100644 index 00000000..2014934f --- /dev/null +++ b/Build-FakieTrainer.ps1 @@ -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 +} diff --git a/PLAY-FAKIE-TRAINER.bat b/PLAY-FAKIE-TRAINER.bat new file mode 100644 index 00000000..3470ea60 --- /dev/null +++ b/PLAY-FAKIE-TRAINER.bat @@ -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 diff --git a/crates/skate-game/src/graph_host/motion.rs b/crates/skate-game/src/graph_host/motion.rs index f908e730..e1974b9f 100644 --- a/crates/skate-game/src/graph_host/motion.rs +++ b/crates/skate-game/src/graph_host/motion.rs @@ -177,6 +177,7 @@ pub struct MotionHost { pub applying_body_tilt: bool, pub crouching_physical: Option, pub body_tilt_physical: Option, + pub hold_fakie: bool, pub fakie_physical: Option, /// 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(), diff --git a/crates/skate-game/src/graph_host/motion_execute.rs b/crates/skate-game/src/graph_host/motion_execute.rs index eec4d9fc..ce34078b 100644 --- a/crates/skate-game/src/graph_host/motion_execute.rs +++ b/crates/skate-game/src/graph_host/motion_execute.rs @@ -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"), }; diff --git a/crates/skate-game/src/modding/panel.rs b/crates/skate-game/src/modding/panel.rs index 1666fb75..6dec36e9 100644 --- a/crates/skate-game/src/modding/panel.rs +++ b/crates/skate-game/src/modding/panel.rs @@ -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, layouts: BTreeMap, drag: Option<(String, Vec2)>, + resize: Option<(String, Vec2)>, signature: Vec<(String, Vec)>, } 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>, nav: Res, window: Single<&Window, With>, + grips: Query<(&Interaction, &ResizeGrip), Changed>, headers: Query<(&Interaction, &Header), Changed>, buttons: Query<(&Interaction, &Action), Changed>, ) { 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), + ) +} diff --git a/crates/skate-game/src/physics/animation_phase.rs b/crates/skate-game/src/physics/animation_phase.rs index 5e794057..fdc22fbe 100644 --- a/crates/skate-game/src/physics/animation_phase.rs +++ b/crates/skate-game/src/physics/animation_phase.rs @@ -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, diff --git a/crates/skate-mods/src/schema.rs b/crates/skate-mods/src/schema.rs index cb7329fe..350a0e0f 100644 --- a/crates/skate-mods/src/schema.rs +++ b/crates/skate-mods/src/schema.rs @@ -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, } } } diff --git a/crates/skate-mods/tests/runtime.rs b/crates/skate-mods/tests/runtime.rs index 570a335e..ad7ec43a 100644 --- a/crates/skate-mods/tests/runtime.rs +++ b/crates/skate-mods/tests/runtime.rs @@ -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)); +} diff --git a/docs/lua-modding.md b/docs/lua-modding.md index b5d51f24..6848b9ae 100644 --- a/docs/lua-modding.md +++ b/docs/lua-modding.md @@ -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. diff --git a/mods/native-trainer/README.md b/mods/native-trainer/README.md index 2e898a9a..614c8705 100644 --- a/mods/native-trainer/README.md +++ b/mods/native-trainer/README.md @@ -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. diff --git a/mods/native-trainer/main.lua b/mods/native-trainer/main.lua index 0847a20b..fe673045 100644 --- a/mods/native-trainer/main.lua +++ b/mods/native-trainer/main.lua @@ -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} diff --git a/mods/native-trainer/mod.json b/mods/native-trainer/mod.json index 5e285d69..2825a7c6 100644 --- a/mods/native-trainer/mod.json +++ b/mods/native-trainer/mod.json @@ -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 } } } diff --git a/sdk/skate.lua b/sdk/skate.lua index 87642eba..da6d7fd9 100644 --- a/sdk/skate.lua +++ b/sdk/skate.lua @@ -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.