diff --git a/Build-FakieFix.ps1 b/Build-FakieFix.ps1 new file mode 100644 index 00000000..ef66383f --- /dev/null +++ b/Build-FakieFix.ps1 @@ -0,0 +1,56 @@ +param([switch]$StageOnly, [string]$DependencyTargetDirectory = (Join-Path $PSScriptRoot 'target')) +$ErrorActionPreference = 'Stop' +$privateDirectory = Join-Path $PSScriptRoot '.local/fakie-fix' +$buildDirectory = Join-Path $privateDirectory 'build' +$artifact = Join-Path $buildDirectory 'skate3rust-fakie-fix.exe' +$destination = Join-Path $privateDirectory 'skate3-fakie-fix.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-fix ` + -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', 'Wheel or drag scrollbar', 'Drag // to resize', 'hold_fakie', 'Native trainer controls are already owned by another mod')) { + if (-not $text.Contains($required)) { throw "Wrong fakie-fix 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-FIX.bat b/PLAY-FAKIE-FIX.bat new file mode 100644 index 00000000..513cbdb6 --- /dev/null +++ b/PLAY-FAKIE-FIX.bat @@ -0,0 +1,24 @@ +@echo off +setlocal +cd /d "%~dp0" +if not defined SKATE3_ASSETS ( + if exist "%~dp0.local\fakie-fix\assets.path" set /p "SKATE3_ASSETS="<"%~dp0.local\fakie-fix\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-fix\skate3-fakie-fix.exe" ( + echo Build missing. Run Build-FakieFix.ps1 first. + pause + exit /b 1 +) +set "SKATE3_MOD_SETTINGS=%~dp0.local\fakie-fix\settings" +if exist "%~dp0.local\fakie-fix\overlay.path" set /p "SKATE3_SESSION_MARKER_OVERLAY="<"%~dp0.local\fakie-fix\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-fix\mods +echo Trainer log: %~dp0.local\fakie-fix\session.log +"%~dp0.local\fakie-fix\skate3-fakie-fix.exe" --assets "%SKATE3_ASSETS%" --start-paused %* > "%~dp0.local\fakie-fix\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 e1974b9f..35cb910d 100644 --- a/crates/skate-game/src/graph_host/motion.rs +++ b/crates/skate-game/src/graph_host/motion.rs @@ -1,3 +1,5 @@ +#[path = "motion_fakie_hold.rs"] +mod fakie_hold; #[path = "motion_stock_execute.rs"] mod stock_execution; #[path = "motion_air_execute.rs"] @@ -178,6 +180,7 @@ pub struct MotionHost { pub crouching_physical: Option, pub body_tilt_physical: Option, pub hold_fakie: bool, + automatic_fakie_conditions: Vec, pub fakie_physical: Option, /// PhysOutAnimation158/157, used directly by IsRidingGoofy82BA5AA8. pub physical_stance: Option<(bool, bool)>, @@ -300,6 +303,7 @@ impl MotionHost { crouching_physical: None, body_tilt_physical: None, hold_fakie: false, + automatic_fakie_conditions: fakie_hold::bind(&graph.binding), fakie_physical: None, physical_stance: None, flags: Default::default(), @@ -356,6 +360,12 @@ impl MotionHost { } impl ConditionHost for MotionHost { fn condition_activation(&mut self, condition: usize, frame: &Frame) -> u32 { + // Gate only the authored automatic switch transitions. Other fakie + // conditions (tricks, pushes, dismounts) still see the real stance. + if self.remap.conditions.get(condition).is_some_and(|&id| + fakie_hold::blocked(self.hold_fakie, &self.automatic_fakie_conditions, id)) { + return 0; + } let result = self .remap .conditions diff --git a/crates/skate-game/src/graph_host/motion_execute.rs b/crates/skate-game/src/graph_host/motion_execute.rs index ce34078b..eec4d9fc 100644 --- a/crates/skate-game/src/graph_host/motion_execute.rs +++ b/crates/skate-game/src/graph_host/motion_execute.rs @@ -553,9 +553,7 @@ impl MotionHost { .physical .ok_or("SetTurning requires actual PhysOutAnimation and stance outputs")?; let intents = set_turning::Intents { - // 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") }, + fakie_turn: 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/graph_host/motion_fakie_hold.rs b/crates/skate-game/src/graph_host/motion_fakie_hold.rs new file mode 100644 index 00000000..88ddf7aa --- /dev/null +++ b/crates/skate-game/src/graph_host/motion_fakie_hold.rs @@ -0,0 +1,82 @@ +//! Trainer gates for the stock automatic fakie-to-switch animation transitions. +//! FakieTurn is player steering and must never be suppressed by this option. +use skate_data::state_graph::binding::{Binding, Node}; + +pub(super) fn bind(binding: &Binding) -> Vec { + let mut gates = vec![false; binding.operations.len()]; + for transition in &binding.transitions { + let Some(target) = transition.target else { continue; }; + let mut names = Vec::new(); + let mut state = Some(target); + while let Some(id) = state { + names.push(binding.states[id].name.as_str()); + state = binding.states[id].parent; + } + names.reverse(); + if names != ["Motion", "OnBoard", "OnGround", "RidingIdle", "Riding", "Turning", "Switch"] { + continue; + } + if let Some(expression) = transition.expression { + mark(binding, expression, &mut gates); + } + } + gates +} +fn mark(binding: &Binding, expression: usize, gates: &mut [bool]) { + for child in &binding.expressions[expression].children { + match *child { + Node::Operation(id) if binding.operations[id].name == "IsRidingFakie" => gates[id] = true, + Node::Expression(id) => mark(binding, id, gates), + _ => {} + } + } +} +pub(super) fn blocked(enabled: bool, gates: &[bool], operation: usize) -> bool { + enabled && gates.get(operation).copied().unwrap_or(false) +} + +#[cfg(test)] +mod tests { + use super::*; + use skate_data::state_graph::{GraphAttribute, GraphElement, StateGraph}; + struct Tree(&'static str, Vec<(&'static str, &'static str)>, Vec); + fn state(name: &'static str, children: Vec) -> Tree { Tree("state", vec![("name", name)], children) } + fn condition() -> Tree { Tree("condition", vec![("name", "IsRidingFakie")], vec![]) } + fn transition(target: &'static str) -> Tree { + Tree("transition", vec![("target", target)], vec![Tree("expression", vec![("op", "and")], vec![condition()])]) + } + fn add(tree: Tree, graph: &mut StateGraph) -> usize { + let id = graph.elements.len(); + graph.elements.push(GraphElement { source_offset: id, tag: tree.0.into(), + attributes: tree.1.into_iter().map(|(name, text)| GraphAttribute { + name: name.into(), text: text.into(), float_bits: 0, boolean_byte: 0, + }).collect(), children: vec![] }); + let children = tree.2.into_iter().map(|child| add(child, graph)).collect(); + graph.elements[id].children = children; + id + } + #[test] + fn gates_idle_and_bump_switch_but_preserves_other_fakie_conditions() { + let tree = state("Motion", vec![state("OnBoard", vec![state("OnGround", vec![ + state("RidingIdle", vec![ + state("Bumped", vec![transition("Riding.Turning.Switch")]), + state("Riding", vec![state("Turning", vec![ + state("Idle", vec![transition("Switch")]), state("Switch", vec![]), + state("Push", vec![Tree("expression", vec![], vec![condition()])]), + ])]), + ]), + // Same leaf name elsewhere must not be treated as automatic switching. + state("Other", vec![state("Idle", vec![transition("Switch")]), state("Switch", vec![])]), + ])])]); + let mut graph = StateGraph { elements: vec![] }; add(tree, &mut graph); + let binding = Binding::from_graph(&graph).unwrap(); + let gates = bind(&binding); + assert_eq!(gates.iter().filter(|&&gate| gate).count(), 2); + assert_eq!(gates.iter().filter(|&&gate| !gate).count(), 2); + for (id, gate) in gates.iter().enumerate() { + assert!(!blocked(false, &gates, id), "disabled trainer must preserve stock transitions"); + assert_eq!(blocked(true, &gates, id), *gate); + } + assert!(!blocked(true, &gates, usize::MAX)); + } +} diff --git a/docs/lua-modding.md b/docs/lua-modding.md index d104ff97..5ff8cf55 100644 --- a/docs/lua-modding.md +++ b/docs/lua-modding.md @@ -268,7 +268,7 @@ native gameplay effects and visuals remain user-run; no game process was launche Trainer 2.1 adds `sdk.trainer.apply { hold_fakie = true }` (default false). -It suppresses the native `FakieTurn` correction input in SetTurning; normal +It blocks the automatic idle/bump transitions into the native stance-switch animation; normal steering, slide overrides and trick inputs remain native. Turning the setting off, disabling the mod or retiring its owner restores stock behavior. diff --git a/mods/native-trainer/README.md b/mods/native-trainer/README.md index 614c8705..7c0c1057 100644 --- a/mods/native-trainer/README.md +++ b/mods/native-trainer/README.md @@ -47,4 +47,4 @@ 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. +Hold fakie stance disables automatic fakie stance switching; 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/mod.json b/mods/native-trainer/mod.json index 2825a7c6..5d57ed5c 100644 --- a/mods/native-trainer/mod.json +++ b/mods/native-trainer/mod.json @@ -229,7 +229,7 @@ "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.", + "description": "Prevents the automatic idle/bump stance-switch animation. 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 da6d7fd9..7556ed52 100644 --- a/sdk/skate.lua +++ b/sdk/skate.lua @@ -104,7 +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 hold_fakie? boolean Defaults false; suppress automatic fakie stance switching. ---@field manual_drag? number 0.25..4, manual balance linear drag. sdk.trainer = {} ---@param tuning TrainerTuning Owned, reversible native tuning. Conflicting owner is rejected.