mirror of
https://github.com/SK8-ENGINE/skate-3-rust-engine.git
synced 2026-10-26 19:47:44 +00:00
Import supplied skater project and standalone Windows setup
This commit is contained in:
commit
a47da5bb74
816 files changed
+105573
No files matched your search
@@ -0,0 +1,4 @@
|
||||
/target/
|
||||
/bin/
|
||||
/logs/
|
||||
/assets/private/
|
||||
@@ -0,0 +1,4 @@
|
||||
@echo off
|
||||
cd /d "%~dp0"
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0Build.ps1"
|
||||
if errorlevel 1 pause
|
||||
@@ -0,0 +1,53 @@
|
||||
param([switch]$StageOnly, [string]$TargetDirectory = (Join-Path $PSScriptRoot 'target'))
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Push-Location $PSScriptRoot
|
||||
try {
|
||||
if (-not $StageOnly) {
|
||||
& cargo build -p skate-game --locked --target-dir $TargetDirectory
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Build failed; see the compiler output above.' }
|
||||
}
|
||||
$debugDirectory = Join-Path $TargetDirectory 'debug'
|
||||
$executable = Join-Path $debugDirectory 'skate-game.exe'
|
||||
if (-not (Test-Path -LiteralPath $executable)) { throw "Missing executable: $executable" }
|
||||
$readobj = Join-Path $env:ProgramFiles 'LLVM/bin/llvm-readobj.exe'
|
||||
if (-not (Test-Path -LiteralPath $readobj)) { throw 'LLVM llvm-readobj is required to stage exact runtime DLL dependencies.' }
|
||||
$rustLibraries = (& rustc --print target-libdir).Trim()
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Could not locate Rust runtime libraries.' }
|
||||
$binDirectory = Join-Path $PSScriptRoot 'bin'
|
||||
New-Item -ItemType Directory -Path $binDirectory -Force | Out-Null
|
||||
$queue = [System.Collections.Generic.Queue[string]]::new()
|
||||
$queue.Enqueue($executable)
|
||||
$seen = @{}
|
||||
$staged = @()
|
||||
while ($queue.Count -gt 0) {
|
||||
$source = $queue.Dequeue()
|
||||
$name = Split-Path -Leaf $source
|
||||
if ($seen.ContainsKey($name)) { continue }
|
||||
$seen[$name] = $true
|
||||
$destination = Join-Path $binDirectory $name
|
||||
Copy-Item -LiteralPath $source -Destination $destination -Force
|
||||
$staged += @{name = $name; sha256 = (Get-FileHash -LiteralPath $destination -Algorithm SHA256).Hash}
|
||||
$imports = & $readobj --coff-imports $source
|
||||
if ($LASTEXITCODE -ne 0) { throw "Could not inspect DLL imports: $source" }
|
||||
foreach ($line in $imports) {
|
||||
if ($line -match '^\s+Name: (.+\.dll)$') {
|
||||
$dependency = $Matches[1]
|
||||
$found = $false
|
||||
foreach ($directory in @($debugDirectory, (Join-Path $debugDirectory 'deps'), $rustLibraries)) {
|
||||
$candidate = Join-Path $directory $dependency
|
||||
if (Test-Path -LiteralPath $candidate) {
|
||||
$queue.Enqueue($candidate)
|
||||
$found = $true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (-not $found -and $dependency -notmatch '^(api-ms-|ext-ms-)' -and
|
||||
-not (Test-Path -LiteralPath (Join-Path $env:WINDIR "System32/$dependency"))) {
|
||||
throw "Runtime dependency not found: $dependency"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$staged | ConvertTo-Json | Set-Content -LiteralPath (Join-Path $binDirectory 'manifest.json') -Encoding UTF8
|
||||
Write-Host "Ready: $binDirectory/skate-game.exe"
|
||||
} finally { Pop-Location }
|
||||
Generated
+5937
File diff suppressed because it is too large.
Load diff
+21
@@ -0,0 +1,21 @@
|
||||
[workspace]
|
||||
members = ["crates/skate-core", "crates/skate-data", "crates/skate-game"]
|
||||
resolver = "3"
|
||||
|
||||
[workspace.package]
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
publish = false
|
||||
|
||||
[workspace.dependencies]
|
||||
bevy = { version = "=0.18.1", features = ["dynamic_linking"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
skate-core = { path = "crates/skate-core" }
|
||||
skate-data = { path = "crates/skate-data" }
|
||||
|
||||
[profile.dev]
|
||||
opt-level = 1
|
||||
|
||||
[profile.dev.package."*"]
|
||||
opt-level = 3
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Push-Location $PSScriptRoot
|
||||
try {
|
||||
$executable = Join-Path $PSScriptRoot 'bin/skate-game.exe'
|
||||
if (-not (Test-Path -LiteralPath $executable)) { throw 'Game is not built. Run BUILD.bat first.' }
|
||||
New-Item -ItemType Directory -Path (Join-Path $PSScriptRoot 'logs') -Force | Out-Null
|
||||
$log = Join-Path $PSScriptRoot ('logs/game-' + (Get-Date -Format 'yyyyMMdd-HHmmss') + '.log')
|
||||
Write-Host 'Starting imported skater project (Easy). Use your XInput controller; Esc exits.'
|
||||
Write-Host "Log: $log"
|
||||
$errorLog = [System.IO.Path]::ChangeExtension($log, 'stderr.log')
|
||||
$assetArgument = '"' + (Join-Path $PSScriptRoot 'assets') + '"'
|
||||
$game = Start-Process -FilePath $executable -WorkingDirectory $PSScriptRoot `
|
||||
-ArgumentList @('--assets', $assetArgument) -NoNewWindow -Wait -PassThru `
|
||||
-RedirectStandardOutput $log -RedirectStandardError $errorLog
|
||||
if ($game.ExitCode -ne 0) {
|
||||
Get-Content -LiteralPath $errorLog -Tail 30
|
||||
throw "Game exited with code $($game.ExitCode). Log: $errorLog"
|
||||
}
|
||||
} finally { Pop-Location }
|
||||
@@ -0,0 +1,4 @@
|
||||
@echo off
|
||||
cd /d "%~dp0"
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0Launch.ps1"
|
||||
if errorlevel 1 pause
|
||||
@@ -0,0 +1,45 @@
|
||||
# Imported skater project
|
||||
|
||||
Double-click **PLAY.bat** to launch. Use an XInput-compatible controller;
|
||||
**Esc** exits. Startup selects the supplied **Easy** profile.
|
||||
|
||||
This folder is separate from the earlier clone. It contains the contents of
|
||||
`crates (1).zip` and `assets.zip`, a reconstructed Cargo workspace, and launch
|
||||
scripts. The supplied stock animation banks, decoded frames, graphs, skater
|
||||
mesh and textures are used together. No previous-clone animation code was
|
||||
needed for setup.
|
||||
|
||||
The supplied scene contains a small test environment. Gameplay quality and
|
||||
Skate 3 parity remain for the user to assess; compiling and starting the
|
||||
project do not establish those claims.
|
||||
|
||||
## Observed runtime issue
|
||||
|
||||
The build launches and reports `GAME_CHARACTER_READY bones=35`. All 35 skin
|
||||
bones match the supplied stock rig; the decoded frame data contains 487 clips.
|
||||
However, the imported motion-graph host exits when it encounters
|
||||
`AddRunoutAttribs` (behavior 86). Both startup sessions recorded that exact
|
||||
unsupported-behavior error. This is an implementation gap, not a missing
|
||||
animation-file error. It has not been bypassed or replaced with guessed behavior.
|
||||
See `logs/game-20260906-033940.stderr.log` for the character-ready message
|
||||
and subsequent failure. No automated gameplay or screenshot test was run.
|
||||
|
||||
## Files
|
||||
|
||||
- `crates/`: imported Rust source. The two startup profile selections were
|
||||
changed from `normal` to `easy`.
|
||||
- `assets/private/`: supplied assets; keep these private.
|
||||
- `bin/`: staged executable and its exact Rust/Bevy runtime DLL dependencies.
|
||||
- `logs/`: per-launch output and error logs.
|
||||
- `Cargo.lock`: resolved dependency versions for reproducible rebuilds.
|
||||
- `SETUP-PROVENANCE.json`: original ZIP hashes and setup changes.
|
||||
|
||||
**BUILD.bat** rebuilds and stages the executable. It requires Cargo/Rust and
|
||||
LLVM `llvm-readobj` at the installed Windows location. The initial build
|
||||
reused the earlier project's compilation cache; PLAY.bat uses only this
|
||||
folder's staged binary, libraries and assets. Subsequent default builds use
|
||||
this folder's own `target/` directory.
|
||||
|
||||
If startup fails, the launcher keeps the console open and shows the error
|
||||
log path. Do not substitute animation files to silence a missing-data error;
|
||||
record the exact error first.
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"archives": {
|
||||
"crates (1).zip": "d039af756525a3071178c366bf6d44a8b426f320e28c414cb34449240be89bd9",
|
||||
"assets.zip": "5127c4608742c4c2f574edb128829c6cc22f02cf957b9ade2fae6c56e5b95f7b"
|
||||
},
|
||||
"setup_changes": [
|
||||
"Added missing Cargo workspace using installed Bevy 0.18.1 and Rust 2024 edition.",
|
||||
"Selected supplied Easy profile at both game startup load sites.",
|
||||
"Added build, staging and launch scripts."
|
||||
],
|
||||
"animation_source": "Supplied assets.zip; no assets from the previous clone required.",
|
||||
"validation": {
|
||||
"build": "passed (56 supplied-source warnings)",
|
||||
"startup": "GAME_CHARACTER_READY bones=35",
|
||||
"skin_bones_matched": 35,
|
||||
"decoded_animation_clips": 487,
|
||||
"runtime_issue": "Unsupported MotionGraph Behavior AddRunoutAttribs (behavior 86); no bypass applied",
|
||||
"gameplay_controls": "user-driven; no injected input or automated visual test"
|
||||
},
|
||||
"modified_imported_files": [
|
||||
"crates/skate-game/src/main.rs",
|
||||
"crates/skate-game/src/physics.rs"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
[package]
|
||||
name = "skate-core"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
@@ -0,0 +1,107 @@
|
||||
//! TU3 Reckoning::UpdateBodyFlip 82D8EC70, with explicit attribute-read inputs.
|
||||
//! Full scalar gates, spin matrix and conditional combined-matrix publication.
|
||||
use crate::trigonometry::sin_cos;
|
||||
|
||||
pub type Matrix = [[f32; 4]; 4];
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct BodyFlipState {
|
||||
/// Reckoning +1584, +1588, +1592.
|
||||
pub angle: f32,
|
||||
pub speed: f32,
|
||||
pub requested_speed: f32,
|
||||
/// Reckoning +1072 and +1008. These are distinct native output buffers.
|
||||
pub spin_transform: Matrix,
|
||||
pub combined_transform: Matrix,
|
||||
}
|
||||
|
||||
/// Actual Attrib::GetAttributePointer results (82B72420), not guessed settings.
|
||||
/// Missing collection/attribute resolves to the supplied live 830D0850 value.
|
||||
pub struct BodyFlipSettings {
|
||||
/// Hash 26E7322CCDC3FE23.
|
||||
pub smoothing: Option<f32>,
|
||||
/// Hash 3D95C06DC6E3A002.
|
||||
pub maximum_speed: Option<f32>,
|
||||
/// Hash 89763E47712871C3.
|
||||
pub spin_scale: Option<f32>,
|
||||
pub missing_attribute_value: f32,
|
||||
}
|
||||
|
||||
pub struct BodyFlipInput {
|
||||
pub requested_speed: f32,
|
||||
/// Reckoning +1568, +1152 and +1232; caller-provided axes, without repair.
|
||||
pub spin_angle: f32,
|
||||
pub normal: [f32; 4],
|
||||
pub flip_axis: [f32; 4],
|
||||
/// Processed +2604; only the non-perfect branch uses this dt.
|
||||
pub timestep: f32,
|
||||
/// Processed +2548 -> layout +4 -> byte +28.
|
||||
pub perfect_body_flips: bool,
|
||||
}
|
||||
|
||||
pub fn update(state: &mut BodyFlipState, settings: &BodyFlipSettings, input: &BodyFlipInput) {
|
||||
let fallback = settings.missing_attribute_value;
|
||||
let smoothing = settings.smoothing.unwrap_or(fallback);
|
||||
let maximum = settings.maximum_speed.unwrap_or(fallback);
|
||||
let spin_scale = settings.spin_scale.unwrap_or(fallback);
|
||||
state.requested_speed = input.requested_speed;
|
||||
let mut speed = (input.requested_speed - state.speed).mul_add(smoothing, state.speed);
|
||||
if input.perfect_body_flips {
|
||||
let full_turn = f32::from_bits(0x40C9_0FDB);
|
||||
let remaining_rate = (full_turn - state.angle.abs()) * 12.0;
|
||||
let sign = if speed > 0.0 { 1.0 } else { -1.0 };
|
||||
if !(speed.abs() < remaining_rate) {
|
||||
speed = sign * remaining_rate;
|
||||
}
|
||||
state.angle = clamp(
|
||||
speed.mul_add(f32::from_bits(0x3C88_8889), state.angle),
|
||||
-full_turn,
|
||||
full_turn,
|
||||
);
|
||||
} else {
|
||||
speed = clamp(speed, -maximum, maximum);
|
||||
state.angle = input.timestep.mul_add(speed, state.angle);
|
||||
}
|
||||
state.speed = speed;
|
||||
state.spin_transform = rotation(input.normal, input.spin_angle * spin_scale);
|
||||
// Native zero-angle branch leaves the +1008 matrix untouched.
|
||||
if state.angle != 0.0 {
|
||||
let flip = rotation(input.flip_axis, state.angle);
|
||||
let spin = state.spin_transform;
|
||||
for col in 0..4 {
|
||||
for lane in 0..4 {
|
||||
let first = if col == 3 {
|
||||
spin[col][0].mul_add(flip[0][lane], flip[3][lane])
|
||||
} else {
|
||||
spin[col][0] * flip[0][lane]
|
||||
};
|
||||
let second = spin[col][1].mul_add(flip[1][lane], first);
|
||||
state.combined_transform[col][lane] = spin[col][2].mul_add(flip[2][lane], second);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn clamp(value: f32, lower: f32, upper: f32) -> f32 {
|
||||
let value = if lower - value >= 0.0 { lower } else { value };
|
||||
if upper - value >= 0.0 { value } else { upper }
|
||||
}
|
||||
|
||||
fn rotation(axis: [f32; 4], angle: f32) -> Matrix {
|
||||
let [x, y, z, _] = axis;
|
||||
let (s, c) = sin_cos(angle);
|
||||
let t = 1.0 - c;
|
||||
let (tx, ty, tz) = (t * x, t * y, t * z);
|
||||
let (sx, sy, sz) = (s * x, s * y, s * z);
|
||||
let xx = tx.mul_add(x, c);
|
||||
let yx = ty * x - sz;
|
||||
let zx = tz.mul_add(x, sy);
|
||||
// The native 822FB890 permute repeats column.x into lane w. It does not
|
||||
// create a renderer-style affine matrix with zero w basis lanes.
|
||||
[
|
||||
[xx, tx.mul_add(y, sz), tx * z - sy, xx],
|
||||
[yx, ty.mul_add(y, c), ty.mul_add(z, sx), yx],
|
||||
[zx, tz * y - sx, tz.mul_add(z, c), zx],
|
||||
[0.0; 4],
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
//! Complete TU3 BodySpin::Update (82D8BF58) and history scan (82D8C2D8).
|
||||
//! Cached curves are explicit inputs, not reconstructed from Skate 2 settings.
|
||||
use crate::point_graph::PointGraph;
|
||||
|
||||
/// Native bytes +0..176. Preserves the unused +136 word and mode-byte padding.
|
||||
/// Construction/air-entry lifecycle belongs to the recovered caller, not Default.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct BodySpinState {
|
||||
words: [u32; 44],
|
||||
}
|
||||
|
||||
impl BodySpinState {
|
||||
///82D8DA98..DAE0 initializes every BodySpin scalar and history entry.
|
||||
pub fn new() -> Self {
|
||||
Self { words: [0; 44] }
|
||||
}
|
||||
pub fn from_words(words: [u32; 44]) -> Self {
|
||||
assert!(
|
||||
words[42] < 30,
|
||||
"native derivative history index must be 0..30"
|
||||
);
|
||||
Self { words }
|
||||
}
|
||||
|
||||
pub fn words(&self) -> &[u32; 44] {
|
||||
&self.words
|
||||
}
|
||||
|
||||
/// +140, consumed by Reckoning::UpdateAirStates after BodySpin::Update.
|
||||
pub fn speed(&self) -> f32 {
|
||||
self.get(140)
|
||||
}
|
||||
|
||||
fn get(&self, offset: usize) -> f32 {
|
||||
f32::from_bits(self.words[offset / 4])
|
||||
}
|
||||
|
||||
fn set(&mut self, offset: usize, value: f32) {
|
||||
self.words[offset / 4] = value.to_bits();
|
||||
}
|
||||
}
|
||||
|
||||
/// Native cached inputs at +176/+180, seven 80-byte curves, and 830BD300.
|
||||
/// The vector threshold's producer must be supplied: no guessed retail default.
|
||||
pub struct BodySpinSettings {
|
||||
pub derivative_floor: f32,
|
||||
pub acceleration_limit: f32,
|
||||
/// Native +184, +264, +344, +424, +504, +584, +664; x/y at +16/+48.
|
||||
pub curves: [PointGraph<8>; 7],
|
||||
pub input_fade_threshold: [f32; 4],
|
||||
}
|
||||
|
||||
const STEP: f32 = f32::from_bits(0x3C88_8889); // 820849C8, fixed in these functions.
|
||||
|
||||
/// Registers f1/f2/r6/r7, including the native mode-byte publication.
|
||||
pub fn update(
|
||||
state: &mut BodySpinState,
|
||||
settings: &BodySpinSettings,
|
||||
input: f32,
|
||||
auto_spin: f32,
|
||||
in_air: bool,
|
||||
mode: u8,
|
||||
) {
|
||||
update_input(state, input, auto_spin, mode);
|
||||
if !in_air {
|
||||
finish_ground(state, input);
|
||||
return;
|
||||
}
|
||||
|
||||
update_air(state, settings, input, auto_spin, mode);
|
||||
}
|
||||
|
||||
/// Ground caller82D8C8F0 supplies auto_spin=0, in_air=false and mode=0.
|
||||
/// This branch does not read any of the airborne BodySpin tuning curves.
|
||||
pub fn update_ground(state: &mut BodySpinState, input: f32) {
|
||||
update_input(state, input, 0.0, 0);
|
||||
finish_ground(state, input);
|
||||
}
|
||||
|
||||
fn update_input(state: &mut BodySpinState, input: f32, auto_spin: f32, mode: u8) {
|
||||
let delta = input - state.get(120);
|
||||
let derivative = delta.mul_add(1.5, state.get(124) * 0.0);
|
||||
state.set(164, auto_spin);
|
||||
state.set(120, input);
|
||||
state.words[43] = (state.words[43] & 0x00FF_FFFF) | ((mode as u32) << 24);
|
||||
state.set(124, derivative);
|
||||
let filtered = if derivative * input > 0.1 {
|
||||
derivative
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let filtered = native_clamp(filtered, -1.0, 1.0);
|
||||
state.set(128, filtered);
|
||||
let index = state.words[42] as usize;
|
||||
state.set(index * 4, filtered);
|
||||
state.words[42] = ((index + 1) % 30) as u32;
|
||||
}
|
||||
|
||||
fn finish_ground(state: &mut BodySpinState, input: f32) {
|
||||
state.set(152, 0.0);
|
||||
state.set(144, 0.0);
|
||||
state.set(140, 0.0);
|
||||
state.set(132, state.get(132).mul_add(0.8, input * 0.2));
|
||||
}
|
||||
|
||||
fn update_air(
|
||||
state: &mut BodySpinState,
|
||||
settings: &BodySpinSettings,
|
||||
input: f32,
|
||||
auto_spin: f32,
|
||||
mode: u8,
|
||||
) {
|
||||
let alternate = usize::from(mode == 0);
|
||||
if state.get(152) == 0.0 {
|
||||
calculate_history_derivative(state, &settings.curves[3 + alternate]);
|
||||
}
|
||||
let time = state.get(152);
|
||||
let normal_acceleration = settings.curves[alternate].evaluate(time);
|
||||
let auto_acceleration = settings.curves[2].evaluate(time);
|
||||
let derivative_gain = settings.curves[3 + alternate].evaluate(time);
|
||||
let candidate = derivative_gain * state.get(128);
|
||||
if candidate.abs() > state.get(148).abs() {
|
||||
state.set(148, candidate);
|
||||
state.set(156, time);
|
||||
}
|
||||
let next_time = time + STEP;
|
||||
state.set(152, next_time);
|
||||
// vcmpgtfp128. followed by CR6 all-lanes test, not a scalar dead zone.
|
||||
if settings
|
||||
.input_fade_threshold
|
||||
.iter()
|
||||
.all(|&v| v > state.get(120).abs())
|
||||
{
|
||||
let faded = state.get(132) * 0.96;
|
||||
state.set(132, faded);
|
||||
state.set(120, faded);
|
||||
} else {
|
||||
state.set(132, state.get(132).mul_add(0.8, input * 0.2));
|
||||
}
|
||||
let proportional = settings.curves[5 + alternate].evaluate(next_time) * state.get(120);
|
||||
state.set(144, proportional);
|
||||
if proportional * state.get(148) < 0.0 {
|
||||
state.set(148, 0.0);
|
||||
}
|
||||
let weight = state
|
||||
.get(148)
|
||||
.abs()
|
||||
.mul_add(1.0 - settings.derivative_floor, settings.derivative_floor);
|
||||
let mut target = -(weight * proportional);
|
||||
let mut acceleration = normal_acceleration;
|
||||
let old_speed = state.get(140);
|
||||
if auto_spin.abs() > f32::from_bits(0x3780_0000)
|
||||
&& (old_speed * auto_spin > 0.0 || old_speed.abs() < 0.02)
|
||||
{
|
||||
acceleration = auto_acceleration;
|
||||
target = native_clamp(auto_spin, -2.0, 2.0);
|
||||
}
|
||||
let limited = select(
|
||||
settings.acceleration_limit - acceleration,
|
||||
acceleration,
|
||||
settings.acceleration_limit,
|
||||
);
|
||||
let (lower, upper) = if old_speed > 0.0 {
|
||||
(-limited, acceleration)
|
||||
} else {
|
||||
(-acceleration, limited)
|
||||
};
|
||||
let change = native_clamp(target - old_speed, lower, upper);
|
||||
let speed = old_speed + change;
|
||||
state.set(140, speed);
|
||||
// Both subtractions are present at 82D8C27C..284.
|
||||
state.set(160, (target - speed) - change);
|
||||
}
|
||||
|
||||
/// 82D8C2D8 scans the 29 entries before the current write position.
|
||||
/// The newest entry starts at negative one native step, with strict peak ties.
|
||||
fn calculate_history_derivative(state: &mut BodySpinState, graph: &PointGraph<8>) {
|
||||
let end = state.words[42] as usize;
|
||||
let mut index = (end + 29) % 30;
|
||||
let mut time = 0.0;
|
||||
let mut peak: f32 = 0.0;
|
||||
state.set(148, 0.0);
|
||||
state.set(156, 0.0);
|
||||
while index != end {
|
||||
time -= STEP;
|
||||
let candidate = graph.evaluate(time) * state.get(index * 4);
|
||||
if candidate.abs() > peak.abs() {
|
||||
peak = candidate;
|
||||
state.set(156, time);
|
||||
}
|
||||
index = (index + 29) % 30;
|
||||
}
|
||||
state.set(148, peak);
|
||||
}
|
||||
|
||||
fn select(test: f32, nonnegative: f32, negative: f32) -> f32 {
|
||||
if test >= 0.0 { nonnegative } else { negative }
|
||||
}
|
||||
|
||||
fn native_clamp(value: f32, lower: f32, upper: f32) -> f32 {
|
||||
let value = select(lower - value, lower, value);
|
||||
select(upper - value, value, upper)
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
//! Original arithmetic ordering and two estimate-refinement steps.
|
||||
use crate::physics::{board_motion_output::inverse_length_squared, native_arithmetic};
|
||||
type Vector = [f32; 4];
|
||||
pub(super) const UP: Vector = [0.0, 1.0, 0.0, 0.0];
|
||||
pub(super) fn dot(a: Vector, b: Vector) -> f32 {
|
||||
native_arithmetic::dot3(a, b)
|
||||
}
|
||||
pub(super) fn add(a: Vector, b: Vector) -> Vector {
|
||||
std::array::from_fn(|i| a[i] + b[i])
|
||||
}
|
||||
pub(super) fn sub(a: Vector, b: Vector) -> Vector {
|
||||
std::array::from_fn(|i| a[i] - b[i])
|
||||
}
|
||||
pub(super) fn scale(a: Vector, b: f32) -> Vector {
|
||||
a.map(|v| v * b)
|
||||
}
|
||||
pub(super) fn madd(a: Vector, b: f32, c: Vector) -> Vector {
|
||||
std::array::from_fn(|i| a[i].mul_add(b, c[i]))
|
||||
}
|
||||
pub(super) fn planar(value: Vector, normal: Vector) -> Vector {
|
||||
sub(value, scale(normal, dot(value, normal)))
|
||||
}
|
||||
pub(super) fn maximum(a: f32, b: f32) -> f32 {
|
||||
if a - b >= -0.0 { a } else { b }
|
||||
}
|
||||
pub(super) fn clamp(v: f32, low: f32, high: f32) -> f32 {
|
||||
let lower = if low - v >= -0.0 { low } else { v };
|
||||
if high - lower >= -0.0 { lower } else { high }
|
||||
}
|
||||
pub(super) fn vector_clamp(v: f32, low: f32, high: f32) -> f32 {
|
||||
native_arithmetic::vector_min(high, native_arithmetic::vector_max(low, v))
|
||||
}
|
||||
pub(super) fn reciprocal(v: f32) -> f32 {
|
||||
let mut r = native_arithmetic::reciprocal_estimate(v);
|
||||
for _ in 0..2 {
|
||||
r = r.mul_add((-r).mul_add(v, 1.0), r);
|
||||
}
|
||||
r
|
||||
}
|
||||
pub(super) fn square_root(square: f32) -> f32 {
|
||||
let value = square * inverse_length_squared(square, 2);
|
||||
if square == 0.0 { 0.0 } else { value }
|
||||
}
|
||||
pub(super) fn length(value: Vector) -> f32 {
|
||||
square_root(dot(value, value))
|
||||
}
|
||||
pub(super) fn normalize(value: Vector) -> Vector {
|
||||
let square = dot(value, value);
|
||||
let inverse = inverse_length_squared(square, 2);
|
||||
let length = if square == 0.0 { 0.0 } else { square * inverse };
|
||||
//82F826F8 initializes830BD350 from82181A88 (1e-6), not its IDA BSS zero.
|
||||
if length > f32::from_bits(0x3586_37bd) {
|
||||
scale(value, inverse)
|
||||
} else {
|
||||
[0.0; 4]
|
||||
}
|
||||
}
|
||||
pub(super) fn cross(a: Vector, b: Vector) -> Vector {
|
||||
[
|
||||
(-a[2]).mul_add(b[1], a[1] * b[2]),
|
||||
(-a[0]).mul_add(b[2], a[2] * b[0]),
|
||||
(-a[1]).mul_add(b[0], a[0] * b[1]),
|
||||
0.0,
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
//! GroundAnimation's Toolkit_CalcGroundJump, original TU3 82D93618.
|
||||
//! Stock graph values and live animation/physical inputs determine launch.
|
||||
mod math;
|
||||
mod types;
|
||||
use crate::{
|
||||
air::state::{AirMath, PhysicsAirMath},
|
||||
trigonometry::acos,
|
||||
};
|
||||
use math::*;
|
||||
pub use types::{GroundJump, GroundJumpInput, GroundJumpMode, GroundJumpSettings};
|
||||
|
||||
pub fn calculate(
|
||||
input: GroundJumpInput,
|
||||
mode: GroundJumpMode,
|
||||
s: &GroundJumpSettings,
|
||||
) -> GroundJump {
|
||||
if input.flags_2468 & 0x0040_0000 == 0 {
|
||||
return GroundJump::default();
|
||||
}
|
||||
let normal = input.filtered_ground_normal;
|
||||
let y_scalar = s.y_scalar_vs_normal_y.evaluate(normal[1]);
|
||||
let current = planar(input.current_velocity, normal);
|
||||
let mut prepared = planar(input.prepared_velocity, normal);
|
||||
if dot(sub(current, prepared), sub(current, prepared)) > 7.0 {
|
||||
let agreement = vector_clamp(dot(normalize(current), normalize(prepared)), 0.0, 1.0);
|
||||
let fraction = acos(agreement) * reciprocal(f32::from_bits(0x4049_0fdb));
|
||||
let bound = length(prepared) * s.speed_scalar_vs_angle.evaluate(fraction);
|
||||
prepared = AirMath.clamp_vector_within_max_length(current, bound);
|
||||
}
|
||||
let mut velocity = if input.flags_2488 & 0x1000_0000 != 0 {
|
||||
let mut value = madd(input.effective_forward, 2.3, prepared);
|
||||
value[1] += 1.0;
|
||||
value
|
||||
} else {
|
||||
let minimum = if input.flags_2484 & 0x800 != 0 {
|
||||
mode.minimum_height_64
|
||||
} else {
|
||||
mode.minimum_height_68
|
||||
};
|
||||
let speed_fraction = clamp(input.surface_speed / s.speed_response_max_speed, 0.0, 1.0);
|
||||
let low = s.minimum_height_vs_speed.evaluate(speed_fraction).mul_add(
|
||||
minimum - s.absolute_minimum_height,
|
||||
s.absolute_minimum_height,
|
||||
);
|
||||
let high = s.maximum_height_vs_speed.evaluate(speed_fraction).mul_add(
|
||||
mode.maximum_height - s.absolute_minimum_height,
|
||||
s.absolute_minimum_height,
|
||||
);
|
||||
let height = low.mul_add(1.0 - input.jump_strength, high * input.jump_strength);
|
||||
let current_height = dot(
|
||||
sub(
|
||||
input.animation_com_position,
|
||||
input.ground_reference_position,
|
||||
),
|
||||
input.reference_up,
|
||||
);
|
||||
let remaining = maximum(height - current_height, 0.0);
|
||||
let square = (remaining * input.gravity_y) * -2.0;
|
||||
let speed = square_root(square);
|
||||
let angle = acos(vector_clamp(normal[1], -1.0, 1.0));
|
||||
let fraction = clamp(angle * f32::from_bits(0x3f22_f983), 0.0, 1.0);
|
||||
let response = maximum(s.vertical_response.evaluate(fraction), s.minimum_scalar);
|
||||
madd(normal, speed * response, prepared)
|
||||
};
|
||||
let bonus = clamp((y_scalar - 1.0) * velocity[1], 0.0, s.maximum_y_bonus);
|
||||
velocity[1] += bonus;
|
||||
let projected = dot(velocity, normal);
|
||||
let side = normalize(cross(UP, input.forward));
|
||||
let x = -((input.jump_controls[0] * s.adjust_x_factor) * projected);
|
||||
let z = (input.jump_controls[1] * s.adjust_z_factor) * projected;
|
||||
let correction = madd(side, x, scale(cross(side, UP), z));
|
||||
GroundJump {
|
||||
velocity: add(velocity, correction),
|
||||
scalar_16: 0.0,
|
||||
active: true,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
use crate::point_graph::PointGraph;
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct GroundJump {
|
||||
pub velocity: [f32; 4], //JumpInfo0, metres/second
|
||||
pub scalar_16: f32, //82D93618 initializes and retains zero
|
||||
pub active: bool, //JumpInfo20
|
||||
}
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct GroundJumpMode {
|
||||
pub minimum_height_64: f32, //physics_mode Hash_BE3F74F978D777E5, metres
|
||||
pub minimum_height_68: f32, //JumpMinHeight, metres
|
||||
pub maximum_height: f32, //JumpMaxHeight72, metres
|
||||
}
|
||||
pub struct GroundJumpSettings {
|
||||
pub vertical_response: PointGraph<16>, //physics_jump0
|
||||
pub y_scalar_vs_normal_y: PointGraph<8>, //128
|
||||
pub speed_scalar_vs_angle: PointGraph<8>, //208
|
||||
pub minimum_height_vs_speed: PointGraph<8>, //288
|
||||
pub maximum_height_vs_speed: PointGraph<8>, //352
|
||||
pub speed_response_max_speed: f32, //420, metres/second
|
||||
pub minimum_scalar: f32, //424
|
||||
pub maximum_y_bonus: f32, //428, metres/second
|
||||
pub adjust_z_factor: f32, //432
|
||||
pub adjust_x_factor: f32, //436
|
||||
pub absolute_minimum_height: f32, //456, metres
|
||||
}
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct GroundJumpInput {
|
||||
pub flags_2468: u32,
|
||||
pub flags_2484: u32,
|
||||
pub flags_2488: u32,
|
||||
pub effective_forward: [f32; 4], //Processed160
|
||||
pub forward: [f32; 4], //352
|
||||
pub current_velocity: [f32; 4], //400 metres/second
|
||||
pub ground_reference_position: [f32; 4], //480 metres
|
||||
pub reference_up: [f32; 4], //544
|
||||
pub filtered_ground_normal: [f32; 4], //576, BoardToolkit normal192
|
||||
pub animation_com_position: [f32; 4], //592 metres
|
||||
pub prepared_velocity: [f32; 4], //704 metres/second
|
||||
pub jump_strength: f32, //2624, animation output
|
||||
pub jump_controls: [f32; 2], //2628/2632, animation output
|
||||
pub gravity_y: f32, //2648 metres/second squared
|
||||
pub surface_speed: f32, //2656 metres/second
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
//! TU3 Reckoning::UpdateGroundNormal 82D8E0F8, including filter 82D7A018.
|
||||
use crate::physics::reciprocal_sqrt::estimate;
|
||||
|
||||
/// Native reckoning +1280..1376: control, position, previous input, filtered
|
||||
/// error, output delta and filtered input delta. All four vector lanes persist.
|
||||
/// Initial values must come from the native constructor/entry path.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct GroundNormalFilter {
|
||||
words: [u32; 24],
|
||||
}
|
||||
|
||||
impl GroundNormalFilter {
|
||||
/// TU3 constructor82D8DA68 initializes current/last target to the seed,
|
||||
/// and all three accumulated difference vectors to zero.
|
||||
pub fn initialized(control: [f32; 4], initial: [f32; 4]) -> Self {
|
||||
let mut words = [0; 24];
|
||||
words[..4].copy_from_slice(&control.map(f32::to_bits));
|
||||
words[4..8].copy_from_slice(&initial.map(f32::to_bits));
|
||||
words[8..12].copy_from_slice(&initial.map(f32::to_bits));
|
||||
Self { words }
|
||||
}
|
||||
|
||||
/// The up-vector filters publish the final corrected up vector only after
|
||||
/// both filter updates and acceleration/damping in82D8C8F0.
|
||||
pub fn publish_current(&mut self, current: [f32; 4]) {
|
||||
self.words[4..8].copy_from_slice(¤t.map(f32::to_bits));
|
||||
}
|
||||
|
||||
pub fn filter_raw(&mut self, control: [f32; 4], input: [f32; 4]) -> [f32; 4] {
|
||||
self.words[..4].copy_from_slice(&control.map(f32::to_bits));
|
||||
self.filter(input)
|
||||
}
|
||||
|
||||
pub fn from_words(words: [u32; 24]) -> Self {
|
||||
Self { words }
|
||||
}
|
||||
|
||||
pub fn words(&self) -> &[u32; 24] {
|
||||
&self.words
|
||||
}
|
||||
|
||||
/// Copies live layout+1216 control words before each native filter call.
|
||||
/// Returns the normal published at reckoning+1216 and +1296. There is no
|
||||
/// invented zero-vector fallback; native degenerate arithmetic is preserved.
|
||||
pub fn update(&mut self, control: [f32; 4], input: [f32; 4]) -> [f32; 4] {
|
||||
self.words[..4].copy_from_slice(&control.map(f32::to_bits));
|
||||
let filtered = self.filter(input);
|
||||
let squared = dot3(filtered);
|
||||
let mut inverse = estimate(squared);
|
||||
for _ in 0..2 {
|
||||
let correction = (-squared).mul_add(inverse * inverse, 1.0);
|
||||
inverse = (inverse * 0.5).mul_add(correction, inverse);
|
||||
}
|
||||
let normal = filtered.map(|v| v * inverse);
|
||||
self.words[4..8].copy_from_slice(&normal.map(f32::to_bits));
|
||||
normal
|
||||
}
|
||||
|
||||
// Full vector filter 82D7A018, with publication order before normalization.
|
||||
pub(crate) fn filter(&mut self, input: [f32; 4]) -> [f32; 4] {
|
||||
let previous = self.words.map(f32::from_bits);
|
||||
let blend = previous[3];
|
||||
let retained = 1.0 - blend;
|
||||
let mut output = [0.0; 4];
|
||||
for lane in 0..4 {
|
||||
let error = input[lane] - previous[4 + lane];
|
||||
let error_blend = previous[12 + lane].mul_add(retained, error * blend);
|
||||
let input_delta = input[lane] - previous[8 + lane];
|
||||
let input_blend = previous[20 + lane].mul_add(retained, input_delta * blend);
|
||||
let predicted = error_blend.mul_add(previous[1], previous[4 + lane]);
|
||||
let predicted = error.mul_add(previous[0], predicted);
|
||||
output[lane] = (input_blend - previous[16 + lane]).mul_add(previous[2], predicted);
|
||||
self.words[12 + lane] = error_blend.to_bits();
|
||||
self.words[20 + lane] = input_blend.to_bits();
|
||||
self.words[16 + lane] = (output[lane] - previous[4 + lane]).to_bits();
|
||||
self.words[4 + lane] = output[lane].to_bits();
|
||||
self.words[8 + lane] = input[lane].to_bits();
|
||||
}
|
||||
output
|
||||
}
|
||||
}
|
||||
|
||||
fn dot3(value: [f32; 4]) -> f32 {
|
||||
crate::physics::native_arithmetic::dot3(value, value)
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
//! Body flip and spin targets selected by TU3 KnownAir.
|
||||
|
||||
use super::{
|
||||
data::{
|
||||
KnownAirFrame, KnownAirModeSettings, KnownAirReckoningFields, KnownAirSettings,
|
||||
KnownAirState, Vector4,
|
||||
},
|
||||
runtime::KnownAirRuntime,
|
||||
trajectory::select,
|
||||
};
|
||||
|
||||
const DEGREES_TO_RADIANS: f32 = f32::from_bits(0x3C8E_FA35); // 0x8206D110.
|
||||
const RADIANS_TO_DEGREES: f32 = f32::from_bits(0x4265_2EE1); // 0x82084620.
|
||||
const DIRECTION_EPSILON: f32 = f32::from_bits(0x3A83_126F); // 0x82063A48.
|
||||
const PI: f32 = f32::from_bits(0x4049_0FDB); // 0x82060C44.
|
||||
const ALIGNMENT_DELAY: f32 = f32::from_bits(0x3D03_126F); // 0x822F91E8.
|
||||
const AUTO_SPIN_SCALE: f32 = f32::from_bits(0x3F99_999A); // 0x821659F4.
|
||||
const FORTY_FIVE_DEGREES: f32 = f32::from_bits(0x4234_0000); // 0x82099018.
|
||||
const HALF: f32 = f32::from_bits(0x3F00_0000); // 0x8209975C.
|
||||
const REVERSE_SCALE: f32 = f32::from_bits(0x3BA3_D70A); // 0x82099920.
|
||||
const DECELERATION_SCALE: f32 = f32::from_bits(0x3F40_0000); // 0x821814A0.
|
||||
|
||||
/// `PhysState_KnownAir::CalculateBodyFlipSpeed`, TU3 `0x82D35F40`.
|
||||
pub fn calculate_body_flip_speed(
|
||||
state: &mut KnownAirState,
|
||||
frame: &KnownAirFrame,
|
||||
settings: &KnownAirSettings,
|
||||
mode: &KnownAirModeSettings,
|
||||
reckoning: &KnownAirReckoningFields,
|
||||
runtime: &mut impl KnownAirRuntime,
|
||||
) -> f32 {
|
||||
if !state.body_flipping_211 {
|
||||
let flags = frame.flags_2468;
|
||||
let has_flip = flags & 0x0000_0020 != 0 || flags & 0x0000_0010 != 0;
|
||||
let has_grab = flags & 0x0000_0080 != 0 || flags & 0x0000_0040 != 0;
|
||||
if has_flip && has_grab && frame.flags_2488 & 0x0200_0000 == 0 {
|
||||
let threshold = settings
|
||||
.flip_start_collision_time_vs_normal_y
|
||||
.evaluate(reckoning.landing_normal_1152[1]);
|
||||
if state.collision_time_196 > threshold {
|
||||
runtime.begin_reckoning_body_flip((flags >> 7) & 1 != 0);
|
||||
state.body_flipping_211 = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if mode.perfect_body_flips_28 {
|
||||
return state.body_flip_target_speed_204;
|
||||
}
|
||||
let flags = frame.flags_2468;
|
||||
if flags & 0x0000_0020 != 0
|
||||
|| flags & 0x0000_0010 != 0
|
||||
|| state.time_in_state_180 < settings.body_flip_min_grab_time_fraction_456
|
||||
{
|
||||
state.body_flip_target_speed_204
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
}
|
||||
|
||||
/// `PhysState_KnownAir::CalculateBodySpinSpeed`, TU3 `0x82D360B0`.
|
||||
pub fn calculate_body_spin_speed(
|
||||
state: &KnownAirState,
|
||||
frame: &KnownAirFrame,
|
||||
settings: &KnownAirSettings,
|
||||
mode: &KnownAirModeSettings,
|
||||
reckoning: &KnownAirReckoningFields,
|
||||
runtime: &mut impl KnownAirRuntime,
|
||||
) -> f32 {
|
||||
let scaled_input = frame.body_spin_input_2640 * settings.max_spin_speed_428;
|
||||
let manual_spin = scaled_input * DEGREES_TO_RADIANS;
|
||||
let mut result = if frame.flags_2472 & 0x1000_0000 != 0 {
|
||||
manual_spin
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
// Native tests the unmasked manual value, not `result`.
|
||||
if manual_spin == 0.0 && state.landing_heading_valid_209 && frame.flags_2484 & 0x0000_8000 == 0
|
||||
{
|
||||
let mut heading_axis = reckoning.heading_axis_1200;
|
||||
if ((frame.flags_2468 >> 20) & 1 != 0) != state.start_flipped_210 {
|
||||
heading_axis = flip_sign_bits(heading_axis);
|
||||
}
|
||||
|
||||
let mut unwrapped_angle = 0.0;
|
||||
if runtime.dot3(reckoning.landing_normal_1152, state.landing_normal_64) > DIRECTION_EPSILON
|
||||
{
|
||||
unwrapped_angle = runtime.signed_angle_about_axis(
|
||||
heading_axis,
|
||||
state.landing_heading_80,
|
||||
reckoning.landing_normal_1152,
|
||||
);
|
||||
if frame.flags_2468 & 0x0010_0000 != 0 {
|
||||
unwrapped_angle += PI;
|
||||
}
|
||||
}
|
||||
|
||||
let mut corrected_angle = runtime.wrap_angle_8258db98(unwrapped_angle);
|
||||
let heading_degrees = corrected_angle * RADIANS_TO_DEGREES;
|
||||
let max_adjust = settings
|
||||
.max_heading_adjust_vs_up_y_160
|
||||
.evaluate(frame.skater_up_544[1]);
|
||||
if heading_degrees.abs() > max_adjust {
|
||||
let wrapped_pi = runtime.wrap_angle_8258db98(PI);
|
||||
corrected_angle = runtime.wrap_angle_8258db98(wrapped_pi + corrected_angle);
|
||||
}
|
||||
|
||||
let mut current_speed = reckoning.body_spin_speed_1572;
|
||||
let remaining = state.collision_time_196 - state.time_in_state_180;
|
||||
let alignment_time = select(remaining - ALIGNMENT_DELAY, remaining, ALIGNMENT_DELAY);
|
||||
let reciprocal_time = 1.0 / alignment_time;
|
||||
let mut target = (reciprocal_time * corrected_angle) * AUTO_SPIN_SCALE;
|
||||
|
||||
if current_speed.abs() < DIRECTION_EPSILON {
|
||||
current_speed = 0.0;
|
||||
}
|
||||
if current_speed * target < 0.0 {
|
||||
let alternate = runtime.wrap_angle_8258db98(unwrapped_angle + PI);
|
||||
if FORTY_FIVE_DEGREES * DEGREES_TO_RADIANS > alternate.abs()
|
||||
&& (reciprocal_time * alternate) * current_speed > 0.0
|
||||
{
|
||||
target = reciprocal_time * alternate;
|
||||
}
|
||||
}
|
||||
|
||||
result = native_clamp(
|
||||
target,
|
||||
-mode.body_spin_speed_limit_56,
|
||||
mode.body_spin_speed_limit_56,
|
||||
);
|
||||
if result.abs() < settings.min_auto_body_speed_424 {
|
||||
result *= HALF;
|
||||
}
|
||||
if unwrapped_angle.abs() < settings.min_auto_body_speed_424 {
|
||||
result = (unwrapped_angle / frame.delta_time_2604) * HALF;
|
||||
}
|
||||
if result * current_speed < 0.0 {
|
||||
result *= REVERSE_SCALE;
|
||||
} else if current_speed.abs() > result.abs() {
|
||||
result *= DECELERATION_SCALE;
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn flip_sign_bits(vector: Vector4) -> Vector4 {
|
||||
vector.map(|lane| f32::from_bits(lane.to_bits() ^ 0x8000_0000))
|
||||
}
|
||||
|
||||
fn native_clamp(value: f32, lower: f32, upper: f32) -> f32 {
|
||||
let value = select(lower - value, lower, value);
|
||||
select(upper - value, value, upper)
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
//! Persistent state and direct inputs for TU3 `PhysState_KnownAir`.
|
||||
|
||||
use crate::point_graph::PointGraph;
|
||||
|
||||
pub type Vector4 = [f32; 4];
|
||||
|
||||
/// The 64-byte trajectory copied by `FillPhysOut` at `0x82D36A78`.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct KnownAirTrajectory {
|
||||
pub position: Vector4,
|
||||
pub velocity: Vector4,
|
||||
pub acceleration: Vector4,
|
||||
pub scalar_48: f32,
|
||||
pub word_52: u32,
|
||||
pub word_56: u32,
|
||||
pub word_60: u32,
|
||||
}
|
||||
|
||||
/// Winning trajectory result at selector+1680. The collision frame lies at
|
||||
/// result+128, outside the copied 64-byte trajectory at result+144.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct KnownAirPrediction {
|
||||
pub trajectory: KnownAirTrajectory,
|
||||
/// Winning PredictionResults+48, distinct from request horizon at +192.
|
||||
pub collision_time_48: f32,
|
||||
pub collision_frame_128: i32,
|
||||
}
|
||||
|
||||
/// Fields owned by the KnownAir object at native offsets +64..+216.
|
||||
/// Construction remains caller-owned because Enter does not initialize every
|
||||
/// field before `InitTrajectoryInfo` and the selector helpers run.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct KnownAirState {
|
||||
pub landing_normal_64: Vector4,
|
||||
pub landing_heading_80: Vector4,
|
||||
pub trajectory_apex_96: Vector4,
|
||||
pub collision_position_112: Vector4,
|
||||
pub selector_vector_128: Vector4,
|
||||
pub trajectory_follow_offset_144: Vector4,
|
||||
pub target_com_position_160: Vector4,
|
||||
pub collision_normal_speed_176: f32,
|
||||
pub time_in_state_180: f32,
|
||||
pub start_y_184: f32,
|
||||
pub max_y_188: f32,
|
||||
pub com_max_y_192: f32,
|
||||
pub collision_time_196: f32,
|
||||
pub time_to_apex_200: f32,
|
||||
pub body_flip_target_speed_204: f32,
|
||||
pub reached_apex_208: bool,
|
||||
pub landing_heading_valid_209: bool,
|
||||
pub start_flipped_210: bool,
|
||||
pub body_flipping_211: bool,
|
||||
pub grind_air_adjust_activated_212: bool,
|
||||
pub targeting_grind_213: bool,
|
||||
pub trajectory_index_216: i32,
|
||||
}
|
||||
|
||||
/// Direct ProcessedPhysIn fields consumed by KnownAir.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct KnownAirFrame {
|
||||
pub start_flip_reference_96: Vector4,
|
||||
pub alternate_head_target_112: Vector4,
|
||||
pub velocity_400: Vector4,
|
||||
pub ground_normal_464: Vector4,
|
||||
pub start_height_484: f32,
|
||||
pub skater_up_544: Vector4,
|
||||
pub flags_2468: u32,
|
||||
pub flags_2472: u32,
|
||||
pub flags_2476: u32,
|
||||
pub flags_2480: u32,
|
||||
pub flags_2484: u32,
|
||||
pub flags_2488: u32,
|
||||
pub next_physics_state_2500: i32,
|
||||
pub delta_time_2604: f32,
|
||||
pub forward_speed_2612: f32,
|
||||
pub body_spin_input_2640: f32,
|
||||
pub selector_landing_normal_2656: Vector4,
|
||||
}
|
||||
|
||||
/// Cached `physics_airstates/default.xml` values used by this state. Values
|
||||
/// must come from the stock collection adapter; no retail defaults live here.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct KnownAirSettings {
|
||||
/// Settings +160; x at +176, y at +208.
|
||||
pub max_heading_adjust_vs_up_y_160: PointGraph<8>,
|
||||
/// Settings +240; x at +256, y at +288.
|
||||
pub landing_speed_scalar_vs_ground_normal_y_240: PointGraph<8>,
|
||||
/// Attribute hash `AA79C93673533C5B`; its recovered name remains unknown.
|
||||
pub flip_start_collision_time_vs_normal_y: PointGraph<8>,
|
||||
pub trajectory_error_blend_away_time_384: f32,
|
||||
pub min_target_heading_velocity_420: f32,
|
||||
pub min_auto_body_speed_424: f32,
|
||||
pub max_spin_speed_428: f32,
|
||||
pub frames_for_grind_air_assist_436: f32,
|
||||
pub body_flip_min_grab_time_fraction_456: f32,
|
||||
/// `physics_reckoning/FlipScalar`, hash `978CE73B0EFE76C8`.
|
||||
pub flip_scalar: f32,
|
||||
}
|
||||
|
||||
/// Fields read through ProcessedPhysIn+2548 and its selected physics mode.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct KnownAirModeSettings {
|
||||
pub upside_down_falling_wipeout_enabled_2: bool,
|
||||
pub perfect_body_flips_28: bool,
|
||||
pub body_spin_speed_limit_56: f32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct KnownAirWipeoutSettings {
|
||||
pub air_falling_min_up_y_260: f32,
|
||||
pub air_falling_max_angle_264: f32,
|
||||
}
|
||||
|
||||
/// Direct Reckoning fields read or cleared by KnownAir.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct KnownAirReckoningFields {
|
||||
pub landing_normal_1152: Vector4,
|
||||
pub heading_axis_1200: Vector4,
|
||||
pub body_spin_angle_1568: f32,
|
||||
pub body_spin_speed_1572: f32,
|
||||
}
|
||||
|
||||
/// Direct writes to the wipeout request object at +35, +116, and +200.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct KnownAirWipeoutRequest {
|
||||
pub requested_35: bool,
|
||||
pub scalar_116: f32,
|
||||
pub counter_200: u32,
|
||||
}
|
||||
|
||||
/// Six skeleton bodies disabled, in this exact native order, while the grind
|
||||
/// air adjustment owns the feet.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum GrindFootBody {
|
||||
Body3152,
|
||||
Body3156,
|
||||
Body3160,
|
||||
Body3168,
|
||||
Body3172,
|
||||
Body3176,
|
||||
}
|
||||
|
||||
/// Trajectory packet passed to the three recovered footplant updates.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct KnownAirFootplantInput {
|
||||
pub trajectory: KnownAirTrajectory,
|
||||
pub remaining_collision_time: f32,
|
||||
pub collision_position: Vector4,
|
||||
pub landing_normal: Vector4,
|
||||
}
|
||||
|
||||
/// Native transform at Reckoning+816, interpreted only by the required math
|
||||
/// adapter because Xenon lane order remains part of that contract.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct AffineTransform {
|
||||
pub vectors: [Vector4; 4],
|
||||
}
|
||||
|
||||
/// Stable inputs/outputs around the unresolved VMX geometry in `0x82D35998`.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct RestoreVelocityGeometry {
|
||||
pub tangential_velocity: Vector4,
|
||||
pub landing_speed_curve_input: f32,
|
||||
/// Source clamped to [0, 1] by the two native `fsel` operations.
|
||||
pub landing_speed_blend_source: f32,
|
||||
}
|
||||
|
||||
/// Fields written by `FillPhysOut` (`0x82D36880`). The two conditional fields
|
||||
/// retain their previous value when flags+2468 bit 3 is clear.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct KnownAirOutput {
|
||||
pub trajectory_apex_0: Vector4,
|
||||
pub collision_position_16: Vector4,
|
||||
pub landing_normal_32: Vector4,
|
||||
pub selector_vector_48: Vector4,
|
||||
pub trajectory_position_64: Vector4,
|
||||
pub landing_heading_80: Vector4,
|
||||
pub selector_com_position_96: Vector4,
|
||||
pub landing_normal_copy_144: Vector4,
|
||||
pub locked_trajectory_velocity_160: Vector4,
|
||||
pub time_in_state_176: f32,
|
||||
pub collision_time_180: f32,
|
||||
pub time_until_collision_184: f32,
|
||||
pub collision_normal_speed_188: f32,
|
||||
pub time_to_apex_196: f32,
|
||||
pub jump_height_200: f32,
|
||||
pub trajectory_index_220: i32,
|
||||
pub selected_trajectory_240: KnownAirTrajectory,
|
||||
pub trajectory_plane_samples_336: [f32; 25],
|
||||
pub reached_apex_436: bool,
|
||||
pub known_air_valid_437: bool,
|
||||
pub locked_trajectory_velocity_valid_452: bool,
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
//! Footplant branch embedded in TU3 KnownAir update.
|
||||
|
||||
use super::{
|
||||
data::{KnownAirFootplantInput, KnownAirFrame, KnownAirState},
|
||||
runtime::KnownAirRuntime,
|
||||
};
|
||||
|
||||
const MIN_FOOTPLANT_TIME: f32 = f32::from_bits(0x3DCC_CCCD); // 0x820641A8.
|
||||
|
||||
/// `0x82D36728`, preserving the reset gate and the three update-call order.
|
||||
pub(crate) fn update_footplant(
|
||||
state: &KnownAirState,
|
||||
frame: &KnownAirFrame,
|
||||
runtime: &mut impl KnownAirRuntime,
|
||||
) {
|
||||
if frame.flags_2480 & 0x0400_0000 == 0
|
||||
|| frame.flags_2476 & 0x0400_0000 != 0
|
||||
|| state.time_in_state_180 <= MIN_FOOTPLANT_TIME
|
||||
{
|
||||
runtime.reset_footplants();
|
||||
return;
|
||||
}
|
||||
|
||||
let mut input = KnownAirFootplantInput {
|
||||
trajectory: runtime.selector_trajectory_2704(),
|
||||
remaining_collision_time: state.collision_time_196 - state.time_in_state_180,
|
||||
collision_position: state.collision_position_112,
|
||||
landing_normal: state.landing_normal_64,
|
||||
};
|
||||
runtime.shift_footplant_trajectory_to_index(&mut input.trajectory, state.trajectory_index_216);
|
||||
runtime.update_footplant_prediction(&input);
|
||||
runtime.update_footplant_lock(&input);
|
||||
runtime.update_footplant_pose(&input);
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
//! TU3 KnownAir entry and exit lifecycle.
|
||||
|
||||
use super::{
|
||||
data::{
|
||||
KnownAirFrame, KnownAirModeSettings, KnownAirReckoningFields, KnownAirSettings,
|
||||
KnownAirState,
|
||||
},
|
||||
runtime::KnownAirRuntime,
|
||||
trajectory::{init_trajectory_info, restore_velocity},
|
||||
};
|
||||
|
||||
const ZERO_VECTOR: [f32; 4] = [0.0; 4]; // 0x830BD350.
|
||||
|
||||
/// `PhysState_KnownAir::Enter`, TU3 `0x82D352D0`.
|
||||
pub fn enter(
|
||||
state: &mut KnownAirState,
|
||||
frame: &KnownAirFrame,
|
||||
settings: &KnownAirSettings,
|
||||
mode: &KnownAirModeSettings,
|
||||
runtime: &mut impl KnownAirRuntime,
|
||||
) {
|
||||
runtime.enable_board_angular_drive_only();
|
||||
runtime.set_air_collision_update_enabled(true);
|
||||
runtime.set_skeleton_collision_state(7);
|
||||
runtime.set_skeleton_physics_truck_tilt_enabled(false);
|
||||
runtime.set_skeleton_inverse_kinematics_enabled(true);
|
||||
|
||||
state.body_flipping_211 = false;
|
||||
state.grind_air_adjust_activated_212 = false;
|
||||
state.targeting_grind_213 = false;
|
||||
runtime.reset_reckoning_flipping();
|
||||
runtime.set_footplant_flag_240(false);
|
||||
runtime.reset_footplants();
|
||||
|
||||
state.reached_apex_208 = false;
|
||||
state.time_in_state_180 = 0.0;
|
||||
state.start_y_184 = frame.start_height_484;
|
||||
state.max_y_188 = runtime.board_transform_height();
|
||||
state.com_max_y_192 = runtime.skeleton_com_position()[1];
|
||||
|
||||
init_trajectory_info(state, frame, settings, mode, runtime);
|
||||
|
||||
if runtime.selector_targeting_grind() {
|
||||
runtime.start_grind_air_adjust_from_selector();
|
||||
state.targeting_grind_213 = true;
|
||||
}
|
||||
|
||||
let skeleton_com = runtime.skeleton_com_position();
|
||||
let (index, offset) = runtime.selector_closest_trajectory_point(skeleton_com, ZERO_VECTOR);
|
||||
state.trajectory_index_216 = index;
|
||||
state.trajectory_follow_offset_144 = offset;
|
||||
|
||||
state.start_flipped_210 = 0.0 > runtime.dot3(frame.velocity_400, frame.start_flip_reference_96);
|
||||
}
|
||||
|
||||
/// `PhysState_KnownAir::Exit`, TU3 `0x82D35920`.
|
||||
pub fn exit(
|
||||
state: &mut KnownAirState,
|
||||
frame: &mut KnownAirFrame,
|
||||
settings: &KnownAirSettings,
|
||||
reckoning: &mut KnownAirReckoningFields,
|
||||
runtime: &mut impl KnownAirRuntime,
|
||||
) {
|
||||
runtime.reset_reckoning_flipping();
|
||||
if frame.next_physics_state_2500 == 100 {
|
||||
restore_velocity(state, frame, settings, runtime);
|
||||
}
|
||||
runtime.reset_trajectory_selector();
|
||||
reckoning.body_spin_speed_1572 = 0.0;
|
||||
reckoning.body_spin_angle_1568 = 0.0;
|
||||
runtime.set_grind_air_adjust_started(false);
|
||||
runtime.set_grind_air_adjust_activated(false);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
//! Readable TU3 `PhysState_KnownAir` (state 201).
|
||||
//!
|
||||
//! The routines in this module preserve the observed Skate 3 call, branch,
|
||||
//! field, and constant order. Engine-owned objects and Xenon-specific vector
|
||||
//! operations are mandatory adapter contracts; this module supplies no
|
||||
//! guessed fallbacks.
|
||||
|
||||
mod body;
|
||||
mod data;
|
||||
mod footplant;
|
||||
mod lifecycle;
|
||||
mod output;
|
||||
mod post_physics;
|
||||
mod runtime;
|
||||
mod trajectory;
|
||||
mod update;
|
||||
|
||||
pub use body::{calculate_body_flip_speed, calculate_body_spin_speed};
|
||||
pub use data::{
|
||||
AffineTransform, GrindFootBody, KnownAirFootplantInput, KnownAirFrame, KnownAirModeSettings,
|
||||
KnownAirOutput, KnownAirPrediction, KnownAirReckoningFields, KnownAirSettings, KnownAirState,
|
||||
KnownAirTrajectory, KnownAirWipeoutRequest, KnownAirWipeoutSettings, RestoreVelocityGeometry,
|
||||
Vector4,
|
||||
};
|
||||
pub use lifecycle::{enter, exit};
|
||||
pub use output::fill_physics_output;
|
||||
pub use post_physics::{check_upside_down_falling_wipeout, update_post_physics};
|
||||
pub use runtime::{KnownAirMath, KnownAirRuntime};
|
||||
pub use trajectory::{init_trajectory_info, restore_velocity, update_trajectory_follow};
|
||||
pub use update::update;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -0,0 +1,62 @@
|
||||
//! TU3 KnownAir PhysOut publication.
|
||||
|
||||
use super::{
|
||||
data::{KnownAirFrame, KnownAirOutput, KnownAirState},
|
||||
runtime::KnownAirRuntime,
|
||||
trajectory::{add, sub, trajectory_position, trajectory_velocity},
|
||||
};
|
||||
|
||||
const FIXED_STEP: f32 = f32::from_bits(0x3C88_8889); // 0x820849C8, 1/60.
|
||||
const MAX_FINITE: f32 = f32::from_bits(0x7F7F_FFFF); // 0x8206D108.
|
||||
|
||||
/// `PhysState_KnownAir::FillPhysOut`, TU3 `0x82D36880`.
|
||||
pub fn fill_physics_output(
|
||||
state: &KnownAirState,
|
||||
frame: &KnownAirFrame,
|
||||
output: &mut KnownAirOutput,
|
||||
runtime: &mut impl KnownAirRuntime,
|
||||
) {
|
||||
let prediction = runtime.selected_prediction();
|
||||
if frame.flags_2468 & 0x0000_0008 != 0 {
|
||||
output.locked_trajectory_velocity_valid_452 = true;
|
||||
let time = state.trajectory_index_216 as f32 * FIXED_STEP;
|
||||
output.locked_trajectory_velocity_160 = trajectory_velocity(&prediction.trajectory, time);
|
||||
}
|
||||
|
||||
output.reached_apex_436 = state.reached_apex_208;
|
||||
output.jump_height_200 = state.max_y_188 - state.start_y_184;
|
||||
output.trajectory_apex_0 = state.trajectory_apex_96;
|
||||
output.time_to_apex_196 = state.time_to_apex_200;
|
||||
output.collision_position_16 = state.collision_position_112;
|
||||
output.landing_normal_32 = state.landing_normal_64;
|
||||
output.landing_normal_copy_144 = state.landing_normal_64;
|
||||
output.time_until_collision_184 = if frame.flags_2468 & 0x0000_0008 != 0 {
|
||||
MAX_FINITE
|
||||
} else {
|
||||
state.collision_time_196 - state.time_in_state_180
|
||||
};
|
||||
output.time_in_state_176 = state.time_in_state_180;
|
||||
output.selector_vector_48 = state.selector_vector_128;
|
||||
|
||||
let output_index = state.trajectory_index_216.max(1);
|
||||
output.trajectory_position_64 =
|
||||
trajectory_position(&prediction.trajectory, output_index as f32 * FIXED_STEP);
|
||||
output.landing_heading_80 = state.landing_heading_80;
|
||||
output.collision_normal_speed_188 = state.collision_normal_speed_176;
|
||||
output.known_air_valid_437 = true;
|
||||
output.selected_trajectory_240 = prediction.trajectory;
|
||||
output.trajectory_index_220 = state.trajectory_index_216;
|
||||
|
||||
let selector_com = runtime.selector_com_position_2784();
|
||||
output.selector_com_position_96 = selector_com;
|
||||
output.collision_time_180 = state.collision_time_196;
|
||||
let com_transform = runtime.reckoning_com_transform_816();
|
||||
let local_com = runtime.transform_point_82d36880(com_transform, selector_com);
|
||||
let first_sample = state.trajectory_index_216.wrapping_sub(1).max(0);
|
||||
for (sample_offset, destination) in output.trajectory_plane_samples_336.iter_mut().enumerate() {
|
||||
let sample_index = first_sample.wrapping_add(sample_offset as i32);
|
||||
let sample = trajectory_position(&prediction.trajectory, sample_index as f32 * FIXED_STEP);
|
||||
let relative = sub(add(sample, local_com), state.collision_position_112);
|
||||
*destination = runtime.dot3(relative, state.landing_normal_64);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
//! TU3 KnownAir post-physics and upside-down wipeout handling.
|
||||
|
||||
use super::{
|
||||
data::{
|
||||
KnownAirFrame, KnownAirModeSettings, KnownAirState, KnownAirWipeoutRequest,
|
||||
KnownAirWipeoutSettings,
|
||||
},
|
||||
runtime::KnownAirRuntime,
|
||||
};
|
||||
|
||||
/// `PhysState_KnownAir::UpdatePostPhysics`, TU3 `0x82D36590`.
|
||||
pub fn update_post_physics(
|
||||
state: &mut KnownAirState,
|
||||
frame: &KnownAirFrame,
|
||||
mode: &KnownAirModeSettings,
|
||||
settings: &KnownAirWipeoutSettings,
|
||||
request: &mut KnownAirWipeoutRequest,
|
||||
runtime: &mut impl KnownAirRuntime,
|
||||
) {
|
||||
if !state.reached_apex_208 {
|
||||
let velocity = runtime.board_body_velocity();
|
||||
if velocity[1] < 0.0 {
|
||||
state.reached_apex_208 = true;
|
||||
}
|
||||
}
|
||||
runtime.check_for_air_wipeout(true);
|
||||
check_upside_down_falling_wipeout(state, frame, mode, settings, request, runtime);
|
||||
}
|
||||
|
||||
/// `PhysState_KnownAir::CheckUpsideDownFallingWipeout`, TU3 `0x82D36650`.
|
||||
pub fn check_upside_down_falling_wipeout(
|
||||
state: &KnownAirState,
|
||||
frame: &KnownAirFrame,
|
||||
mode: &KnownAirModeSettings,
|
||||
settings: &KnownAirWipeoutSettings,
|
||||
request: &mut KnownAirWipeoutRequest,
|
||||
runtime: &mut impl KnownAirRuntime,
|
||||
) {
|
||||
if !mode.upside_down_falling_wipeout_enabled_2 {
|
||||
return;
|
||||
}
|
||||
if frame.flags_2468 & 0x0000_0020 != 0 {
|
||||
return;
|
||||
}
|
||||
if !(frame.skater_up_544[1] < settings.air_falling_min_up_y_260) {
|
||||
return;
|
||||
}
|
||||
if runtime.dot3(state.landing_normal_64, frame.skater_up_544)
|
||||
< settings.air_falling_max_angle_264
|
||||
{
|
||||
request.requested_35 = true;
|
||||
request.scalar_116 = 0.0;
|
||||
request.counter_200 = request.counter_200.wrapping_add(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
//! Mandatory engine and Xenon-math boundaries for TU3 KnownAir.
|
||||
|
||||
use super::data::{
|
||||
AffineTransform, GrindFootBody, KnownAirFootplantInput, KnownAirPrediction, KnownAirTrajectory,
|
||||
RestoreVelocityGeometry, Vector4,
|
||||
};
|
||||
|
||||
/// Xenon operations whose exact estimate/refinement, lane, or angle behavior
|
||||
/// is observable in the recovered routines. There is deliberately no default
|
||||
/// implementation or scalar substitute.
|
||||
pub trait KnownAirMath {
|
||||
/// `vmsum3fp128` including its native rounding and lane behavior.
|
||||
fn dot3(&mut self, left: Vector4, right: Vector4) -> f32;
|
||||
/// The `vrefp` reciprocal refinement sequence used for finite differences.
|
||||
fn divide_vector_by_scalar(&mut self, vector: Vector4, scalar: f32) -> Vector4;
|
||||
/// The two `vrsqrtefp128` refinements, zero selection, and returned length.
|
||||
fn normalize_safe_with_length(&mut self, vector: Vector4) -> (Vector4, f32);
|
||||
/// `0x8286CD88`, including its native orientation and degeneracy behavior.
|
||||
fn signed_angle_about_axis(&mut self, from: Vector4, to: Vector4, axis: Vector4) -> f32;
|
||||
/// `0x8258DB98` exactly; callers retain every observed invocation.
|
||||
fn wrap_angle_8258db98(&mut self, angle: f32) -> f32;
|
||||
/// VMX geometry at `0x82D35A0C..0x82D35B78`. The surrounding trajectory
|
||||
/// evaluation, curve evaluation, blend, board write, and speed publication
|
||||
/// remain explicit in readable Rust.
|
||||
fn restore_velocity_geometry_82d35998(
|
||||
&mut self,
|
||||
trajectory_velocity: Vector4,
|
||||
ground_normal: Vector4,
|
||||
) -> RestoreVelocityGeometry;
|
||||
/// Affine point transform at `0x82D36AE4..0x82D36B30`.
|
||||
fn transform_point_82d36880(&mut self, transform: AffineTransform, point: Vector4) -> Vector4;
|
||||
}
|
||||
|
||||
/// Every external call and direct engine-owned object mutation performed by
|
||||
/// the recovered KnownAir routines. Implementers must bind all methods.
|
||||
pub trait KnownAirRuntime: KnownAirMath {
|
||||
fn enable_board_angular_drive_only(&mut self);
|
||||
fn set_air_collision_update_enabled(&mut self, enabled: bool);
|
||||
fn set_skeleton_collision_state(&mut self, state: u32);
|
||||
fn set_skeleton_physics_truck_tilt_enabled(&mut self, enabled: bool);
|
||||
fn set_skeleton_inverse_kinematics_enabled(&mut self, enabled: bool);
|
||||
fn reset_reckoning_flipping(&mut self);
|
||||
fn set_footplant_flag_240(&mut self, value: bool);
|
||||
fn reset_footplants(&mut self);
|
||||
fn board_transform_height(&mut self) -> f32;
|
||||
fn skeleton_com_position(&mut self) -> Vector4;
|
||||
|
||||
fn selector_targeting_grind(&self) -> bool;
|
||||
fn start_grind_air_adjust_from_selector(&mut self);
|
||||
fn selector_closest_trajectory_point(
|
||||
&mut self,
|
||||
skeleton_com: Vector4,
|
||||
zero: Vector4,
|
||||
) -> (i32, Vector4);
|
||||
fn selected_prediction(&self) -> KnownAirPrediction;
|
||||
/// Selector+2704, distinct from the winning result trajectory at +1680.
|
||||
fn selector_trajectory_2704(&self) -> KnownAirTrajectory;
|
||||
fn selector_contact_position(&mut self) -> Vector4;
|
||||
fn selector_vector_2848(&self) -> Vector4;
|
||||
fn selector_com_position_2784(&self) -> Vector4;
|
||||
fn highest_trajectory_position(&mut self, trajectory: &KnownAirTrajectory) -> (Vector4, f32);
|
||||
fn selector_has_just_changed(&self) -> bool;
|
||||
fn reset_trajectory_selector(&mut self);
|
||||
|
||||
fn begin_reckoning_body_flip(&mut self, side: bool);
|
||||
fn update_air_collision(&mut self);
|
||||
fn grind_air_adjust_started(&self) -> bool;
|
||||
fn set_grind_air_adjust_started(&mut self, value: bool);
|
||||
fn set_grind_air_adjust_activated(&mut self, value: bool);
|
||||
fn grind_air_adjust_adjusting(&self) -> bool;
|
||||
fn disable_grind_foot_collision(&mut self, body: GrindFootBody);
|
||||
fn update_reckoning_air_states(
|
||||
&mut self,
|
||||
landing_normal: Vector4,
|
||||
alignment_weight: f32,
|
||||
body_spin_speed: f32,
|
||||
body_flip_speed: f32,
|
||||
);
|
||||
fn update_known_air_skeleton(&mut self, target_com_position: Vector4);
|
||||
fn set_skeleton_add_skateboard_error(&mut self, value: bool);
|
||||
fn set_head_tracking_target(&mut self, target: Vector4, valid: bool);
|
||||
fn update_board_steering_tilt(&mut self, tilt: f32);
|
||||
|
||||
fn shift_footplant_trajectory_to_index(
|
||||
&mut self,
|
||||
trajectory: &mut KnownAirTrajectory,
|
||||
index: i32,
|
||||
);
|
||||
fn update_footplant_prediction(&mut self, input: &KnownAirFootplantInput);
|
||||
fn update_footplant_lock(&mut self, input: &KnownAirFootplantInput);
|
||||
fn update_footplant_pose(&mut self, input: &KnownAirFootplantInput);
|
||||
|
||||
fn board_body_velocity(&mut self) -> Vector4;
|
||||
fn set_board_velocity(&mut self, velocity: Vector4);
|
||||
fn board_forward_axis(&mut self) -> Vector4;
|
||||
fn check_for_air_wipeout(&mut self, use_com: bool);
|
||||
fn reckoning_com_transform_816(&self) -> AffineTransform;
|
||||
}
|
||||
@@ -0,0 +1,376 @@
|
||||
use crate::point_graph::PointGraph;
|
||||
|
||||
use super::super::*;
|
||||
|
||||
pub fn flat_graph(value: f32) -> PointGraph<8> {
|
||||
PointGraph {
|
||||
x: [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0],
|
||||
y: [value; 8],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn trajectory() -> KnownAirTrajectory {
|
||||
KnownAirTrajectory {
|
||||
position: [0.0; 4],
|
||||
velocity: [0.0; 4],
|
||||
acceleration: [0.0; 4],
|
||||
scalar_48: 1.0,
|
||||
word_52: 0,
|
||||
word_56: 0,
|
||||
word_60: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn state() -> KnownAirState {
|
||||
KnownAirState {
|
||||
landing_normal_64: [0.0, 1.0, 0.0, 0.0],
|
||||
landing_heading_80: [1.0, 0.0, 0.0, 0.0],
|
||||
trajectory_apex_96: [0.0; 4],
|
||||
collision_position_112: [0.0; 4],
|
||||
selector_vector_128: [0.0; 4],
|
||||
trajectory_follow_offset_144: [0.0; 4],
|
||||
target_com_position_160: [0.0; 4],
|
||||
collision_normal_speed_176: 0.0,
|
||||
time_in_state_180: 0.0,
|
||||
start_y_184: 0.0,
|
||||
max_y_188: 0.0,
|
||||
com_max_y_192: 0.0,
|
||||
collision_time_196: 1.0,
|
||||
time_to_apex_200: 0.0,
|
||||
body_flip_target_speed_204: 0.0,
|
||||
reached_apex_208: false,
|
||||
landing_heading_valid_209: false,
|
||||
start_flipped_210: false,
|
||||
body_flipping_211: false,
|
||||
grind_air_adjust_activated_212: false,
|
||||
targeting_grind_213: false,
|
||||
trajectory_index_216: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn frame() -> KnownAirFrame {
|
||||
KnownAirFrame {
|
||||
start_flip_reference_96: [0.0; 4],
|
||||
alternate_head_target_112: [0.0; 4],
|
||||
velocity_400: [0.0; 4],
|
||||
ground_normal_464: [0.0, 1.0, 0.0, 0.0],
|
||||
start_height_484: 0.0,
|
||||
skater_up_544: [0.0, 1.0, 0.0, 0.0],
|
||||
flags_2468: 0,
|
||||
flags_2472: 0,
|
||||
flags_2476: 0,
|
||||
flags_2480: 0,
|
||||
flags_2484: 0,
|
||||
flags_2488: 0,
|
||||
next_physics_state_2500: 0,
|
||||
delta_time_2604: 1.0 / 60.0,
|
||||
forward_speed_2612: 0.0,
|
||||
body_spin_input_2640: 0.0,
|
||||
selector_landing_normal_2656: [0.0, 1.0, 0.0, 0.0],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn settings() -> KnownAirSettings {
|
||||
KnownAirSettings {
|
||||
max_heading_adjust_vs_up_y_160: flat_graph(180.0),
|
||||
landing_speed_scalar_vs_ground_normal_y_240: flat_graph(1.0),
|
||||
flip_start_collision_time_vs_normal_y: flat_graph(10.0),
|
||||
trajectory_error_blend_away_time_384: 1.0,
|
||||
min_target_heading_velocity_420: 0.1,
|
||||
min_auto_body_speed_424: 0.01,
|
||||
max_spin_speed_428: 360.0,
|
||||
frames_for_grind_air_assist_436: 4.0,
|
||||
body_flip_min_grab_time_fraction_456: 0.25,
|
||||
flip_scalar: 1.0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mode() -> KnownAirModeSettings {
|
||||
KnownAirModeSettings {
|
||||
upside_down_falling_wipeout_enabled_2: true,
|
||||
perfect_body_flips_28: false,
|
||||
body_spin_speed_limit_56: 20.0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reckoning() -> KnownAirReckoningFields {
|
||||
KnownAirReckoningFields {
|
||||
landing_normal_1152: [0.0, 1.0, 0.0, 0.0],
|
||||
heading_axis_1200: [1.0, 0.0, 0.0, 0.0],
|
||||
body_spin_angle_1568: 0.0,
|
||||
body_spin_speed_1572: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn output() -> KnownAirOutput {
|
||||
KnownAirOutput {
|
||||
trajectory_apex_0: [0.0; 4],
|
||||
collision_position_16: [0.0; 4],
|
||||
landing_normal_32: [0.0; 4],
|
||||
selector_vector_48: [0.0; 4],
|
||||
trajectory_position_64: [0.0; 4],
|
||||
landing_heading_80: [0.0; 4],
|
||||
selector_com_position_96: [0.0; 4],
|
||||
landing_normal_copy_144: [0.0; 4],
|
||||
locked_trajectory_velocity_160: [0.0; 4],
|
||||
time_in_state_176: 0.0,
|
||||
collision_time_180: 0.0,
|
||||
time_until_collision_184: 0.0,
|
||||
collision_normal_speed_188: 0.0,
|
||||
time_to_apex_196: 0.0,
|
||||
jump_height_200: 0.0,
|
||||
trajectory_index_220: 0,
|
||||
selected_trajectory_240: trajectory(),
|
||||
trajectory_plane_samples_336: [0.0; 25],
|
||||
reached_apex_436: false,
|
||||
known_air_valid_437: false,
|
||||
locked_trajectory_velocity_valid_452: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MockRuntime {
|
||||
pub calls: Vec<String>,
|
||||
pub prediction: KnownAirPrediction,
|
||||
pub selector_trajectory: KnownAirTrajectory,
|
||||
pub contact: Vector4,
|
||||
pub selector_vector: Vector4,
|
||||
pub selector_com: Vector4,
|
||||
pub highest: (Vector4, f32),
|
||||
pub board_y: f32,
|
||||
pub com: Vector4,
|
||||
pub targeting_grind: bool,
|
||||
pub closest: (i32, Vector4),
|
||||
pub grind_started: bool,
|
||||
pub grind_adjusting: bool,
|
||||
pub board_velocity: Vector4,
|
||||
pub written_board_velocity: Vector4,
|
||||
pub forward: Vector4,
|
||||
pub signed_angle: f32,
|
||||
pub restore_geometry: RestoreVelocityGeometry,
|
||||
}
|
||||
|
||||
impl MockRuntime {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
calls: Vec::new(),
|
||||
prediction: KnownAirPrediction {
|
||||
trajectory: trajectory(),
|
||||
collision_time_48: 1.0,
|
||||
collision_frame_128: 2,
|
||||
},
|
||||
selector_trajectory: trajectory(),
|
||||
contact: [0.0; 4],
|
||||
selector_vector: [0.0; 4],
|
||||
selector_com: [0.0; 4],
|
||||
highest: ([0.0; 4], 0.0),
|
||||
board_y: 0.0,
|
||||
com: [0.0; 4],
|
||||
targeting_grind: false,
|
||||
closest: (0, [0.0; 4]),
|
||||
grind_started: false,
|
||||
grind_adjusting: false,
|
||||
board_velocity: [0.0; 4],
|
||||
written_board_velocity: [0.0; 4],
|
||||
forward: [1.0, 0.0, 0.0, 0.0],
|
||||
signed_angle: 0.0,
|
||||
restore_geometry: RestoreVelocityGeometry {
|
||||
tangential_velocity: [0.0; 4],
|
||||
landing_speed_curve_input: 0.0,
|
||||
landing_speed_blend_source: 0.0,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn call(&mut self, value: impl Into<String>) {
|
||||
self.calls.push(value.into());
|
||||
}
|
||||
}
|
||||
|
||||
impl KnownAirMath for MockRuntime {
|
||||
fn dot3(&mut self, left: Vector4, right: Vector4) -> f32 {
|
||||
left[0] * right[0] + left[1] * right[1] + left[2] * right[2]
|
||||
}
|
||||
|
||||
fn divide_vector_by_scalar(&mut self, vector: Vector4, scalar: f32) -> Vector4 {
|
||||
vector.map(|value| value / scalar)
|
||||
}
|
||||
|
||||
fn normalize_safe_with_length(&mut self, vector: Vector4) -> (Vector4, f32) {
|
||||
let length = (vector[0] * vector[0] + vector[1] * vector[1] + vector[2] * vector[2]).sqrt();
|
||||
if length == 0.0 {
|
||||
([0.0; 4], 0.0)
|
||||
} else {
|
||||
(vector.map(|value| value / length), length)
|
||||
}
|
||||
}
|
||||
|
||||
fn signed_angle_about_axis(&mut self, _: Vector4, _: Vector4, _: Vector4) -> f32 {
|
||||
self.call("math.signed_angle");
|
||||
self.signed_angle
|
||||
}
|
||||
|
||||
fn wrap_angle_8258db98(&mut self, angle: f32) -> f32 {
|
||||
self.call("math.wrap");
|
||||
angle
|
||||
}
|
||||
|
||||
fn restore_velocity_geometry_82d35998(
|
||||
&mut self,
|
||||
_: Vector4,
|
||||
_: Vector4,
|
||||
) -> RestoreVelocityGeometry {
|
||||
self.call("math.restore_geometry");
|
||||
self.restore_geometry
|
||||
}
|
||||
|
||||
fn transform_point_82d36880(&mut self, _: AffineTransform, point: Vector4) -> Vector4 {
|
||||
point
|
||||
}
|
||||
}
|
||||
|
||||
impl KnownAirRuntime for MockRuntime {
|
||||
fn enable_board_angular_drive_only(&mut self) {
|
||||
self.call("board.angular_only");
|
||||
}
|
||||
fn set_air_collision_update_enabled(&mut self, value: bool) {
|
||||
self.call(format!("collision.enabled:{value}"));
|
||||
}
|
||||
fn set_skeleton_collision_state(&mut self, value: u32) {
|
||||
self.call(format!("collision.state:{value}"));
|
||||
}
|
||||
fn set_skeleton_physics_truck_tilt_enabled(&mut self, value: bool) {
|
||||
self.call(format!("skeleton.truck_tilt:{value}"));
|
||||
}
|
||||
fn set_skeleton_inverse_kinematics_enabled(&mut self, value: bool) {
|
||||
self.call(format!("skeleton.ik:{value}"));
|
||||
}
|
||||
fn reset_reckoning_flipping(&mut self) {
|
||||
self.call("reckoning.reset_flip");
|
||||
}
|
||||
fn set_footplant_flag_240(&mut self, value: bool) {
|
||||
self.call(format!("footplant.flag:{value}"));
|
||||
}
|
||||
fn reset_footplants(&mut self) {
|
||||
self.call("footplant.reset");
|
||||
}
|
||||
fn board_transform_height(&mut self) -> f32 {
|
||||
self.call("board.height");
|
||||
self.board_y
|
||||
}
|
||||
fn skeleton_com_position(&mut self) -> Vector4 {
|
||||
self.call("skeleton.com");
|
||||
self.com
|
||||
}
|
||||
fn selector_targeting_grind(&self) -> bool {
|
||||
self.targeting_grind
|
||||
}
|
||||
fn start_grind_air_adjust_from_selector(&mut self) {
|
||||
self.call("grind.start");
|
||||
}
|
||||
fn selector_closest_trajectory_point(&mut self, _: Vector4, _: Vector4) -> (i32, Vector4) {
|
||||
self.call("selector.closest");
|
||||
self.closest
|
||||
}
|
||||
fn selected_prediction(&self) -> KnownAirPrediction {
|
||||
self.prediction
|
||||
}
|
||||
fn selector_trajectory_2704(&self) -> KnownAirTrajectory {
|
||||
self.selector_trajectory
|
||||
}
|
||||
fn selector_contact_position(&mut self) -> Vector4 {
|
||||
self.call("selector.contact");
|
||||
self.contact
|
||||
}
|
||||
fn selector_vector_2848(&self) -> Vector4 {
|
||||
self.selector_vector
|
||||
}
|
||||
fn selector_com_position_2784(&self) -> Vector4 {
|
||||
self.selector_com
|
||||
}
|
||||
fn highest_trajectory_position(&mut self, _: &KnownAirTrajectory) -> (Vector4, f32) {
|
||||
self.call("trajectory.highest");
|
||||
self.highest
|
||||
}
|
||||
fn selector_has_just_changed(&self) -> bool {
|
||||
false
|
||||
}
|
||||
fn reset_trajectory_selector(&mut self) {
|
||||
self.call("selector.reset");
|
||||
}
|
||||
fn begin_reckoning_body_flip(&mut self, side: bool) {
|
||||
self.call(format!("reckoning.begin_flip:{side}"));
|
||||
}
|
||||
fn update_air_collision(&mut self) {
|
||||
self.call("collision.update");
|
||||
}
|
||||
fn grind_air_adjust_started(&self) -> bool {
|
||||
self.grind_started
|
||||
}
|
||||
fn set_grind_air_adjust_started(&mut self, value: bool) {
|
||||
self.call(format!("grind.started:{value}"));
|
||||
}
|
||||
fn set_grind_air_adjust_activated(&mut self, value: bool) {
|
||||
self.call(format!("grind.activated:{value}"));
|
||||
}
|
||||
fn grind_air_adjust_adjusting(&self) -> bool {
|
||||
self.grind_adjusting
|
||||
}
|
||||
fn disable_grind_foot_collision(&mut self, body: GrindFootBody) {
|
||||
let name = match body {
|
||||
GrindFootBody::Body3152 => "3152",
|
||||
GrindFootBody::Body3156 => "3156",
|
||||
GrindFootBody::Body3160 => "3160",
|
||||
GrindFootBody::Body3168 => "3168",
|
||||
GrindFootBody::Body3172 => "3172",
|
||||
GrindFootBody::Body3176 => "3176",
|
||||
};
|
||||
self.call(format!("grind.disable:{name}"));
|
||||
}
|
||||
fn update_reckoning_air_states(&mut self, _: Vector4, _: f32, _: f32, _: f32) {
|
||||
self.call("reckoning.update_air");
|
||||
}
|
||||
fn update_known_air_skeleton(&mut self, _: Vector4) {
|
||||
self.call("skeleton.update_known_air");
|
||||
}
|
||||
fn set_skeleton_add_skateboard_error(&mut self, value: bool) {
|
||||
self.call(format!("skeleton.board_error:{value}"));
|
||||
}
|
||||
fn set_head_tracking_target(&mut self, _: Vector4, valid: bool) {
|
||||
self.call(format!("head.target:{valid}"));
|
||||
}
|
||||
fn update_board_steering_tilt(&mut self, _: f32) {
|
||||
self.call("board.steering_zero");
|
||||
}
|
||||
fn shift_footplant_trajectory_to_index(&mut self, _: &mut KnownAirTrajectory, _: i32) {
|
||||
self.call("footplant.shift");
|
||||
}
|
||||
fn update_footplant_prediction(&mut self, _: &KnownAirFootplantInput) {
|
||||
self.call("footplant.prediction");
|
||||
}
|
||||
fn update_footplant_lock(&mut self, _: &KnownAirFootplantInput) {
|
||||
self.call("footplant.lock");
|
||||
}
|
||||
fn update_footplant_pose(&mut self, _: &KnownAirFootplantInput) {
|
||||
self.call("footplant.pose");
|
||||
}
|
||||
fn board_body_velocity(&mut self) -> Vector4 {
|
||||
self.call("board.velocity");
|
||||
self.board_velocity
|
||||
}
|
||||
fn set_board_velocity(&mut self, value: Vector4) {
|
||||
self.call("board.set_velocity");
|
||||
self.written_board_velocity = value;
|
||||
}
|
||||
fn board_forward_axis(&mut self) -> Vector4 {
|
||||
self.call("board.forward");
|
||||
self.forward
|
||||
}
|
||||
fn check_for_air_wipeout(&mut self, value: bool) {
|
||||
self.call(format!("wipeout.check:{value}"));
|
||||
}
|
||||
fn reckoning_com_transform_816(&self) -> AffineTransform {
|
||||
AffineTransform {
|
||||
vectors: [[0.0; 4]; 4],
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,404 @@
|
||||
use super::*;
|
||||
|
||||
mod mock;
|
||||
|
||||
use mock::{MockRuntime, frame, mode, output, reckoning, settings, state};
|
||||
|
||||
fn assert_close(actual: f32, expected: f32) {
|
||||
assert!(
|
||||
(actual - expected).abs() < 0.000_01,
|
||||
"{actual} != {expected}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn init_trajectory_info_builds_stock_landing_values() {
|
||||
let mut state = state();
|
||||
let mut frame = frame();
|
||||
frame.selector_landing_normal_2656 = [0.0, 1.0, 0.0, 0.0];
|
||||
let mut settings = settings();
|
||||
settings.flip_scalar = 0.5;
|
||||
settings.min_target_heading_velocity_420 = 0.5;
|
||||
let mode = mode();
|
||||
let mut runtime = MockRuntime::new();
|
||||
runtime.prediction.trajectory.velocity = [3.0, 4.0, 0.0, 0.0];
|
||||
runtime.prediction.trajectory.scalar_48 = 9.0; // Independent query horizon.
|
||||
runtime.prediction.collision_time_48 = 2.0;
|
||||
runtime.prediction.collision_frame_128 = 3;
|
||||
runtime.contact = [9.0, 8.0, 7.0, 0.0];
|
||||
runtime.selector_vector = [6.0, 5.0, 4.0, 0.0];
|
||||
runtime.highest = ([1.0, 12.0, 3.0, 0.0], 0.75);
|
||||
|
||||
init_trajectory_info(&mut state, &frame, &settings, &mode, &mut runtime);
|
||||
|
||||
assert_eq!(state.trajectory_apex_96, runtime.highest.0);
|
||||
assert_eq!(state.time_to_apex_200, 0.75);
|
||||
assert_eq!(state.collision_position_112, runtime.contact);
|
||||
assert_eq!(state.selector_vector_128, runtime.selector_vector);
|
||||
assert_close(state.collision_normal_speed_176, 4.0);
|
||||
assert_eq!(state.landing_heading_80, [1.0, 0.0, 0.0, 0.0]);
|
||||
assert!(state.landing_heading_valid_209);
|
||||
assert_close(
|
||||
state.body_flip_target_speed_204,
|
||||
-core::f32::consts::FRAC_PI_2,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn init_negative_collision_clears_speed_and_heading_validity() {
|
||||
let mut state = state();
|
||||
state.collision_normal_speed_176 = 99.0;
|
||||
state.landing_heading_valid_209 = true;
|
||||
let frame = frame();
|
||||
let settings = settings();
|
||||
let mode = mode();
|
||||
let mut runtime = MockRuntime::new();
|
||||
runtime.prediction.collision_time_48 = -1.0;
|
||||
|
||||
init_trajectory_info(&mut state, &frame, &settings, &mode, &mut runtime);
|
||||
|
||||
assert_eq!(state.collision_normal_speed_176, 0.0);
|
||||
assert_eq!(state.landing_heading_80, [0.0; 4]);
|
||||
assert!(!state.landing_heading_valid_209);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enter_preserves_recovered_effect_order_and_initializes_heights() {
|
||||
let mut state = state();
|
||||
let mut frame = frame();
|
||||
frame.start_height_484 = 3.0;
|
||||
frame.velocity_400 = [-1.0, 0.0, 0.0, 0.0];
|
||||
frame.start_flip_reference_96 = [1.0, 0.0, 0.0, 0.0];
|
||||
let settings = settings();
|
||||
let mode = mode();
|
||||
let mut runtime = MockRuntime::new();
|
||||
runtime.board_y = 4.0;
|
||||
runtime.com = [0.0, 5.0, 0.0, 0.0];
|
||||
runtime.targeting_grind = true;
|
||||
runtime.closest = (11, [0.25, 0.0, 0.0, 0.0]);
|
||||
|
||||
enter(&mut state, &frame, &settings, &mode, &mut runtime);
|
||||
|
||||
assert_eq!(state.start_y_184, 3.0);
|
||||
assert_eq!(state.max_y_188, 4.0);
|
||||
assert_eq!(state.com_max_y_192, 5.0);
|
||||
assert_eq!(state.trajectory_index_216, 11);
|
||||
assert_eq!(state.trajectory_follow_offset_144, runtime.closest.1);
|
||||
assert!(state.targeting_grind_213);
|
||||
assert!(state.start_flipped_210);
|
||||
assert_eq!(
|
||||
&runtime.calls[..9],
|
||||
[
|
||||
"board.angular_only",
|
||||
"collision.enabled:true",
|
||||
"collision.state:7",
|
||||
"skeleton.truck_tilt:false",
|
||||
"skeleton.ik:true",
|
||||
"reckoning.reset_flip",
|
||||
"footplant.flag:false",
|
||||
"footplant.reset",
|
||||
"board.height",
|
||||
]
|
||||
);
|
||||
assert!(runtime.calls.iter().any(|call| call == "grind.start"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exit_restores_velocity_before_selector_and_grind_cleanup() {
|
||||
let mut state = state();
|
||||
state.trajectory_index_216 = 0;
|
||||
let mut frame = frame();
|
||||
frame.next_physics_state_2500 = 100;
|
||||
frame.ground_normal_464 = [0.0, 1.0, 0.0, 0.0];
|
||||
let mut settings = settings();
|
||||
settings.landing_speed_scalar_vs_ground_normal_y_240 = mock::flat_graph(2.0);
|
||||
let mut reckoning = reckoning();
|
||||
reckoning.body_spin_angle_1568 = 2.0;
|
||||
reckoning.body_spin_speed_1572 = 3.0;
|
||||
let mut runtime = MockRuntime::new();
|
||||
runtime.restore_geometry = RestoreVelocityGeometry {
|
||||
tangential_velocity: [2.0, 0.0, 0.0, 0.0],
|
||||
landing_speed_curve_input: 0.0,
|
||||
landing_speed_blend_source: 0.5,
|
||||
};
|
||||
runtime.board_velocity = [0.0, 1.0, 0.0, 0.0];
|
||||
runtime.forward = [1.0, 0.0, 0.0, 0.0];
|
||||
|
||||
exit(
|
||||
&mut state,
|
||||
&mut frame,
|
||||
&settings,
|
||||
&mut reckoning,
|
||||
&mut runtime,
|
||||
);
|
||||
|
||||
assert_eq!(state.trajectory_index_216, 1);
|
||||
assert_eq!(runtime.written_board_velocity, [3.0, 1.0, 0.0, 0.0]);
|
||||
assert_eq!(frame.forward_speed_2612, 3.0);
|
||||
assert_eq!(reckoning.body_spin_speed_1572, 0.0);
|
||||
assert_eq!(reckoning.body_spin_angle_1568, 0.0);
|
||||
let reset = runtime
|
||||
.calls
|
||||
.iter()
|
||||
.position(|v| v == "reckoning.reset_flip")
|
||||
.unwrap();
|
||||
let write = runtime
|
||||
.calls
|
||||
.iter()
|
||||
.position(|v| v == "board.set_velocity")
|
||||
.unwrap();
|
||||
let selector = runtime
|
||||
.calls
|
||||
.iter()
|
||||
.position(|v| v == "selector.reset")
|
||||
.unwrap();
|
||||
let grind = runtime
|
||||
.calls
|
||||
.iter()
|
||||
.position(|v| v == "grind.started:false")
|
||||
.unwrap();
|
||||
assert!(reset < write && write < selector && selector < grind);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_runs_footplant_grind_and_publication_in_native_order() {
|
||||
let mut state = state();
|
||||
state.time_in_state_180 = 0.2;
|
||||
state.collision_time_196 = 0.25;
|
||||
state.reached_apex_208 = true;
|
||||
state.targeting_grind_213 = true;
|
||||
state.trajectory_index_216 = 4;
|
||||
let mut frame = frame();
|
||||
frame.flags_2480 = 0x0400_0000;
|
||||
frame.delta_time_2604 = 1.0 / 60.0;
|
||||
let settings = settings();
|
||||
let mode = mode();
|
||||
let reckoning = reckoning();
|
||||
let mut runtime = MockRuntime::new();
|
||||
runtime.grind_started = true;
|
||||
runtime.grind_adjusting = true;
|
||||
runtime.board_y = 7.0;
|
||||
runtime.com = [0.0, 8.0, 0.0, 0.0];
|
||||
|
||||
update(
|
||||
&mut state,
|
||||
&frame,
|
||||
&settings,
|
||||
&mode,
|
||||
&reckoning,
|
||||
&mut runtime,
|
||||
);
|
||||
|
||||
assert!(state.grind_air_adjust_activated_212);
|
||||
assert_eq!(state.trajectory_index_216, 5);
|
||||
assert_close(state.time_in_state_180, 0.2 + frame.delta_time_2604);
|
||||
assert_eq!(state.max_y_188, 7.0);
|
||||
assert_eq!(state.com_max_y_192, 8.0);
|
||||
let expected_bodies = ["3152", "3156", "3160", "3168", "3172", "3176"];
|
||||
let actual_bodies: Vec<_> = runtime
|
||||
.calls
|
||||
.iter()
|
||||
.filter_map(|call| call.strip_prefix("grind.disable:"))
|
||||
.collect();
|
||||
assert_eq!(actual_bodies, expected_bodies);
|
||||
let foot = runtime
|
||||
.calls
|
||||
.iter()
|
||||
.position(|v| v == "footplant.shift")
|
||||
.unwrap();
|
||||
let collision = runtime
|
||||
.calls
|
||||
.iter()
|
||||
.position(|v| v == "collision.update")
|
||||
.unwrap();
|
||||
let reckon = runtime
|
||||
.calls
|
||||
.iter()
|
||||
.position(|v| v == "reckoning.update_air")
|
||||
.unwrap();
|
||||
let skeleton = runtime
|
||||
.calls
|
||||
.iter()
|
||||
.position(|v| v == "skeleton.update_known_air")
|
||||
.unwrap();
|
||||
let head = runtime
|
||||
.calls
|
||||
.iter()
|
||||
.position(|v| v == "head.target:true")
|
||||
.unwrap();
|
||||
let steering = runtime
|
||||
.calls
|
||||
.iter()
|
||||
.position(|v| v == "board.steering_zero")
|
||||
.unwrap();
|
||||
assert!(
|
||||
foot < collision
|
||||
&& collision < reckon
|
||||
&& reckon < skeleton
|
||||
&& skeleton < head
|
||||
&& head < steering
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trajectory_follow_uses_selector_2704_and_honors_locked_index() {
|
||||
let mut state = state();
|
||||
state.trajectory_index_216 = 0;
|
||||
state.time_in_state_180 = 2.0;
|
||||
let mut frame = frame();
|
||||
frame.flags_2468 = 0x0000_0008;
|
||||
let settings = settings();
|
||||
let mut runtime = MockRuntime::new();
|
||||
runtime.prediction.trajectory.position = [100.0, 0.0, 0.0, 0.0];
|
||||
runtime.selector_trajectory.position = [10.0, 0.0, 0.0, 0.0];
|
||||
|
||||
update_trajectory_follow(&mut state, &frame, &settings, &runtime);
|
||||
|
||||
assert_eq!(state.trajectory_index_216, 0);
|
||||
assert_eq!(state.target_com_position_160, [10.0, 0.0, 0.0, 0.0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auto_spin_uses_recovered_alignment_chain_and_manual_gate() {
|
||||
let mut state = state();
|
||||
state.landing_heading_valid_209 = true;
|
||||
state.landing_normal_64 = [0.0, 1.0, 0.0, 0.0];
|
||||
state.landing_heading_80 = [1.0, 0.0, 0.0, 0.0];
|
||||
state.collision_time_196 = 1.0;
|
||||
let mut frame = frame();
|
||||
frame.delta_time_2604 = 1.0 / 60.0;
|
||||
frame.skater_up_544 = [0.0, 1.0, 0.0, 0.0];
|
||||
let mut settings = settings();
|
||||
settings.max_heading_adjust_vs_up_y_160 = mock::flat_graph(180.0);
|
||||
settings.min_auto_body_speed_424 = 0.01;
|
||||
let mode = mode();
|
||||
let reckoning = reckoning();
|
||||
let mut runtime = MockRuntime::new();
|
||||
runtime.signed_angle = 0.5;
|
||||
|
||||
let automatic =
|
||||
calculate_body_spin_speed(&state, &frame, &settings, &mode, &reckoning, &mut runtime);
|
||||
assert_close(automatic, 0.6);
|
||||
|
||||
frame.body_spin_input_2640 = 1.0;
|
||||
frame.flags_2472 = 0;
|
||||
runtime.calls.clear();
|
||||
let masked_manual =
|
||||
calculate_body_spin_speed(&state, &frame, &settings, &mode, &reckoning, &mut runtime);
|
||||
assert_eq!(masked_manual, 0.0);
|
||||
assert!(!runtime.calls.iter().any(|v| v == "math.signed_angle"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn body_flip_starts_only_after_stock_flag_and_curve_gates() {
|
||||
let mut state = state();
|
||||
state.collision_time_196 = 2.0;
|
||||
state.body_flip_target_speed_204 = -3.0;
|
||||
let mut frame = frame();
|
||||
frame.flags_2468 = 0x20 | 0x80;
|
||||
let mut settings = settings();
|
||||
settings.flip_start_collision_time_vs_normal_y = mock::flat_graph(1.0);
|
||||
let mode = mode();
|
||||
let reckoning = reckoning();
|
||||
let mut runtime = MockRuntime::new();
|
||||
|
||||
let speed = calculate_body_flip_speed(
|
||||
&mut state,
|
||||
&frame,
|
||||
&settings,
|
||||
&mode,
|
||||
&reckoning,
|
||||
&mut runtime,
|
||||
);
|
||||
|
||||
assert!(state.body_flipping_211);
|
||||
assert_eq!(speed, -3.0);
|
||||
assert!(
|
||||
runtime
|
||||
.calls
|
||||
.iter()
|
||||
.any(|v| v == "reckoning.begin_flip:true")
|
||||
);
|
||||
|
||||
state.body_flipping_211 = false;
|
||||
frame.flags_2488 = 0x0200_0000;
|
||||
runtime.calls.clear();
|
||||
calculate_body_flip_speed(
|
||||
&mut state,
|
||||
&frame,
|
||||
&settings,
|
||||
&mode,
|
||||
&reckoning,
|
||||
&mut runtime,
|
||||
);
|
||||
assert!(!state.body_flipping_211);
|
||||
assert!(runtime.calls.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn post_physics_sets_apex_then_requests_upside_down_wipeout() {
|
||||
let mut state = state();
|
||||
state.landing_normal_64 = [0.0, 1.0, 0.0, 0.0];
|
||||
let mut frame = frame();
|
||||
frame.skater_up_544 = [0.0, -1.0, 0.0, 0.0];
|
||||
let mode = mode();
|
||||
let wipeout_settings = KnownAirWipeoutSettings {
|
||||
air_falling_min_up_y_260: 0.0,
|
||||
air_falling_max_angle_264: 0.0,
|
||||
};
|
||||
let mut request = KnownAirWipeoutRequest {
|
||||
requested_35: false,
|
||||
scalar_116: 9.0,
|
||||
counter_200: 4,
|
||||
};
|
||||
let mut runtime = MockRuntime::new();
|
||||
runtime.board_velocity = [0.0, -2.0, 0.0, 0.0];
|
||||
|
||||
update_post_physics(
|
||||
&mut state,
|
||||
&frame,
|
||||
&mode,
|
||||
&wipeout_settings,
|
||||
&mut request,
|
||||
&mut runtime,
|
||||
);
|
||||
|
||||
assert!(state.reached_apex_208);
|
||||
assert!(request.requested_35);
|
||||
assert_eq!(request.scalar_116, 0.0);
|
||||
assert_eq!(request.counter_200, 5);
|
||||
assert_eq!(runtime.calls[0], "board.velocity");
|
||||
assert_eq!(runtime.calls[1], "wipeout.check:true");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn output_writes_all_samples_and_preserves_conditional_fields() {
|
||||
let mut state = state();
|
||||
state.trajectory_index_216 = 2;
|
||||
state.collision_position_112 = [1.0, 0.0, 0.0, 0.0];
|
||||
state.landing_normal_64 = [1.0, 0.0, 0.0, 0.0];
|
||||
state.max_y_188 = 7.0;
|
||||
state.start_y_184 = 2.0;
|
||||
state.collision_time_196 = 3.0;
|
||||
state.time_in_state_180 = 1.0;
|
||||
let frame = frame();
|
||||
let mut output = output();
|
||||
output.locked_trajectory_velocity_valid_452 = true;
|
||||
output.locked_trajectory_velocity_160 = [9.0; 4];
|
||||
let mut runtime = MockRuntime::new();
|
||||
runtime.prediction.trajectory.position = [0.0; 4];
|
||||
runtime.prediction.trajectory.velocity = [60.0, 0.0, 0.0, 0.0];
|
||||
runtime.selector_trajectory.position = [999.0, 0.0, 0.0, 0.0];
|
||||
runtime.selector_com = [0.0; 4];
|
||||
|
||||
fill_physics_output(&state, &frame, &mut output, &mut runtime);
|
||||
|
||||
assert_eq!(output.locked_trajectory_velocity_160, [9.0; 4]);
|
||||
assert!(output.locked_trajectory_velocity_valid_452);
|
||||
assert_eq!(output.jump_height_200, 5.0);
|
||||
assert_eq!(output.time_until_collision_184, 2.0);
|
||||
assert_eq!(output.trajectory_position_64[0], 2.0);
|
||||
assert_eq!(output.trajectory_plane_samples_336[0], 0.0);
|
||||
assert_close(output.trajectory_plane_samples_336[24], 24.0);
|
||||
assert!(output.known_air_valid_437);
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
//! KnownAir trajectory setup, ground restoration, and follow update.
|
||||
|
||||
use super::{
|
||||
data::{
|
||||
KnownAirFrame, KnownAirModeSettings, KnownAirSettings, KnownAirState, KnownAirTrajectory,
|
||||
Vector4,
|
||||
},
|
||||
runtime::KnownAirRuntime,
|
||||
};
|
||||
|
||||
const FIXED_STEP: f32 = f32::from_bits(0x3C88_8889); // 0x820849C8, 1/60.
|
||||
const HALF: f32 = f32::from_bits(0x3F00_0000); // 0x8209975C.
|
||||
const ONE: f32 = f32::from_bits(0x3F80_0000); // 0x8231A844.
|
||||
const PERFECT_FLIP_NUMERATOR: f32 = f32::from_bits(0xC0F1_463B); // 0x822F9450.
|
||||
const NEGATIVE_TWO_PI: f32 = f32::from_bits(0xC0C9_0FDB); // 0x822F9434.
|
||||
const HEADING_LENGTH_SQUARED_MIN: f32 = f32::from_bits(0x3DCC_CCCD); // 0x820641A8.
|
||||
|
||||
/// `PhysState_KnownAir::InitTrajectoryInfo`, TU3 `0x82D35508`.
|
||||
pub fn init_trajectory_info(
|
||||
state: &mut KnownAirState,
|
||||
frame: &KnownAirFrame,
|
||||
settings: &KnownAirSettings,
|
||||
mode: &KnownAirModeSettings,
|
||||
runtime: &mut impl KnownAirRuntime,
|
||||
) {
|
||||
state.landing_normal_64 = frame.selector_landing_normal_2656;
|
||||
|
||||
let prediction = runtime.selected_prediction();
|
||||
let (apex, time_to_apex) = runtime.highest_trajectory_position(&prediction.trajectory);
|
||||
state.time_to_apex_200 = time_to_apex;
|
||||
state.trajectory_apex_96 = apex;
|
||||
|
||||
state.collision_position_112 = runtime.selector_contact_position();
|
||||
//82D35580 reads r30+48; r30 is the complete winning result.
|
||||
state.collision_time_196 = prediction.collision_time_48;
|
||||
state.body_flip_target_speed_204 = if mode.perfect_body_flips_28 {
|
||||
PERFECT_FLIP_NUMERATOR / state.collision_time_196
|
||||
} else {
|
||||
(settings.flip_scalar / state.collision_time_196) * NEGATIVE_TWO_PI
|
||||
};
|
||||
state.selector_vector_128 = runtime.selector_vector_2848();
|
||||
|
||||
let mut heading = [0.0; 4];
|
||||
let mut heading_valid = false;
|
||||
state.collision_normal_speed_176 = 0.0;
|
||||
if state.collision_time_196 >= 0.0 {
|
||||
let collision_frame = prediction.collision_frame_128;
|
||||
let previous_time = collision_frame.wrapping_sub(2) as f32 * FIXED_STEP;
|
||||
let collision_time = collision_frame.wrapping_sub(1) as f32 * FIXED_STEP;
|
||||
let previous = trajectory_position(&prediction.trajectory, previous_time);
|
||||
let collision = trajectory_position(&prediction.trajectory, collision_time);
|
||||
let delta = sub(collision, previous);
|
||||
let velocity = runtime.divide_vector_by_scalar(delta, FIXED_STEP);
|
||||
|
||||
state.collision_normal_speed_176 = runtime.dot3(state.landing_normal_64, velocity);
|
||||
let normal_velocity = mul_scalar(state.landing_normal_64, state.collision_normal_speed_176);
|
||||
let tangent = sub(velocity, normal_velocity);
|
||||
let (normalized, length) = runtime.normalize_safe_with_length(tangent);
|
||||
if !(settings.min_target_heading_velocity_420 > length) {
|
||||
heading = normalized;
|
||||
heading_valid = true;
|
||||
}
|
||||
}
|
||||
|
||||
state.landing_heading_80 = heading;
|
||||
state.landing_heading_valid_209 = heading_valid;
|
||||
let heading_length_squared = runtime.dot3(heading, heading);
|
||||
if HEADING_LENGTH_SQUARED_MIN > heading_length_squared {
|
||||
state.landing_heading_valid_209 = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Ground transition velocity restoration, TU3 `0x82D35998`.
|
||||
pub fn restore_velocity(
|
||||
state: &mut KnownAirState,
|
||||
frame: &mut KnownAirFrame,
|
||||
settings: &KnownAirSettings,
|
||||
runtime: &mut impl KnownAirRuntime,
|
||||
) {
|
||||
let prediction = runtime.selected_prediction();
|
||||
if state.trajectory_index_216 == 0 {
|
||||
state.trajectory_index_216 = 1;
|
||||
}
|
||||
|
||||
let sample_time = state.trajectory_index_216 as f32 * FIXED_STEP;
|
||||
let trajectory_velocity = trajectory_velocity(&prediction.trajectory, sample_time);
|
||||
let geometry =
|
||||
runtime.restore_velocity_geometry_82d35998(trajectory_velocity, frame.ground_normal_464);
|
||||
let curve_value = settings
|
||||
.landing_speed_scalar_vs_ground_normal_y_240
|
||||
.evaluate(geometry.landing_speed_curve_input);
|
||||
let lower_clamped = select(
|
||||
-geometry.landing_speed_blend_source,
|
||||
0.0,
|
||||
geometry.landing_speed_blend_source,
|
||||
);
|
||||
let blend_weight = select(ONE - lower_clamped, lower_clamped, ONE);
|
||||
let speed_scale = (curve_value - ONE).mul_add(blend_weight, ONE);
|
||||
let scaled_tangent = mul_scalar(geometry.tangential_velocity, speed_scale);
|
||||
let current_board_velocity = runtime.board_body_velocity();
|
||||
let preserved_normal_speed = runtime.dot3(current_board_velocity, frame.ground_normal_464);
|
||||
let restored = core::array::from_fn(|lane| {
|
||||
frame.ground_normal_464[lane].mul_add(preserved_normal_speed, scaled_tangent[lane])
|
||||
});
|
||||
runtime.set_board_velocity(restored);
|
||||
|
||||
let forward = runtime.board_forward_axis();
|
||||
frame.forward_speed_2612 = runtime.dot3(restored, forward);
|
||||
}
|
||||
|
||||
/// `PhysState_KnownAir::UpdateTrajectoryFollow`, TU3 `0x82D36460`.
|
||||
pub fn update_trajectory_follow(
|
||||
state: &mut KnownAirState,
|
||||
frame: &KnownAirFrame,
|
||||
settings: &KnownAirSettings,
|
||||
runtime: &impl KnownAirRuntime,
|
||||
) {
|
||||
state.trajectory_index_216 = state.trajectory_index_216.wrapping_add(1);
|
||||
|
||||
let ratio = (settings.trajectory_error_blend_away_time_384 - state.time_in_state_180)
|
||||
/ settings.trajectory_error_blend_away_time_384;
|
||||
let lower_clamped = select(-ratio, 0.0, ratio);
|
||||
let blend = select(ONE - lower_clamped, lower_clamped, ONE);
|
||||
let sample_time = state.trajectory_index_216 as f32 * FIXED_STEP;
|
||||
let trajectory = runtime.selector_trajectory_2704();
|
||||
let trajectory_position = trajectory_position(&trajectory, sample_time);
|
||||
state.target_com_position_160 = core::array::from_fn(|lane| {
|
||||
state.trajectory_follow_offset_144[lane].mul_add(blend, trajectory_position[lane])
|
||||
});
|
||||
|
||||
if frame.flags_2468 & 0x0000_0008 != 0 {
|
||||
state.trajectory_index_216 = state.trajectory_index_216.wrapping_sub(1);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn trajectory_position(trajectory: &KnownAirTrajectory, time: f32) -> Vector4 {
|
||||
let time_squared = time * time;
|
||||
core::array::from_fn(|lane| {
|
||||
let linear = trajectory.velocity[lane].mul_add(time, trajectory.position[lane]);
|
||||
let half_acceleration = trajectory.acceleration[lane] * HALF;
|
||||
half_acceleration.mul_add(time_squared, linear)
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn trajectory_velocity(trajectory: &KnownAirTrajectory, time: f32) -> Vector4 {
|
||||
core::array::from_fn(|lane| {
|
||||
trajectory.acceleration[lane].mul_add(time, trajectory.velocity[lane])
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn add(left: Vector4, right: Vector4) -> Vector4 {
|
||||
core::array::from_fn(|lane| left[lane] + right[lane])
|
||||
}
|
||||
|
||||
pub(crate) fn sub(left: Vector4, right: Vector4) -> Vector4 {
|
||||
core::array::from_fn(|lane| left[lane] - right[lane])
|
||||
}
|
||||
|
||||
pub(crate) fn mul_scalar(vector: Vector4, scalar: f32) -> Vector4 {
|
||||
vector.map(|lane| lane * scalar)
|
||||
}
|
||||
|
||||
pub(crate) fn select(test: f32, nonnegative: f32, negative: f32) -> f32 {
|
||||
if test >= 0.0 { nonnegative } else { negative }
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
//! Full outer TU3 KnownAir update in recovered call order.
|
||||
|
||||
use super::{
|
||||
body::{calculate_body_flip_speed, calculate_body_spin_speed},
|
||||
data::{
|
||||
GrindFootBody, KnownAirFrame, KnownAirModeSettings, KnownAirReckoningFields,
|
||||
KnownAirSettings, KnownAirState,
|
||||
},
|
||||
footplant::update_footplant,
|
||||
runtime::KnownAirRuntime,
|
||||
trajectory::{init_trajectory_info, select, update_trajectory_follow},
|
||||
};
|
||||
|
||||
const ALIGNMENT_LEAD_FRAMES: f32 = f32::from_bits(0x4040_0000); // 0x82063B08, 3.
|
||||
const MIN_ALIGNMENT_FRAMES: f32 = f32::from_bits(0x4000_0000); // 0x82060C50, 2.
|
||||
const ONE: f32 = f32::from_bits(0x3F80_0000); // 0x8231A844.
|
||||
const BOARD_ERROR_WINDOW: f32 = f32::from_bits(0x3D75_C28F); // 0x82099460, 0.06.
|
||||
const ZERO: f32 = 0.0; // 0x82165A10.
|
||||
|
||||
const GRIND_FOOT_BODIES: [GrindFootBody; 6] = [
|
||||
GrindFootBody::Body3152,
|
||||
GrindFootBody::Body3156,
|
||||
GrindFootBody::Body3160,
|
||||
GrindFootBody::Body3168,
|
||||
GrindFootBody::Body3172,
|
||||
GrindFootBody::Body3176,
|
||||
];
|
||||
|
||||
/// `PhysState_KnownAir::Update`, TU3 `0x82D35C58`.
|
||||
pub fn update(
|
||||
state: &mut KnownAirState,
|
||||
frame: &KnownAirFrame,
|
||||
settings: &KnownAirSettings,
|
||||
mode: &KnownAirModeSettings,
|
||||
reckoning: &KnownAirReckoningFields,
|
||||
runtime: &mut impl KnownAirRuntime,
|
||||
) {
|
||||
update_footplant(state, frame, runtime);
|
||||
if runtime.selector_has_just_changed() {
|
||||
init_trajectory_info(state, frame, settings, mode, runtime);
|
||||
}
|
||||
runtime.update_air_collision();
|
||||
|
||||
let body_flip_speed =
|
||||
calculate_body_flip_speed(state, frame, settings, mode, reckoning, runtime);
|
||||
let body_spin_speed =
|
||||
calculate_body_spin_speed(state, frame, settings, mode, reckoning, runtime);
|
||||
|
||||
let remaining_frames =
|
||||
(state.collision_time_196 - state.time_in_state_180) / frame.delta_time_2604;
|
||||
let after_lead = remaining_frames - ALIGNMENT_LEAD_FRAMES;
|
||||
let alignment_frames = select(
|
||||
after_lead - MIN_ALIGNMENT_FRAMES,
|
||||
after_lead,
|
||||
MIN_ALIGNMENT_FRAMES,
|
||||
);
|
||||
|
||||
if !state.grind_air_adjust_activated_212
|
||||
&& (state.reached_apex_208 || alignment_frames < settings.frames_for_grind_air_assist_436)
|
||||
{
|
||||
let started = runtime.grind_air_adjust_started();
|
||||
runtime.set_grind_air_adjust_activated(started);
|
||||
state.grind_air_adjust_activated_212 = true;
|
||||
}
|
||||
|
||||
if state.targeting_grind_213
|
||||
&& state.grind_air_adjust_activated_212
|
||||
&& runtime.grind_air_adjust_adjusting()
|
||||
{
|
||||
for body in GRIND_FOOT_BODIES {
|
||||
runtime.disable_grind_foot_collision(body);
|
||||
}
|
||||
}
|
||||
|
||||
runtime.update_reckoning_air_states(
|
||||
state.landing_normal_64,
|
||||
ONE / alignment_frames,
|
||||
body_spin_speed,
|
||||
body_flip_speed,
|
||||
);
|
||||
update_trajectory_follow(state, frame, settings, runtime);
|
||||
runtime.update_known_air_skeleton(state.target_com_position_160);
|
||||
|
||||
if state.time_in_state_180 < BOARD_ERROR_WINDOW {
|
||||
runtime.set_skeleton_add_skateboard_error(true);
|
||||
}
|
||||
|
||||
let head_target = if frame.flags_2472 & 0x0040_0000 != 0 {
|
||||
frame.alternate_head_target_112
|
||||
} else {
|
||||
state.collision_position_112
|
||||
};
|
||||
runtime.set_head_tracking_target(head_target, true);
|
||||
runtime.update_board_steering_tilt(ZERO);
|
||||
|
||||
let board_y = runtime.board_transform_height();
|
||||
if !(state.max_y_188 > board_y) {
|
||||
state.max_y_188 = board_y;
|
||||
}
|
||||
if frame.flags_2468 & 0x0000_0008 == 0 {
|
||||
state.time_in_state_180 += frame.delta_time_2604;
|
||||
}
|
||||
|
||||
let com_y = runtime.skeleton_com_position()[1];
|
||||
state.com_max_y_192 = select(state.com_max_y_192 - com_y, state.com_max_y_192, com_y);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
//! Recovered airborne reckoning units. Air-state scheduling remains a caller boundary.
|
||||
pub mod body_flip;
|
||||
pub mod body_spin;
|
||||
pub mod ground_normal;
|
||||
pub mod ground_jump;
|
||||
pub mod known;
|
||||
pub mod reckoning;
|
||||
pub mod state;
|
||||
pub mod trajectory;
|
||||
@@ -0,0 +1,75 @@
|
||||
//! Additional persistent Reckoning fields and actual inputs for82D8DBD8.
|
||||
use crate::{
|
||||
air::{body_flip::BodyFlipSettings, body_spin::BodySpinSettings},
|
||||
physics::skeleton_animation_record::{AnimationPartTransform, IDENTITY},
|
||||
point_graph::PointGraph,
|
||||
};
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct AirState {
|
||||
///1568/1572, shared with Ground entry and PhysicsAir exit.
|
||||
pub spin_angle: f32,
|
||||
pub spin_speed: f32,
|
||||
///1580 is cleared by UpdateAirStates (1576 lives in ReckoningFrames).
|
||||
pub secondary_lean_angle: f32,
|
||||
pub flip_angle: f32,
|
||||
pub flip_speed: f32,
|
||||
pub flip_requested_speed: f32,
|
||||
///1072; final combined1008 has one owner, ReckoningFrames.body_flip.
|
||||
pub spin_transform: AnimationPartTransform,
|
||||
///1232 and1596; their native trajectory/entry producers remain the caller.
|
||||
pub flip_axis: [f32; 4],
|
||||
pub flip_active: bool,
|
||||
///1597, BeginBodyFlip82D8EC60 and ResetBodyFlip82D8EAA4.
|
||||
pub flip_side: bool,
|
||||
}
|
||||
impl AirState {
|
||||
///82D33178: scalar zeros, identity1072, world-X axis and inactive flip.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
spin_angle: 0.0,
|
||||
spin_speed: 0.0,
|
||||
secondary_lean_angle: 0.0,
|
||||
flip_angle: 0.0,
|
||||
flip_speed: 0.0,
|
||||
flip_requested_speed: 0.0,
|
||||
spin_transform: IDENTITY,
|
||||
flip_axis: [1.0, 0.0, 0.0, 0.0],
|
||||
flip_active: false,
|
||||
flip_side: false,
|
||||
}
|
||||
}
|
||||
///Ground entry82D37538/PhysicsAir exit82D346A0 reset exactly this pair.
|
||||
pub fn reset_spin(&mut self) {
|
||||
self.spin_angle = 0.0;
|
||||
self.spin_speed = 0.0;
|
||||
}
|
||||
}
|
||||
pub struct Settings {
|
||||
pub ground_normal_smoothing: [f32; 4],
|
||||
///PointNegGraphData8 layout336; caller directly evaluates its X/Y352/384.
|
||||
pub max_up_angle_delta: PointGraph<8>,
|
||||
pub tilt_vs_rotation: PointGraph<8>,
|
||||
pub tilt_vs_slope: PointGraph<8>,
|
||||
pub body_spin: BodySpinSettings,
|
||||
pub body_flip: BodyFlipSettings,
|
||||
}
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Input {
|
||||
pub landing_normal: [f32; 4],
|
||||
pub normal_blend: f32,
|
||||
pub target_spin: f32,
|
||||
pub flip_request: f32,
|
||||
///Processed752, from the actual Skeleton animation COM-to-deck producer.
|
||||
pub com_to_deck: [f32; 4],
|
||||
pub timestep: f32,
|
||||
///Processed2812, live PhysBodySpin animation attribute.
|
||||
pub physical_body_spin: f32,
|
||||
///Processed2644, produced by Skeleton82BDD008; used only when2484bit15 is set.
|
||||
pub grind_adjusted_body_spin: f32,
|
||||
pub additive_spin: bool,
|
||||
pub direct_spin: bool,
|
||||
pub reverse_stance: bool,
|
||||
///Actual selected physics_mode layout108 and28.
|
||||
pub easy_body_spins: bool,
|
||||
pub perfect_body_flips: bool,
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
//! Original TU3 angle limiter82BD3E78 and Air heading rotation82D8DFD4.
|
||||
//! Basic arithmetic runs on the host; this is not bit-exact Xenon emulation.
|
||||
use crate::{
|
||||
math::Vector3,
|
||||
physics::{
|
||||
board_ground::angle_between,
|
||||
board_motion_output::inverse_length_squared,
|
||||
native_arithmetic::{dot3, vector_max, vector_min},
|
||||
},
|
||||
trigonometry,
|
||||
};
|
||||
pub(super) type V = [f32; 4];
|
||||
const EPSILON: f32 = f32::from_bits(0x3586_37bd);
|
||||
const TAU: f32 = f32::from_bits(0x40c9_0fdb);
|
||||
const INV_TAU: f32 = f32::from_bits(0x3e22_f983);
|
||||
|
||||
pub(super) fn angle(a: V, b: V) -> f32 {
|
||||
angle_between(
|
||||
Vector3::new(a[0], a[1], a[2]),
|
||||
Vector3::new(b[0], b[1], b[2]),
|
||||
)
|
||||
}
|
||||
pub(super) fn wrap(angle: f32) -> f32 {
|
||||
let turns = angle * INV_TAU;
|
||||
let fraction = turns - turns.floor();
|
||||
(fraction - if fraction > 0.5 { 1.0 } else { 0.0 }) * TAU
|
||||
}
|
||||
pub(super) fn length(v: V) -> f32 {
|
||||
let sq = dot3(v, v);
|
||||
let inverse = inverse_length_squared(sq, 2);
|
||||
if sq == 0.0 { 0.0 } else { sq * inverse }
|
||||
}
|
||||
pub(super) fn normalize(v: V) -> V {
|
||||
let inverse = inverse_length_squared(dot3(v, v), 2);
|
||||
v.map(|x| x * inverse)
|
||||
}
|
||||
pub(super) fn normalize_safe(v: V, fallback: V) -> V {
|
||||
let sq = dot3(v, v);
|
||||
let inverse = inverse_length_squared(sq, 2);
|
||||
let magnitude = if sq == 0.0 { 0.0 } else { sq * inverse };
|
||||
if magnitude > EPSILON {
|
||||
v.map(|x| x * inverse)
|
||||
} else {
|
||||
fallback
|
||||
}
|
||||
}
|
||||
fn cross(a: V, b: V) -> V {
|
||||
[
|
||||
(-a[2]).mul_add(b[1], a[1] * b[2]),
|
||||
(-a[0]).mul_add(b[2], a[2] * b[0]),
|
||||
(-a[1]).mul_add(b[0], a[0] * b[1]),
|
||||
(-a[3]).mul_add(b[3], a[3] * b[3]),
|
||||
]
|
||||
}
|
||||
|
||||
///82BD3E78. Retains the source's original (not normalized) endpoint operands
|
||||
/// and final target magnitude. The parallel-axis branch returns target as-is.
|
||||
pub fn limit_angle(target: V, from: V, maximum: f32) -> V {
|
||||
let normalized_from = normalize_safe(from, [0.0; 4]);
|
||||
let normalized_target = normalize_safe(target, [0.0; 4]);
|
||||
let axis = cross(normalized_target, normalized_from);
|
||||
let cosine = vector_min(
|
||||
vector_max(dot3(normalized_target, normalized_from), -1.0),
|
||||
1.0,
|
||||
);
|
||||
let angle = trigonometry::acos(cosine);
|
||||
//The limiter uses floor(angle/tau + .5), unlike its caller's fraction wrap.
|
||||
let turns = angle.mul_add(INV_TAU, 0.5).floor();
|
||||
let closest_angle = (-turns).mul_add(TAU, angle);
|
||||
if closest_angle.abs() < maximum || dot3(axis, axis) < f32::from_bits(0x3780_0000) {
|
||||
return target;
|
||||
}
|
||||
let axis = normalize(axis);
|
||||
let (sin, cos) = trigonometry::sin_cos(-maximum * 0.5);
|
||||
let q = axis.map(|v| v * sin);
|
||||
let first = cross(q, from);
|
||||
let intermediate = std::array::from_fn(|i| cos.mul_add(from[i], first[i]));
|
||||
let second = cross(q, intermediate);
|
||||
let rotated: V = std::array::from_fn(|i| second[i].mul_add(2.0, from[i]));
|
||||
let magnitude = length(target);
|
||||
rotated.map(|v| v * magnitude)
|
||||
}
|
||||
|
||||
///82D8DFD4..E0B0: Rodrigues columns followed by X/Y/Z ordered FMAs.
|
||||
///The native permutation repeats each column's X into its W lane.
|
||||
pub(super) fn rotate_heading(heading: V, up: V, angle: f32) -> V {
|
||||
let [x, y, z, _] = up;
|
||||
let (s, c) = trigonometry::sin_cos(angle);
|
||||
let t = 1.0 - c;
|
||||
let (tx, ty, tz) = (t * x, t * y, t * z);
|
||||
let (sx, sy, sz) = (s * x, s * y, s * z);
|
||||
let xx = tx.mul_add(x, c);
|
||||
let yx = ty * x - sz;
|
||||
let zx = tz.mul_add(x, sy);
|
||||
let columns = [
|
||||
[xx, tx.mul_add(y, sz), tx * z - sy, xx],
|
||||
[yx, ty.mul_add(y, c), ty.mul_add(z, sx), yx],
|
||||
[zx, tz * y - sx, tz.mul_add(z, c), zx],
|
||||
];
|
||||
std::array::from_fn(|i| {
|
||||
let first = columns[0][i] * heading[0];
|
||||
let second = columns[1][i].mul_add(heading[1], first);
|
||||
columns[2][i].mul_add(heading[2], second)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
//! Complete original TU3 Reckoning::UpdateAirStates82D8DBD8.
|
||||
//! Shared Ground filters, frame owners and BodySpin history persist across states.
|
||||
mod data;
|
||||
mod math;
|
||||
use crate::{
|
||||
air::{
|
||||
body_flip::{self, BodyFlipInput, BodyFlipState},
|
||||
body_spin::{self, BodySpinState},
|
||||
},
|
||||
math::Vector3,
|
||||
riding::{ground_orientation::GroundOrientation, reckoning_frames::ReckoningFrames},
|
||||
};
|
||||
pub use data::{AirState, Input, Settings};
|
||||
pub use math::limit_angle as clamp_vector_within_max_angle;
|
||||
use math::{angle, length, limit_angle, normalize, normalize_safe, rotate_heading, wrap};
|
||||
|
||||
pub fn update(
|
||||
orientation: &mut GroundOrientation,
|
||||
frames: &mut ReckoningFrames,
|
||||
body_spin: &mut BodySpinState,
|
||||
state: &mut AirState,
|
||||
settings: &Settings,
|
||||
input: &Input,
|
||||
) {
|
||||
let old_up = lanes(orientation.up);
|
||||
orientation.dynamic_up = orientation.up;
|
||||
frames.target_lean_angle = 0.0;
|
||||
state.secondary_lean_angle = 0.0;
|
||||
let ground_normal = orientation
|
||||
.ground_filter
|
||||
.update(settings.ground_normal_smoothing, input.landing_normal);
|
||||
orientation.ground_normal = vector(ground_normal);
|
||||
|
||||
state.spin_speed = if input.additive_spin {
|
||||
input.grind_adjusted_body_spin + input.target_spin
|
||||
} else if input.direct_spin {
|
||||
input.target_spin
|
||||
} else {
|
||||
body_spin::update(
|
||||
body_spin,
|
||||
&settings.body_spin,
|
||||
input.physical_body_spin,
|
||||
input.target_spin,
|
||||
true,
|
||||
u8::from(input.easy_body_spins),
|
||||
);
|
||||
body_spin.speed()
|
||||
};
|
||||
state.spin_angle = state
|
||||
.spin_speed
|
||||
.mul_add(f32::from_bits(0x3c88_8889), state.spin_angle);
|
||||
if state.flip_active {
|
||||
//A short call-boundary view, not a second retained owner of matrix1008.
|
||||
let mut flip = BodyFlipState {
|
||||
angle: state.flip_angle,
|
||||
speed: state.flip_speed,
|
||||
requested_speed: state.flip_requested_speed,
|
||||
spin_transform: state.spin_transform,
|
||||
combined_transform: frames.body_flip,
|
||||
};
|
||||
body_flip::update(
|
||||
&mut flip,
|
||||
&settings.body_flip,
|
||||
&BodyFlipInput {
|
||||
requested_speed: input.flip_request,
|
||||
spin_angle: state.spin_angle,
|
||||
normal: old_up,
|
||||
flip_axis: state.flip_axis,
|
||||
timestep: input.timestep,
|
||||
perfect_body_flips: input.perfect_body_flips,
|
||||
},
|
||||
);
|
||||
state.flip_angle = flip.angle;
|
||||
state.flip_speed = flip.speed;
|
||||
state.flip_requested_speed = flip.requested_speed;
|
||||
state.spin_transform = flip.spin_transform;
|
||||
frames.body_flip = flip.combined_transform;
|
||||
}
|
||||
|
||||
let wrapped = wrap(angle(old_up, ground_normal));
|
||||
let maximum = wrapped * input.normal_blend;
|
||||
let target = if wrapped.abs() >= f32::from_bits(0x40c8_f61e) {
|
||||
if old_up[1].abs() < f32::from_bits(0x3f7d_70a4) {
|
||||
[0.0, 1.0, 0.0, 0.0]
|
||||
} else {
|
||||
[1.0, 0.0, 0.0, 0.0]
|
||||
}
|
||||
} else {
|
||||
ground_normal
|
||||
};
|
||||
let candidate = normalize(limit_angle(target, old_up, maximum));
|
||||
let max_delta = settings
|
||||
.max_up_angle_delta
|
||||
.evaluate(length(input.com_to_deck));
|
||||
let selected = if wrap(angle(candidate, old_up)).abs() > max_delta {
|
||||
limit_angle(candidate, old_up, max_delta)
|
||||
} else {
|
||||
candidate
|
||||
};
|
||||
let up = normalize_safe(selected, old_up);
|
||||
orientation.up_velocity = vector(std::array::from_fn(|i| up[i] - old_up[i]));
|
||||
orientation.up = vector(up);
|
||||
orientation.target = orientation.up;
|
||||
|
||||
if !state.flip_active {
|
||||
frames.heading = rotate_heading(frames.heading, up, input.timestep * state.spin_speed);
|
||||
}
|
||||
//82D8C868 advances histories using retained control words, then publishes
|
||||
//the chosen up into each current-position slot; no filter reinitialization.
|
||||
orientation.slow_filter.filter(up);
|
||||
orientation.fast_filter.filter(up);
|
||||
orientation.slow_filter.publish_current(up);
|
||||
orientation.fast_filter.publish_current(up);
|
||||
frames.calculate_transform(up, ground_normal);
|
||||
frames.calculate_tilt(
|
||||
input.reverse_stance,
|
||||
&settings.tilt_vs_rotation,
|
||||
&settings.tilt_vs_slope,
|
||||
);
|
||||
}
|
||||
fn lanes(v: Vector3) -> [f32; 4] {
|
||||
[v.x, v.y, v.z, 0.0]
|
||||
}
|
||||
fn vector(v: [f32; 4]) -> Vector3 {
|
||||
Vector3::new(v[0], v[1], v[2])
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -0,0 +1,176 @@
|
||||
use super::*;
|
||||
use crate::{
|
||||
air::{body_flip::BodyFlipSettings, body_spin::BodySpinSettings},
|
||||
physics::skeleton_animation_record::IDENTITY,
|
||||
point_graph::PointGraph,
|
||||
riding::ground_orientation::GroundOrientationSettings,
|
||||
};
|
||||
fn curve(value: f32) -> PointGraph<8> {
|
||||
PointGraph {
|
||||
x: [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0],
|
||||
y: [value; 8],
|
||||
}
|
||||
}
|
||||
fn orientation() -> GroundOrientation {
|
||||
GroundOrientation::new(&GroundOrientationSettings {
|
||||
ground_normal_smoothing: [1.0, 0.0, 0.0, 1.0],
|
||||
up_vector_smoothing_slow: [0.2, 0.1, 0.05, 0.3],
|
||||
up_vector_smoothing_fast: [0.7, 0.2, 0.1, 0.8],
|
||||
dynamic_up_vs_ground_y: curve(0.0),
|
||||
ground_vector_blend: curve(0.0),
|
||||
deck_angle_usage_vs_speed: curve(0.0),
|
||||
up_vector_smoothing_vs_speed: curve(0.0),
|
||||
up_vector_max_delta_vs_speed: curve(1.0),
|
||||
ground_blend_max_delta: 0.0,
|
||||
up_vector_max_acceleration: 1.0,
|
||||
anti_wobble_damping: 0.5,
|
||||
extra_side_damping: 0.0,
|
||||
minimum_wheels_for_ground_blend: 1,
|
||||
})
|
||||
}
|
||||
fn settings() -> Settings {
|
||||
Settings {
|
||||
ground_normal_smoothing: [1.0, 0.0, 0.0, 1.0],
|
||||
max_up_angle_delta: curve(0.5),
|
||||
tilt_vs_rotation: curve(0.0),
|
||||
tilt_vs_slope: curve(0.0),
|
||||
body_spin: BodySpinSettings {
|
||||
derivative_floor: 0.4,
|
||||
acceleration_limit: 0.2,
|
||||
curves: [curve(1.0); 7],
|
||||
input_fade_threshold: [f32::from_bits(0x37800000); 4],
|
||||
},
|
||||
body_flip: BodyFlipSettings {
|
||||
smoothing: Some(0.1),
|
||||
maximum_speed: Some(5.0),
|
||||
spin_scale: Some(0.5),
|
||||
missing_attribute_value: 0.0,
|
||||
},
|
||||
}
|
||||
}
|
||||
fn input() -> Input {
|
||||
Input {
|
||||
landing_normal: [0.0, 1.0, 0.0, 0.0],
|
||||
normal_blend: 0.1,
|
||||
target_spin: 0.0,
|
||||
flip_request: 0.0,
|
||||
com_to_deck: [0.0, 1.0, 0.0, 0.0],
|
||||
timestep: 1.0 / 60.0,
|
||||
physical_body_spin: 0.0,
|
||||
grind_adjusted_body_spin: 0.0,
|
||||
additive_spin: false,
|
||||
direct_spin: true,
|
||||
reverse_stance: false,
|
||||
easy_body_spins: true,
|
||||
perfect_body_flips: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn angle_limit_rotates_from_toward_target_and_preserves_parallel_branch() {
|
||||
let target = [0.0, 0.0, 1.0, 0.0];
|
||||
let from = [1.0, 0.0, 0.0, 0.0];
|
||||
let result = math::limit_angle(target, from, std::f32::consts::FRAC_PI_4);
|
||||
assert!((result[0] - std::f32::consts::FRAC_1_SQRT_2).abs() < 0.0001);
|
||||
assert!((result[2] - std::f32::consts::FRAC_1_SQRT_2).abs() < 0.0001);
|
||||
assert_eq!(
|
||||
math::limit_angle([-2.0, 0.0, 0.0, 0.0], from, 0.1),
|
||||
[-2.0, 0.0, 0.0, 0.0]
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn direct_and_additive_spin_use_fixed_angle_step_and_keep_body_spin_history() {
|
||||
let mut orientation = orientation();
|
||||
let mut frames = ReckoningFrames::new();
|
||||
let mut spin = BodySpinState::new();
|
||||
let mut state = AirState::new();
|
||||
let mut input = input();
|
||||
input.target_spin = 2.0;
|
||||
input.timestep = 0.1;
|
||||
let old_history = *spin.words();
|
||||
update(
|
||||
&mut orientation,
|
||||
&mut frames,
|
||||
&mut spin,
|
||||
&mut state,
|
||||
&settings(),
|
||||
&input,
|
||||
);
|
||||
assert_eq!(state.spin_angle, 2.0 * f32::from_bits(0x3c888889));
|
||||
assert_eq!(spin.words(), &old_history);
|
||||
//Heading uses caller dt even though accumulated spin angle uses1/60.
|
||||
assert!((frames.heading[0] - 0.2_f32.cos()).abs() < 0.0001);
|
||||
assert!((frames.heading[2] + 0.2_f32.sin()).abs() < 0.0001);
|
||||
input.additive_spin = true;
|
||||
input.grind_adjusted_body_spin = 3.0;
|
||||
update(
|
||||
&mut orientation,
|
||||
&mut frames,
|
||||
&mut spin,
|
||||
&mut state,
|
||||
&settings(),
|
||||
&input,
|
||||
);
|
||||
assert_eq!(state.spin_speed, 5.0);
|
||||
assert_eq!(spin.words(), &old_history);
|
||||
}
|
||||
#[test]
|
||||
fn airborne_filter_update_retains_controls_and_publishes_current_normal() {
|
||||
let mut orientation = orientation();
|
||||
let mut frames = ReckoningFrames::new();
|
||||
let mut spin = BodySpinState::new();
|
||||
let mut state = AirState::new();
|
||||
let mut input = input();
|
||||
let slow_control: Vec<_> = orientation.slow_filter.words()[..4].to_vec();
|
||||
let fast_control: Vec<_> = orientation.fast_filter.words()[..4].to_vec();
|
||||
input.landing_normal = [0.0, 0.8, 0.6, 0.0];
|
||||
update(
|
||||
&mut orientation,
|
||||
&mut frames,
|
||||
&mut spin,
|
||||
&mut state,
|
||||
&settings(),
|
||||
&input,
|
||||
);
|
||||
assert!(orientation.ground_normal.z > 0.59);
|
||||
assert!(orientation.up.z > 0.0 && orientation.up.z < orientation.ground_normal.z);
|
||||
assert_eq!(
|
||||
&orientation.slow_filter.words()[..4],
|
||||
slow_control.as_slice()
|
||||
);
|
||||
assert_eq!(
|
||||
&orientation.fast_filter.words()[..4],
|
||||
fast_control.as_slice()
|
||||
);
|
||||
assert_eq!(
|
||||
&orientation.slow_filter.words()[4..8],
|
||||
&lanes(orientation.up).map(f32::to_bits)
|
||||
);
|
||||
assert_eq!(frames.ground[1], lanes(orientation.ground_normal));
|
||||
}
|
||||
#[test]
|
||||
fn active_zero_flip_preserves_combined_frame_and_spin_reset_preserves_flip() {
|
||||
let mut orientation = orientation();
|
||||
let mut frames = ReckoningFrames::new();
|
||||
let mut spin = BodySpinState::new();
|
||||
let mut state = AirState::new();
|
||||
let mut input = input();
|
||||
state.flip_active = true;
|
||||
input.target_spin = 1.0;
|
||||
update(
|
||||
&mut orientation,
|
||||
&mut frames,
|
||||
&mut spin,
|
||||
&mut state,
|
||||
&settings(),
|
||||
&input,
|
||||
);
|
||||
assert_eq!(frames.body_flip, IDENTITY);
|
||||
assert_eq!(frames.heading, [1.0, 0.0, 0.0, 0.0]);
|
||||
state.flip_angle = 0.3;
|
||||
state.flip_speed = 0.2;
|
||||
state.reset_spin();
|
||||
assert_eq!([state.spin_angle, state.spin_speed], [0.0; 2]);
|
||||
assert_eq!([state.flip_angle, state.flip_speed], [0.3, 0.2]);
|
||||
assert!(state.flip_active);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
//! TU3 PhysicsAir board update (`0x82D34AB0`).
|
||||
|
||||
use super::{
|
||||
data::{AirBoardForce, PhysicsAirFrame, PhysicsAirReckoningFields, PhysicsAirState},
|
||||
jump_velocity::calculate_velocity_from_jump,
|
||||
runtime::PhysicsAirRuntime,
|
||||
};
|
||||
|
||||
const AIR_COLLISION_FORCE_TAG: u32 = 15;
|
||||
|
||||
pub fn update_skateboard(
|
||||
state: &PhysicsAirState,
|
||||
frame: &PhysicsAirFrame,
|
||||
reckoning: &PhysicsAirReckoningFields,
|
||||
runtime: &mut impl PhysicsAirRuntime,
|
||||
) {
|
||||
runtime.update_board_steering_tilt(0.0);
|
||||
|
||||
let mut force = AirBoardForce::ZERO;
|
||||
let collision_force_created = runtime
|
||||
.calculate_air_collision_force(reckoning.collision_reference_normal_1216, &mut force);
|
||||
if collision_force_created {
|
||||
runtime.enqueue_board_force(AIR_COLLISION_FORCE_TAG, force);
|
||||
return;
|
||||
}
|
||||
|
||||
if !state.use_centre_of_mass_velocity && frame.frames_since_jump_correction_2576 < 12 {
|
||||
let velocity =
|
||||
calculate_velocity_from_jump(frame, frame.frames_since_jump_correction_2576, runtime);
|
||||
runtime.set_board_velocity(velocity);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
//! Persistent fields and direct inputs used by TU3 `PhysState_PhysicsAir`.
|
||||
|
||||
use crate::point_graph::PointGraph;
|
||||
|
||||
pub type Vector4 = [f32; 4];
|
||||
|
||||
/// Native trajectory storage at PhysicsAir+80.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct AirTrajectory {
|
||||
pub position: Vector4,
|
||||
pub velocity: Vector4,
|
||||
pub acceleration: Vector4,
|
||||
/// Native trajectory +48. Both recovered initializers write -1.0.
|
||||
pub scalar_48: f32,
|
||||
}
|
||||
|
||||
/// Fields owned by the TU3 PhysicsAir object. Enter preserves prior trajectory
|
||||
/// storage when COM mode is not selected; construct once and retain this owner.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct PhysicsAirState {
|
||||
/// PhysicsAir+80.
|
||||
pub centre_of_mass_trajectory: AirTrajectory,
|
||||
/// PhysicsAir+144.
|
||||
pub landing_normal: Vector4,
|
||||
/// PhysicsAir+160.
|
||||
pub time_in_state: f32,
|
||||
/// PhysicsAir+164/+168.
|
||||
pub start_y: f32,
|
||||
pub max_y: f32,
|
||||
/// PhysicsAir+172/+173/+174.
|
||||
pub reached_apex: bool,
|
||||
pub use_centre_of_mass_velocity: bool,
|
||||
/// The producer is selector+9662. A more specific stock name is unresolved.
|
||||
pub selector_latch_174: bool,
|
||||
/// PhysicsAir+176, decremented as a signed wrapping integer.
|
||||
pub trajectory_query_countdown: i32,
|
||||
}
|
||||
|
||||
impl Default for PhysicsAirState {
|
||||
///Original constructor82D341D0: zero vectors80/96/112, -1 at128,
|
||||
///up144, zero160/164/168 and172..176. Entry is a separate operation.
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
centre_of_mass_trajectory: AirTrajectory {
|
||||
position: [0.0; 4], velocity: [0.0; 4], acceleration: [0.0; 4], scalar_48: -1.0,
|
||||
},
|
||||
landing_normal: [0.0, 1.0, 0.0, 0.0],
|
||||
time_in_state: 0.0, start_y: 0.0, max_y: 0.0,
|
||||
reached_apex: false, use_centre_of_mass_velocity: false,
|
||||
selector_latch_174: false, trajectory_query_countdown: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct ProcessedPhysIn values consumed by the recovered methods.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct PhysicsAirFrame {
|
||||
pub current_velocity_400: Vector4,
|
||||
/// Y lane of the all-states ground position, read directly at +500.
|
||||
pub ground_position_y_500: f32,
|
||||
pub trajectory_position_592: Vector4,
|
||||
pub trajectory_velocity_608: Vector4,
|
||||
pub jump_velocity_848: Vector4,
|
||||
pub flags_2468: u32,
|
||||
pub previous_physics_state_2504: i32,
|
||||
pub previous_physics_category_2516: i32,
|
||||
pub frames_since_jump_correction_2576: i32,
|
||||
pub delta_time_2604: f32,
|
||||
pub body_spin_input_2640: f32,
|
||||
pub gravity_y_2648: f32,
|
||||
pub state_timer_2664: f32,
|
||||
}
|
||||
|
||||
/// Cached collection values read at PhysicsAir settings +320..+444.
|
||||
/// No retail defaults are embedded; the stock vault adapter must provide them.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct PhysicsAirSettings {
|
||||
/// PointGraph x at +320 and y at +352.
|
||||
pub body_spin_over_time_320: PointGraph<8>,
|
||||
pub landing_normal_blend_388: f32,
|
||||
pub body_spin_scale_428: f32,
|
||||
pub landing_normal_angle_limit_444: f32,
|
||||
}
|
||||
|
||||
/// Reckoning fields read directly by PhysicsAir, apart from the full
|
||||
/// `Reckoning::UpdateAirStates` effect exposed by the runtime interface.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct PhysicsAirReckoningFields {
|
||||
pub current_landing_normal_1152: Vector4,
|
||||
pub collision_reference_normal_1216: Vector4,
|
||||
pub body_spin_angle_1568: f32,
|
||||
pub body_spin_speed_1572: f32,
|
||||
}
|
||||
|
||||
/// Native zero-initialized point-force record passed to `0x82D944E8`.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct AirBoardForce {
|
||||
pub force_world: Vector4,
|
||||
pub point_board_local: Vector4,
|
||||
}
|
||||
|
||||
impl AirBoardForce {
|
||||
pub const ZERO: Self = Self {
|
||||
force_world: [0.0; 4],
|
||||
point_board_local: [0.0; 4],
|
||||
};
|
||||
}
|
||||
|
||||
/// Writes performed by `FillPhysOut` (`0x82D34E90`). `scalar_184_write` is
|
||||
/// `None` when native code leaves the existing PhysOut field untouched.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct PhysicsAirOutput {
|
||||
pub is_at_apex: bool,
|
||||
pub jump_height: f32,
|
||||
pub landing_normal: Vector4,
|
||||
pub scalar_184_write: Option<f32>,
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
//! TU3 PhysicsAir jump-velocity correction (`0x82D34BC0`).
|
||||
|
||||
use super::data::{PhysicsAirFrame, Vector4};
|
||||
|
||||
const VERTICAL_ERROR_FLOOR: f32 = f32::from_bits(0xC040_0000); // -3.0
|
||||
const HORIZONTAL_ERROR_LIMIT_SQUARED: f32 = f32::from_bits(0x4110_0000); // 9.0
|
||||
|
||||
/// Numerical operations used by the recovered formula. The PC adapter uses
|
||||
/// independently implemented arithmetic and the original refinement steps;
|
||||
/// agreement with every Xenon floating-point edge case is not established.
|
||||
pub trait PhysicsAirMath {
|
||||
/// Native `vminfp128`, used by the OffBoard air-entry velocity cap.
|
||||
fn minimum_vminfp(&mut self, left: f32, right: f32) -> f32;
|
||||
|
||||
/// Native `vmsum3fp128(value, value)`; only lane zero is consumed.
|
||||
fn length_squared_vmsum3fp(&mut self, value: Vector4) -> f32;
|
||||
|
||||
/// The native vmsum/vrsqrte/two-refinement sequence at 82D34D10..68.
|
||||
fn length_vmsum3fp_vrsqrte(&mut self, value: Vector4) -> f32;
|
||||
|
||||
/// `PhysicsUtility::ClampVectorWithinMaxLength`, `0x82BD3D90`.
|
||||
fn clamp_vector_within_max_length(&mut self, value: Vector4, maximum_length: f32) -> Vector4;
|
||||
}
|
||||
|
||||
/// Reconstructs the full branch contract and arithmetic order of `0x82D34BC0`.
|
||||
/// The frame count is signed in the caller and in the target's `extsw/fcfid`.
|
||||
pub fn calculate_velocity_from_jump(
|
||||
frame: &PhysicsAirFrame,
|
||||
frames: i32,
|
||||
math: &mut impl PhysicsAirMath,
|
||||
) -> Vector4 {
|
||||
let frame_seconds = frame.delta_time_2604 * frames as f32;
|
||||
let gravity_delta = frame_seconds * frame.gravity_y_2648;
|
||||
let mut predicted = frame.jump_velocity_848;
|
||||
predicted[0] += 0.0;
|
||||
predicted[1] += gravity_delta;
|
||||
predicted[2] += 0.0;
|
||||
predicted[3] += 0.0;
|
||||
|
||||
let current = frame.current_velocity_400;
|
||||
let mut error = core::array::from_fn(|lane| current[lane] - predicted[lane]);
|
||||
let mut output = predicted;
|
||||
|
||||
// `vcmpgtfp(error.y, -3.0)`: false, including equality/unordered, selects
|
||||
// the current vertical velocity.
|
||||
if !(error[1] > VERTICAL_ERROR_FLOOR) {
|
||||
output[1] = current[1];
|
||||
}
|
||||
|
||||
error[1] = 0.0;
|
||||
let horizontal_error_squared = math.length_squared_vmsum3fp(error);
|
||||
// Native fcmpu+blt skips correction only for an ordered value below 9.0.
|
||||
if !(horizontal_error_squared < HORIZONTAL_ERROR_LIMIT_SQUARED) {
|
||||
let mut predicted_horizontal = output;
|
||||
predicted_horizontal[1] = 0.0;
|
||||
let maximum_length = math.length_vmsum3fp_vrsqrte(predicted_horizontal);
|
||||
|
||||
let mut current_horizontal = current;
|
||||
current_horizontal[1] = 0.0;
|
||||
let clamped = math.clamp_vector_within_max_length(current_horizontal, maximum_length);
|
||||
output[0] = clamped[0];
|
||||
output[2] = clamped[2];
|
||||
}
|
||||
output
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
//! TU3 PhysicsAir Enter/Exit/output lifecycle.
|
||||
|
||||
use super::{
|
||||
data::{
|
||||
AirTrajectory, PhysicsAirFrame, PhysicsAirOutput, PhysicsAirReckoningFields,
|
||||
PhysicsAirState, Vector4,
|
||||
},
|
||||
jump_velocity::calculate_velocity_from_jump,
|
||||
runtime::PhysicsAirRuntime,
|
||||
};
|
||||
|
||||
const TRAJECTORY_UNBOUNDED: f32 = f32::from_bits(0xBF80_0000); // -1.0
|
||||
const OFFBOARD_VERTICAL_SCALE: f32 = f32::from_bits(0x3F26_6666); // 0.65
|
||||
const OFFBOARD_VERTICAL_LIMIT: f32 = f32::from_bits(0x4040_0000); // 3.0
|
||||
const OUTPUT_SCALAR_184: f32 = f32::from_bits(0x4080_0000); // 4.0
|
||||
|
||||
/// Full TU3 `PhysState_PhysicsAir::Enter`, `0x82D34388`.
|
||||
pub fn enter(
|
||||
state: &mut PhysicsAirState,
|
||||
frame: &PhysicsAirFrame,
|
||||
world_acceleration: Vector4,
|
||||
runtime: &mut impl PhysicsAirRuntime,
|
||||
) {
|
||||
runtime.set_air_collision_update_enabled(true);
|
||||
runtime.set_skeleton_collision_state(7);
|
||||
runtime.set_skeleton_inverse_kinematics_enabled(true);
|
||||
runtime.set_skeleton_collision_state(6);
|
||||
runtime.enable_board_angular_drive_only();
|
||||
runtime.set_footplant_flag_240(false);
|
||||
runtime.reset_footplants();
|
||||
|
||||
state.selector_latch_174 = false;
|
||||
state.reached_apex = false;
|
||||
state.trajectory_query_countdown = 0;
|
||||
state.landing_normal = [0.0, 1.0, 0.0, 0.0];
|
||||
state.time_in_state = 0.0;
|
||||
state.max_y = runtime.board_transform_height();
|
||||
state.start_y = frame.ground_position_y_500;
|
||||
|
||||
let (use_centre_of_mass_velocity, request_heading_update) = initial_centre_of_mass_mode(frame);
|
||||
state.use_centre_of_mass_velocity = use_centre_of_mass_velocity;
|
||||
|
||||
if use_centre_of_mass_velocity {
|
||||
let mut velocity = frame.trajectory_velocity_608;
|
||||
if frame.previous_physics_state_2504 == 500 {
|
||||
let scaled_y = velocity[1] * OFFBOARD_VERTICAL_SCALE;
|
||||
velocity[1] = runtime.minimum_vminfp(scaled_y, OFFBOARD_VERTICAL_LIMIT);
|
||||
}
|
||||
if frame.frames_since_jump_correction_2576 < 12 {
|
||||
velocity = calculate_velocity_from_jump(
|
||||
frame,
|
||||
frame.frames_since_jump_correction_2576,
|
||||
runtime,
|
||||
);
|
||||
}
|
||||
state.centre_of_mass_trajectory = AirTrajectory {
|
||||
position: frame.trajectory_position_592,
|
||||
velocity,
|
||||
acceleration: world_acceleration,
|
||||
scalar_48: TRAJECTORY_UNBOUNDED,
|
||||
};
|
||||
}
|
||||
|
||||
// This branch-local flag deliberately differs from the COM selection in
|
||||
// the grind/category-400 path.
|
||||
if request_heading_update {
|
||||
runtime.request_skeleton_heading_update();
|
||||
}
|
||||
}
|
||||
|
||||
/// TU3 `PhysState_PhysicsAir::Exit`, `0x82D346A0`.
|
||||
pub fn exit(state: &mut PhysicsAirState, reckoning: &mut PhysicsAirReckoningFields) {
|
||||
state.use_centre_of_mass_velocity = false;
|
||||
reckoning.body_spin_speed_1572 = 0.0;
|
||||
reckoning.body_spin_angle_1568 = 0.0;
|
||||
}
|
||||
|
||||
/// TU3 `PhysState_PhysicsAir::FillPhysOut`, `0x82D34E90`.
|
||||
pub fn fill_physics_output(state: &PhysicsAirState) -> PhysicsAirOutput {
|
||||
PhysicsAirOutput {
|
||||
is_at_apex: state.reached_apex,
|
||||
jump_height: state.max_y - state.start_y,
|
||||
landing_normal: state.landing_normal,
|
||||
scalar_184_write: state.selector_latch_174.then_some(OUTPUT_SCALAR_184),
|
||||
}
|
||||
}
|
||||
|
||||
fn initial_centre_of_mass_mode(frame: &PhysicsAirFrame) -> (bool, bool) {
|
||||
if frame.previous_physics_category_2516 == 400 || frame.previous_physics_state_2504 == 701 {
|
||||
(frame.flags_2468 & 0x0000_4000 == 0, false)
|
||||
} else {
|
||||
let selected = !matches!(frame.previous_physics_state_2504, 100 | 103 | 201);
|
||||
(selected, selected)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
//! Independent arithmetic for original PhysicsAir's jump-velocity helpers.
|
||||
//! Retains original refinements; PC results are not certified Xenon bit-exact.
|
||||
use super::PhysicsAirMath;
|
||||
use crate::physics::{board_motion_output::inverse_length_squared, native_arithmetic};
|
||||
|
||||
pub struct AirMath;
|
||||
impl PhysicsAirMath for AirMath {
|
||||
fn minimum_vminfp(&mut self, left: f32, right: f32) -> f32 {
|
||||
native_arithmetic::vector_min(left, right)
|
||||
}
|
||||
fn length_squared_vmsum3fp(&mut self, value: [f32; 4]) -> f32 {
|
||||
native_arithmetic::dot3(value, value)
|
||||
}
|
||||
fn length_vmsum3fp_vrsqrte(&mut self, value: [f32; 4]) -> f32 {
|
||||
let square = native_arithmetic::dot3(value, value);
|
||||
let length = square * inverse_length_squared(square, 2);
|
||||
if square == 0.0 { 0.0 } else { length }
|
||||
}
|
||||
fn clamp_vector_within_max_length(&mut self, value: [f32; 4], maximum: f32) -> [f32; 4] {
|
||||
//82BD3D90..3E74, including the original short-vector bge and fsel.
|
||||
let length = self.length_vmsum3fp_vrsqrte(value);
|
||||
if !(length >= f32::from_bits(0x3780_0000)) { return value; }
|
||||
let bound = if maximum - length >= -0.0 { length } else { maximum };
|
||||
let mut inverse = native_arithmetic::reciprocal_estimate(length);
|
||||
for _ in 0..2 {
|
||||
inverse = inverse.mul_add((-inverse).mul_add(length, 1.0), inverse);
|
||||
}
|
||||
value.map(|v| (v * bound) * inverse)
|
||||
}
|
||||
}
|
||||
|
||||
///8296EBB0 uses one normalization refinement and a clamped dot product.
|
||||
pub fn angle_between_vectors(left: [f32; 4], right: [f32; 4]) -> f32 {
|
||||
let xyz = |v: [f32; 4]| crate::math::Vector3::new(v[0], v[1], v[2]);
|
||||
crate::physics::board_ground::angle_between(xyz(left), xyz(right))
|
||||
}
|
||||
|
||||
///Toolkit_ClampJumpVelocity82D93518. The output uses the CURRENT velocity's
|
||||
///direction; the supplied reference only bounds its magnitude.
|
||||
pub fn clamp_jump_velocity(reference: [f32; 4], current: [f32; 4]) -> [f32; 4] {
|
||||
let reference_speed = AirMath.length_vmsum3fp_vrsqrte(reference);
|
||||
let speed = AirMath.length_vmsum3fp_vrsqrte(current);
|
||||
let denominator = if speed - 1.0 >= -0.0 { speed } else { 1.0 };
|
||||
let scale = if denominator > reference_speed { reference_speed / denominator } else { 1.0 };
|
||||
current.map(|v| v * scale)
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
//! Readable TU3 `PhysState_PhysicsAir` lifecycle and update composition.
|
||||
//!
|
||||
//! The state owns the recovered branch/order semantics. Calls into the board,
|
||||
//! skeleton, trajectory selector, wipeout controller, and unresolved Xenon
|
||||
//! vector math remain mandatory runtime interfaces with no fallback.
|
||||
|
||||
mod board;
|
||||
mod data;
|
||||
mod jump_velocity;
|
||||
mod lifecycle;
|
||||
mod math;
|
||||
mod post_physics;
|
||||
mod runtime;
|
||||
mod update;
|
||||
|
||||
pub use board::update_skateboard;
|
||||
pub use data::{
|
||||
AirBoardForce, AirTrajectory, PhysicsAirFrame, PhysicsAirOutput, PhysicsAirReckoningFields,
|
||||
PhysicsAirSettings, PhysicsAirState,
|
||||
};
|
||||
pub use jump_velocity::{PhysicsAirMath, calculate_velocity_from_jump};
|
||||
pub use math::{AirMath, angle_between_vectors, clamp_jump_velocity};
|
||||
pub use lifecycle::{enter, exit, fill_physics_output};
|
||||
pub use post_physics::update_post_physics;
|
||||
pub use runtime::{PhysicsAirLaunchInfo, PhysicsAirRuntime};
|
||||
pub use update::{integrate_trajectory_fixed_step, update, wrap_signed_angle};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -0,0 +1,15 @@
|
||||
//! TU3 PhysicsAir post-physics update (`0x82D34DD8`).
|
||||
|
||||
use super::{data::PhysicsAirState, runtime::PhysicsAirRuntime};
|
||||
|
||||
/// Latches the first strict downward board velocity, then always runs the air
|
||||
/// wipeout check. Negative zero and NaN do not mark the apex.
|
||||
pub fn update_post_physics(state: &mut PhysicsAirState, runtime: &mut impl PhysicsAirRuntime) {
|
||||
if !state.reached_apex {
|
||||
let board_velocity = runtime.board_body_velocity();
|
||||
if board_velocity[1] < 0.0 {
|
||||
state.reached_apex = true;
|
||||
}
|
||||
}
|
||||
runtime.check_for_air_wipeout(false);
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
//! Mandatory engine-facing effects used by TU3 PhysicsAir.
|
||||
|
||||
use super::{
|
||||
data::{AirBoardForce, PhysicsAirReckoningFields, Vector4},
|
||||
jump_velocity::PhysicsAirMath,
|
||||
};
|
||||
|
||||
/// Mutations of the 272-byte trajectory launch record recovered at
|
||||
/// `0x82D33448`, `0x82BE33D0`, and `0x82D34570`.
|
||||
pub trait PhysicsAirLaunchInfo {
|
||||
fn set_start_velocity(&mut self, velocity: Vector4);
|
||||
fn centre_of_mass_animation_position(&self) -> Vector4;
|
||||
fn set_trajectory_start_position_override(&mut self, position: Vector4);
|
||||
fn set_board_position_override(&mut self, position: Vector4);
|
||||
fn cone_angles(&self) -> [f32; 2];
|
||||
fn set_cone_angles(&mut self, angles: [f32; 2]);
|
||||
fn set_player_jumped(&mut self, jumped: bool);
|
||||
fn set_use_trajectory_start_position_override(&mut self, enabled: bool);
|
||||
fn set_trajectory_count(&mut self, count: u16);
|
||||
}
|
||||
|
||||
/// Every external call/read made by the recovered PhysicsAir methods. There is
|
||||
/// deliberately no no-op/default implementation: an engine adapter must bind
|
||||
/// each stock-owned object before this state can be scheduled.
|
||||
pub trait PhysicsAirRuntime: PhysicsAirMath {
|
||||
type LaunchInfo: PhysicsAirLaunchInfo;
|
||||
|
||||
fn set_air_collision_update_enabled(&mut self, enabled: bool);
|
||||
fn set_skeleton_collision_state(&mut self, state: u32);
|
||||
fn set_skeleton_inverse_kinematics_enabled(&mut self, enabled: bool);
|
||||
fn enable_board_angular_drive_only(&mut self);
|
||||
fn set_footplant_flag_240(&mut self, value: bool);
|
||||
fn reset_footplants(&mut self);
|
||||
fn board_transform_height(&mut self) -> f32;
|
||||
fn request_skeleton_heading_update(&mut self);
|
||||
|
||||
fn update_air_collision(&mut self);
|
||||
fn trajectory_query_just_started(&self) -> bool;
|
||||
fn construct_trajectory_launch_info(&mut self) -> Self::LaunchInfo;
|
||||
fn fill_skeleton_launch_info(&mut self, info: &mut Self::LaunchInfo);
|
||||
fn launch_trajectory(&mut self, info: &Self::LaunchInfo);
|
||||
fn update_trajectory_selector(&mut self);
|
||||
fn selector_landing_normal(&self) -> Option<Vector4>;
|
||||
fn selector_centre_of_mass_trajectory_ready(&self) -> bool;
|
||||
|
||||
/// `AngleBetweenVectors` (`0x8296EBB0`), including its unresolved VMX
|
||||
/// normalization behavior. The caller performs the recovered angle wrap.
|
||||
fn angle_between_vectors(&mut self, left: Vector4, right: Vector4) -> f32;
|
||||
///Return the completed canonical fields: UpdateSkateboard reads the newly
|
||||
///filtered collision normal1216, after82D8DBD8, not the entry snapshot.
|
||||
fn update_reckoning_air_states(
|
||||
&mut self,
|
||||
landing_normal: Vector4,
|
||||
normal_blend: f32,
|
||||
body_spin: f32,
|
||||
body_flip: f32,
|
||||
) -> PhysicsAirReckoningFields;
|
||||
fn update_known_air_skeleton(&mut self, trajectory_position: Vector4);
|
||||
fn update_animated_skateboard_skeleton(&mut self, argument: bool);
|
||||
|
||||
fn update_board_steering_tilt(&mut self, tilt: f32);
|
||||
/// `Toolkit_CalcCollisionForce` (`0x82D944E8`). The adapter supplies its
|
||||
/// bound settings and complete live ProcessedPhysIn; PhysicsAir only passes
|
||||
/// the reference normal and receives the point-force record.
|
||||
fn calculate_air_collision_force(
|
||||
&mut self,
|
||||
reference_normal: Vector4,
|
||||
force: &mut AirBoardForce,
|
||||
) -> bool;
|
||||
fn enqueue_board_force(&mut self, tag: u32, force: AirBoardForce);
|
||||
fn set_board_velocity(&mut self, velocity: Vector4);
|
||||
fn enable_skateboard_error_on_skeleton(&mut self);
|
||||
|
||||
fn board_body_velocity(&mut self) -> Vector4;
|
||||
fn check_for_air_wipeout(&mut self, argument: bool);
|
||||
}
|
||||
@@ -0,0 +1,551 @@
|
||||
use super::*;
|
||||
use crate::point_graph::PointGraph;
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
struct FakeLaunchInfo {
|
||||
start_velocity: [f32; 4],
|
||||
animation_position: [f32; 4],
|
||||
trajectory_override: [f32; 4],
|
||||
board_override: [f32; 4],
|
||||
cone_angles: [f32; 2],
|
||||
jumped_writes: Vec<bool>,
|
||||
use_override: bool,
|
||||
trajectory_count: u16,
|
||||
}
|
||||
|
||||
impl PhysicsAirLaunchInfo for FakeLaunchInfo {
|
||||
fn set_start_velocity(&mut self, velocity: [f32; 4]) {
|
||||
self.start_velocity = velocity;
|
||||
}
|
||||
|
||||
fn centre_of_mass_animation_position(&self) -> [f32; 4] {
|
||||
self.animation_position
|
||||
}
|
||||
|
||||
fn set_trajectory_start_position_override(&mut self, position: [f32; 4]) {
|
||||
self.trajectory_override = position;
|
||||
}
|
||||
|
||||
fn set_board_position_override(&mut self, position: [f32; 4]) {
|
||||
self.board_override = position;
|
||||
}
|
||||
|
||||
fn cone_angles(&self) -> [f32; 2] {
|
||||
self.cone_angles
|
||||
}
|
||||
|
||||
fn set_cone_angles(&mut self, angles: [f32; 2]) {
|
||||
self.cone_angles = angles;
|
||||
}
|
||||
|
||||
fn set_player_jumped(&mut self, jumped: bool) {
|
||||
self.jumped_writes.push(jumped);
|
||||
}
|
||||
|
||||
fn set_use_trajectory_start_position_override(&mut self, enabled: bool) {
|
||||
self.use_override = enabled;
|
||||
}
|
||||
|
||||
fn set_trajectory_count(&mut self, count: u16) {
|
||||
self.trajectory_count = count;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct FakeRuntime {
|
||||
events: Vec<String>,
|
||||
board_height: f32,
|
||||
board_velocity: [f32; 4],
|
||||
query_started: bool,
|
||||
selector_normal: Option<[f32; 4]>,
|
||||
selector_com_ready: bool,
|
||||
angle: f32,
|
||||
collision: bool,
|
||||
collision_force: AirBoardForce,
|
||||
enqueued_force: Option<(u32, AirBoardForce)>,
|
||||
set_velocity: Option<[f32; 4]>,
|
||||
launched: Option<FakeLaunchInfo>,
|
||||
reckoning_call: Option<([f32; 4], f32, f32, f32)>,
|
||||
reckoning_output: PhysicsAirReckoningFields,
|
||||
collision_reference: Option<[f32; 4]>,
|
||||
known_air_position: Option<[f32; 4]>,
|
||||
forced_squared_length: Option<f32>,
|
||||
forced_length: Option<f32>,
|
||||
forced_clamp: Option<[f32; 4]>,
|
||||
}
|
||||
|
||||
impl Default for FakeRuntime {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
events: Vec::new(),
|
||||
board_height: 0.0,
|
||||
board_velocity: [0.0; 4],
|
||||
query_started: false,
|
||||
selector_normal: None,
|
||||
selector_com_ready: false,
|
||||
angle: 0.0,
|
||||
collision: false,
|
||||
collision_force: AirBoardForce::ZERO,
|
||||
enqueued_force: None,
|
||||
set_velocity: None,
|
||||
launched: None,
|
||||
reckoning_call: None,
|
||||
reckoning_output: reckoning(),
|
||||
collision_reference: None,
|
||||
known_air_position: None,
|
||||
forced_squared_length: None,
|
||||
forced_length: None,
|
||||
forced_clamp: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PhysicsAirMath for FakeRuntime {
|
||||
fn minimum_vminfp(&mut self, left: f32, right: f32) -> f32 {
|
||||
if left < right { left } else { right }
|
||||
}
|
||||
|
||||
fn length_squared_vmsum3fp(&mut self, value: [f32; 4]) -> f32 {
|
||||
self.forced_squared_length
|
||||
.unwrap_or(value[0] * value[0] + value[1] * value[1] + value[2] * value[2])
|
||||
}
|
||||
|
||||
fn length_vmsum3fp_vrsqrte(&mut self, value: [f32; 4]) -> f32 {
|
||||
self.forced_length
|
||||
.unwrap_or((value[0] * value[0] + value[1] * value[1] + value[2] * value[2]).sqrt())
|
||||
}
|
||||
|
||||
fn clamp_vector_within_max_length(&mut self, value: [f32; 4], maximum_length: f32) -> [f32; 4] {
|
||||
if let Some(forced) = self.forced_clamp {
|
||||
return forced;
|
||||
}
|
||||
let length = (value[0] * value[0] + value[1] * value[1] + value[2] * value[2]).sqrt();
|
||||
if length > maximum_length {
|
||||
value.map(|lane| lane * (maximum_length / length))
|
||||
} else {
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PhysicsAirRuntime for FakeRuntime {
|
||||
type LaunchInfo = FakeLaunchInfo;
|
||||
|
||||
fn set_air_collision_update_enabled(&mut self, enabled: bool) {
|
||||
self.events.push(format!("collision-enabled:{enabled}"));
|
||||
}
|
||||
|
||||
fn set_skeleton_collision_state(&mut self, state: u32) {
|
||||
self.events.push(format!("collision-state:{state}"));
|
||||
}
|
||||
|
||||
fn set_skeleton_inverse_kinematics_enabled(&mut self, enabled: bool) {
|
||||
self.events.push(format!("ik:{enabled}"));
|
||||
}
|
||||
|
||||
fn enable_board_angular_drive_only(&mut self) {
|
||||
self.events.push("angular-drive".into());
|
||||
}
|
||||
|
||||
fn set_footplant_flag_240(&mut self, value: bool) {
|
||||
self.events.push(format!("footplant-240:{value}"));
|
||||
}
|
||||
|
||||
fn reset_footplants(&mut self) {
|
||||
self.events.push("footplant-reset".into());
|
||||
}
|
||||
|
||||
fn board_transform_height(&mut self) -> f32 {
|
||||
self.events.push("board-height".into());
|
||||
self.board_height
|
||||
}
|
||||
|
||||
fn request_skeleton_heading_update(&mut self) {
|
||||
self.events.push("heading".into());
|
||||
}
|
||||
|
||||
fn update_air_collision(&mut self) {
|
||||
self.events.push("air-collision".into());
|
||||
}
|
||||
|
||||
fn trajectory_query_just_started(&self) -> bool {
|
||||
self.query_started
|
||||
}
|
||||
|
||||
fn construct_trajectory_launch_info(&mut self) -> Self::LaunchInfo {
|
||||
self.events.push("launch-construct".into());
|
||||
FakeLaunchInfo::default()
|
||||
}
|
||||
|
||||
fn fill_skeleton_launch_info(&mut self, info: &mut Self::LaunchInfo) {
|
||||
self.events.push("launch-fill".into());
|
||||
info.animation_position = [10.0, 20.0, 30.0, 40.0];
|
||||
info.cone_angles = [2.0, 4.0];
|
||||
}
|
||||
|
||||
fn launch_trajectory(&mut self, info: &Self::LaunchInfo) {
|
||||
self.events.push("launch".into());
|
||||
self.launched = Some(info.clone());
|
||||
}
|
||||
|
||||
fn update_trajectory_selector(&mut self) {
|
||||
self.events.push("selector-update".into());
|
||||
}
|
||||
|
||||
fn selector_landing_normal(&self) -> Option<[f32; 4]> {
|
||||
self.selector_normal
|
||||
}
|
||||
|
||||
fn selector_centre_of_mass_trajectory_ready(&self) -> bool {
|
||||
self.selector_com_ready
|
||||
}
|
||||
|
||||
fn angle_between_vectors(&mut self, _left: [f32; 4], _right: [f32; 4]) -> f32 {
|
||||
self.events.push("angle".into());
|
||||
self.angle
|
||||
}
|
||||
|
||||
fn update_reckoning_air_states(
|
||||
&mut self,
|
||||
landing_normal: [f32; 4],
|
||||
normal_blend: f32,
|
||||
body_spin: f32,
|
||||
body_flip: f32,
|
||||
) -> PhysicsAirReckoningFields {
|
||||
self.events.push("reckoning".into());
|
||||
self.reckoning_call = Some((landing_normal, normal_blend, body_spin, body_flip));
|
||||
self.reckoning_output
|
||||
}
|
||||
|
||||
fn update_known_air_skeleton(&mut self, trajectory_position: [f32; 4]) {
|
||||
self.events.push("known-air".into());
|
||||
self.known_air_position = Some(trajectory_position);
|
||||
}
|
||||
|
||||
fn update_animated_skateboard_skeleton(&mut self, argument: bool) {
|
||||
self.events.push(format!("animated-board:{argument}"));
|
||||
}
|
||||
|
||||
fn update_board_steering_tilt(&mut self, tilt: f32) {
|
||||
self.events.push(format!("steering-tilt:{tilt}"));
|
||||
}
|
||||
|
||||
fn calculate_air_collision_force(
|
||||
&mut self,
|
||||
reference_normal: [f32; 4],
|
||||
force: &mut AirBoardForce,
|
||||
) -> bool {
|
||||
self.events.push("collision-force".into());
|
||||
self.collision_reference = Some(reference_normal);
|
||||
*force = self.collision_force;
|
||||
self.collision
|
||||
}
|
||||
|
||||
fn enqueue_board_force(&mut self, tag: u32, force: AirBoardForce) {
|
||||
self.events.push("enqueue-force".into());
|
||||
self.enqueued_force = Some((tag, force));
|
||||
}
|
||||
|
||||
fn set_board_velocity(&mut self, velocity: [f32; 4]) {
|
||||
self.events.push("set-board-velocity".into());
|
||||
self.set_velocity = Some(velocity);
|
||||
}
|
||||
|
||||
fn enable_skateboard_error_on_skeleton(&mut self) {
|
||||
self.events.push("board-error".into());
|
||||
}
|
||||
|
||||
fn board_body_velocity(&mut self) -> [f32; 4] {
|
||||
self.events.push("board-velocity".into());
|
||||
self.board_velocity
|
||||
}
|
||||
|
||||
fn check_for_air_wipeout(&mut self, argument: bool) {
|
||||
self.events.push(format!("wipeout:{argument}"));
|
||||
}
|
||||
}
|
||||
|
||||
fn frame() -> PhysicsAirFrame {
|
||||
PhysicsAirFrame {
|
||||
current_velocity_400: [1.0, 2.0, 3.0, 0.0],
|
||||
ground_position_y_500: 4.0,
|
||||
trajectory_position_592: [1.0, 5.0, 2.0, 0.0],
|
||||
trajectory_velocity_608: [2.0, 10.0, 4.0, 0.0],
|
||||
jump_velocity_848: [2.0, 8.0, 4.0, 0.0],
|
||||
flags_2468: 0,
|
||||
previous_physics_state_2504: 500,
|
||||
previous_physics_category_2516: 0,
|
||||
frames_since_jump_correction_2576: 12,
|
||||
delta_time_2604: 0.1,
|
||||
body_spin_input_2640: 3.0,
|
||||
gravity_y_2648: -10.0,
|
||||
state_timer_2664: 1.0,
|
||||
}
|
||||
}
|
||||
|
||||
fn state() -> PhysicsAirState {
|
||||
PhysicsAirState {
|
||||
centre_of_mass_trajectory: AirTrajectory {
|
||||
position: [90.0; 4],
|
||||
velocity: [80.0; 4],
|
||||
acceleration: [70.0; 4],
|
||||
scalar_48: 60.0,
|
||||
},
|
||||
landing_normal: [9.0; 4],
|
||||
time_in_state: 9.0,
|
||||
start_y: 9.0,
|
||||
max_y: 9.0,
|
||||
reached_apex: true,
|
||||
use_centre_of_mass_velocity: false,
|
||||
selector_latch_174: true,
|
||||
trajectory_query_countdown: 9,
|
||||
}
|
||||
}
|
||||
|
||||
fn reckoning() -> PhysicsAirReckoningFields {
|
||||
PhysicsAirReckoningFields {
|
||||
current_landing_normal_1152: [0.0, 1.0, 0.0, 0.0],
|
||||
collision_reference_normal_1216: [0.0, 0.0, 1.0, 0.0],
|
||||
body_spin_angle_1568: 2.0,
|
||||
body_spin_speed_1572: 3.0,
|
||||
}
|
||||
}
|
||||
|
||||
fn settings() -> PhysicsAirSettings {
|
||||
PhysicsAirSettings {
|
||||
body_spin_over_time_320: PointGraph {
|
||||
x: [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0],
|
||||
y: [2.0; 8],
|
||||
},
|
||||
landing_normal_blend_388: 0.25,
|
||||
body_spin_scale_428: 4.0,
|
||||
landing_normal_angle_limit_444: 1.0,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enter_preserves_order_and_offboard_velocity_rule() {
|
||||
let mut state = state();
|
||||
let frame = frame();
|
||||
let mut runtime = FakeRuntime {
|
||||
board_height: 6.0,
|
||||
..Default::default()
|
||||
};
|
||||
enter(&mut state, &frame, [0.0, -9.0, 0.0, 0.0], &mut runtime);
|
||||
|
||||
assert_eq!(
|
||||
runtime.events,
|
||||
[
|
||||
"collision-enabled:true",
|
||||
"collision-state:7",
|
||||
"ik:true",
|
||||
"collision-state:6",
|
||||
"angular-drive",
|
||||
"footplant-240:false",
|
||||
"footplant-reset",
|
||||
"board-height",
|
||||
"heading",
|
||||
]
|
||||
);
|
||||
assert!(state.use_centre_of_mass_velocity);
|
||||
assert_eq!(
|
||||
state.centre_of_mass_trajectory.velocity,
|
||||
[2.0, 3.0, 4.0, 0.0]
|
||||
);
|
||||
assert_eq!(state.landing_normal, [0.0, 1.0, 0.0, 0.0]);
|
||||
assert_eq!((state.start_y, state.max_y), (4.0, 6.0));
|
||||
assert!(!state.reached_apex && !state.selector_latch_174);
|
||||
assert_eq!(state.trajectory_query_countdown, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn category_400_can_select_com_without_requesting_heading() {
|
||||
let mut state = state();
|
||||
let mut frame = frame();
|
||||
frame.previous_physics_category_2516 = 400;
|
||||
frame.previous_physics_state_2504 = 100;
|
||||
let mut runtime = FakeRuntime::default();
|
||||
|
||||
enter(&mut state, &frame, [0.0; 4], &mut runtime);
|
||||
assert!(state.use_centre_of_mass_velocity);
|
||||
assert!(!runtime.events.iter().any(|event| event == "heading"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn jump_velocity_uses_current_vertical_and_clamped_current_horizontal() {
|
||||
let mut frame = frame();
|
||||
frame.current_velocity_400 = [10.0, 4.0, 0.0, 7.0];
|
||||
frame.jump_velocity_848 = [2.0, 10.0, 0.0, 5.0];
|
||||
let mut runtime = FakeRuntime {
|
||||
forced_squared_length: Some(64.0),
|
||||
forced_length: Some(2.0),
|
||||
forced_clamp: Some([2.0, 0.0, 0.0, 99.0]),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let result = calculate_velocity_from_jump(&frame, 2, &mut runtime);
|
||||
assert_eq!(result, [2.0, 4.0, 0.0, 5.0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_runs_com_launch_and_fixed_step_in_native_order() {
|
||||
let mut state = state();
|
||||
state.use_centre_of_mass_velocity = true;
|
||||
state.selector_latch_174 = false;
|
||||
state.trajectory_query_countdown = 0;
|
||||
state.time_in_state = 0.25;
|
||||
state.max_y = 5.0;
|
||||
state.centre_of_mass_trajectory = AirTrajectory {
|
||||
position: [1.0, 2.0, 3.0, 0.0],
|
||||
velocity: [4.0, 5.0, 6.0, 0.0],
|
||||
acceleration: [0.0, -6.0, 0.0, 0.0],
|
||||
scalar_48: -1.0,
|
||||
};
|
||||
let mut frame = frame();
|
||||
frame.frames_since_jump_correction_2576 = 12;
|
||||
let mut runtime = FakeRuntime {
|
||||
board_height: 7.0,
|
||||
selector_normal: Some([0.0, 0.8, 0.2, 0.0]),
|
||||
reckoning_output: PhysicsAirReckoningFields {
|
||||
collision_reference_normal_1216: [1.0, 0.0, 0.0, 0.0],
|
||||
..reckoning()
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
update(
|
||||
&mut state,
|
||||
&frame,
|
||||
&settings(),
|
||||
&mut reckoning(),
|
||||
[0.0, -6.0, 0.0, 0.0],
|
||||
&mut runtime,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
runtime.events,
|
||||
[
|
||||
"air-collision",
|
||||
"launch-construct",
|
||||
"launch-fill",
|
||||
"launch-fill",
|
||||
"launch",
|
||||
"selector-update",
|
||||
"angle",
|
||||
"reckoning",
|
||||
"known-air",
|
||||
"steering-tilt:0",
|
||||
"collision-force",
|
||||
"board-error",
|
||||
"board-height",
|
||||
]
|
||||
);
|
||||
let launch = runtime.launched.unwrap();
|
||||
assert_eq!(launch.jumped_writes, [true, false]);
|
||||
assert_eq!(launch.trajectory_override, [10.0, 19.35, 30.0, 40.0]);
|
||||
assert_eq!(launch.board_override, [10.0, 19.2, 30.0, 40.0]);
|
||||
assert_eq!(launch.cone_angles, [0.8, 1.6]);
|
||||
assert!(launch.use_override);
|
||||
assert_eq!(launch.trajectory_count, 5);
|
||||
assert_eq!(state.trajectory_query_countdown, 7);
|
||||
assert_eq!(state.max_y, 7.0);
|
||||
assert_eq!(state.time_in_state, 0.35);
|
||||
assert_eq!(
|
||||
runtime.collision_reference,
|
||||
Some([1.0, 0.0, 0.0, 0.0]),
|
||||
"Board collision used the pre-update reckoning normal"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skateboard_collision_force_wins_over_direct_velocity_correction() {
|
||||
let state = state();
|
||||
let force = AirBoardForce {
|
||||
force_world: [1.0, 2.0, 3.0, 4.0],
|
||||
point_board_local: [5.0, 6.0, 7.0, 8.0],
|
||||
};
|
||||
let mut runtime = FakeRuntime {
|
||||
collision: true,
|
||||
collision_force: force,
|
||||
..Default::default()
|
||||
};
|
||||
let mut frame = frame();
|
||||
frame.frames_since_jump_correction_2576 = 0;
|
||||
|
||||
update_skateboard(&state, &frame, &reckoning(), &mut runtime);
|
||||
assert_eq!(runtime.enqueued_force, Some((15, force)));
|
||||
assert_eq!(runtime.set_velocity, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skateboard_miss_applies_jump_velocity_only_before_frame_twelve() {
|
||||
let mut state = state();
|
||||
state.use_centre_of_mass_velocity = false;
|
||||
let mut frame = frame();
|
||||
frame.frames_since_jump_correction_2576 = 2;
|
||||
frame.current_velocity_400 = [2.0, 4.0, 4.0, 0.0];
|
||||
frame.jump_velocity_848 = [2.0, 10.0, 4.0, 0.0];
|
||||
let mut runtime = FakeRuntime {
|
||||
forced_squared_length: Some(0.0),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
update_skateboard(&state, &frame, &reckoning(), &mut runtime);
|
||||
assert_eq!(runtime.set_velocity, Some([2.0, 4.0, 4.0, 0.0]));
|
||||
assert_eq!(
|
||||
runtime.events,
|
||||
["steering-tilt:0", "collision-force", "set-board-velocity"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn post_physics_latches_only_strict_downward_velocity_then_checks_wipeout() {
|
||||
let mut state = state();
|
||||
state.reached_apex = false;
|
||||
let mut runtime = FakeRuntime {
|
||||
board_velocity: [0.0, -0.0, 0.0, 0.0],
|
||||
..Default::default()
|
||||
};
|
||||
update_post_physics(&mut state, &mut runtime);
|
||||
assert!(!state.reached_apex);
|
||||
runtime.board_velocity[1] = -0.01;
|
||||
update_post_physics(&mut state, &mut runtime);
|
||||
assert!(state.reached_apex);
|
||||
assert_eq!(
|
||||
runtime.events,
|
||||
[
|
||||
"board-velocity",
|
||||
"wipeout:false",
|
||||
"board-velocity",
|
||||
"wipeout:false"
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exit_and_output_publish_only_recovered_fields() {
|
||||
let mut state = state();
|
||||
state.max_y = 9.0;
|
||||
state.start_y = 4.0;
|
||||
state.reached_apex = true;
|
||||
state.selector_latch_174 = true;
|
||||
state.landing_normal = [1.0, 2.0, 3.0, 4.0];
|
||||
let mut reckoning = reckoning();
|
||||
|
||||
let output = fill_physics_output(&state);
|
||||
assert_eq!(output.jump_height, 5.0);
|
||||
assert_eq!(output.scalar_184_write.map(f32::to_bits), Some(0x4080_0000));
|
||||
assert_eq!(output.landing_normal, [1.0, 2.0, 3.0, 4.0]);
|
||||
|
||||
exit(&mut state, &mut reckoning);
|
||||
assert!(!state.use_centre_of_mass_velocity);
|
||||
assert_eq!(reckoning.body_spin_angle_1568, 0.0);
|
||||
assert_eq!(reckoning.body_spin_speed_1572, 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn angle_wrap_uses_recovered_turn_constants() {
|
||||
assert_eq!(wrap_signed_angle(0.0).to_bits(), 0.0_f32.to_bits());
|
||||
assert!(wrap_signed_angle(4.0) < 0.0);
|
||||
assert!(wrap_signed_angle(2.0) > 0.0);
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
//! TU3 PhysicsAir Update (`0x82D346C0`) and COM launch helper (`0x82D34570`).
|
||||
|
||||
use crate::point_graph::PointGraph;
|
||||
|
||||
use super::{
|
||||
board::update_skateboard,
|
||||
data::{
|
||||
AirTrajectory, PhysicsAirFrame, PhysicsAirReckoningFields, PhysicsAirSettings,
|
||||
PhysicsAirState, Vector4,
|
||||
},
|
||||
jump_velocity::calculate_velocity_from_jump,
|
||||
runtime::{PhysicsAirLaunchInfo, PhysicsAirRuntime},
|
||||
};
|
||||
|
||||
const FIXED_STEP: f32 = f32::from_bits(0x3C88_8889); // 1/60, 0x82098D40
|
||||
const HALF: f32 = f32::from_bits(0x3F00_0000);
|
||||
const ONE: f32 = f32::from_bits(0x3F80_0000);
|
||||
const TRAJECTORY_UNBOUNDED: f32 = f32::from_bits(0xBF80_0000);
|
||||
const TWO: f32 = f32::from_bits(0x4000_0000);
|
||||
const DEGREES_TO_RADIANS: f32 = f32::from_bits(0x3C8E_FA35);
|
||||
const TWO_PI: f32 = f32::from_bits(0x40C9_0FDB);
|
||||
const RECIPROCAL_TWO_PI: f32 = f32::from_bits(0x3E22_F983);
|
||||
const HALF_TURN: f32 = f32::from_bits(0x3F00_0000);
|
||||
const COM_START_Y_OFFSET: f32 = f32::from_bits(0xBF26_6666); // -0.65
|
||||
const BOARD_START_Y_OFFSET: f32 = f32::from_bits(0xBF4C_CCCD); // -0.8
|
||||
const CONE_SCALE: f32 = f32::from_bits(0x3ECC_CCCD); // 0.4
|
||||
|
||||
/// Full recovered outer update, retaining native effect order.
|
||||
pub fn update(
|
||||
state: &mut PhysicsAirState,
|
||||
frame: &PhysicsAirFrame,
|
||||
settings: &PhysicsAirSettings,
|
||||
reckoning: &mut PhysicsAirReckoningFields,
|
||||
world_acceleration: Vector4,
|
||||
runtime: &mut impl PhysicsAirRuntime,
|
||||
) {
|
||||
runtime.update_air_collision();
|
||||
|
||||
state.trajectory_query_countdown = state.trajectory_query_countdown.wrapping_sub(1);
|
||||
if state.trajectory_query_countdown <= 0
|
||||
&& frame.flags_2468 & 0x0000_8000 == 0
|
||||
&& frame.previous_physics_state_2504 != 0
|
||||
&& !runtime.trajectory_query_just_started()
|
||||
{
|
||||
launch_trajectory(state, frame, world_acceleration, runtime);
|
||||
state.trajectory_query_countdown = 7;
|
||||
}
|
||||
|
||||
runtime.update_trajectory_selector();
|
||||
|
||||
let current_normal = reckoning.current_landing_normal_1152;
|
||||
let landing_normal = runtime.selector_landing_normal().unwrap_or(current_normal);
|
||||
state.landing_normal = landing_normal;
|
||||
|
||||
let raw_angle = runtime.angle_between_vectors(landing_normal, current_normal);
|
||||
let wrapped_angle = wrap_signed_angle(raw_angle);
|
||||
let mut normal_blend = if wrapped_angle <= settings.landing_normal_angle_limit_444 {
|
||||
settings.landing_normal_blend_388
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
if !state.use_centre_of_mass_velocity {
|
||||
if !state.selector_latch_174 {
|
||||
state.selector_latch_174 = runtime.selector_centre_of_mass_trajectory_ready();
|
||||
}
|
||||
if state.selector_latch_174 {
|
||||
state.use_centre_of_mass_velocity = true;
|
||||
state.centre_of_mass_trajectory = AirTrajectory {
|
||||
position: frame.trajectory_position_592,
|
||||
velocity: frame.trajectory_velocity_608,
|
||||
acceleration: world_acceleration,
|
||||
scalar_48: TRAJECTORY_UNBOUNDED,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if frame.state_timer_2664 <= 0.0 {
|
||||
normal_blend = 0.0;
|
||||
} else if frame.flags_2468 & 0x0000_4000 != 0 {
|
||||
state.use_centre_of_mass_velocity = false;
|
||||
}
|
||||
|
||||
let scaled_spin_input = frame.body_spin_input_2640 * settings.body_spin_scale_428;
|
||||
let spin_radians = scaled_spin_input * DEGREES_TO_RADIANS;
|
||||
let curve = evaluate_spin_curve(&settings.body_spin_over_time_320, state.time_in_state * TWO);
|
||||
let target_body_spin = curve * spin_radians;
|
||||
*reckoning =
|
||||
runtime.update_reckoning_air_states(landing_normal, normal_blend, target_body_spin, 0.0);
|
||||
|
||||
if state.use_centre_of_mass_velocity {
|
||||
integrate_trajectory_fixed_step(&mut state.centre_of_mass_trajectory);
|
||||
runtime.update_known_air_skeleton(state.centre_of_mass_trajectory.position);
|
||||
} else {
|
||||
runtime.update_animated_skateboard_skeleton(false);
|
||||
}
|
||||
|
||||
update_skateboard(state, frame, reckoning, runtime);
|
||||
runtime.enable_skateboard_error_on_skeleton();
|
||||
|
||||
let board_y = runtime.board_transform_height();
|
||||
// fcmpu+bgt keeps the old value only for an ordered old > new result;
|
||||
// unordered values take the board-height write.
|
||||
if !(state.max_y > board_y) {
|
||||
state.max_y = board_y;
|
||||
}
|
||||
state.time_in_state += frame.delta_time_2604;
|
||||
}
|
||||
|
||||
/// Scalar wrap at `0x82D347E8..0x82D34848`, after AngleBetweenVectors.
|
||||
pub fn wrap_signed_angle(angle: f32) -> f32 {
|
||||
let turns = angle * RECIPROCAL_TWO_PI;
|
||||
let fraction = turns - turns.floor();
|
||||
let signed_fraction = if fraction > HALF_TURN {
|
||||
fraction - ONE
|
||||
} else {
|
||||
fraction
|
||||
};
|
||||
signed_fraction * TWO_PI
|
||||
}
|
||||
|
||||
/// Fixed 1/60 ballistic step at `0x82D349B8..0x82D34A20`.
|
||||
pub fn integrate_trajectory_fixed_step(trajectory: &mut AirTrajectory) {
|
||||
let step_squared = FIXED_STEP * FIXED_STEP;
|
||||
let linear_position: Vector4 = core::array::from_fn(|lane| {
|
||||
trajectory.velocity[lane].mul_add(FIXED_STEP, trajectory.position[lane])
|
||||
});
|
||||
let next_velocity: Vector4 = core::array::from_fn(|lane| {
|
||||
trajectory.acceleration[lane].mul_add(FIXED_STEP, trajectory.velocity[lane])
|
||||
});
|
||||
let half = ONE * HALF;
|
||||
let half_acceleration = trajectory.acceleration.map(|lane| lane * half);
|
||||
let next_position: Vector4 = core::array::from_fn(|lane| {
|
||||
half_acceleration[lane].mul_add(step_squared, linear_position[lane])
|
||||
});
|
||||
trajectory.position = next_position;
|
||||
trajectory.velocity = next_velocity;
|
||||
}
|
||||
|
||||
fn launch_trajectory(
|
||||
state: &PhysicsAirState,
|
||||
frame: &PhysicsAirFrame,
|
||||
world_acceleration: Vector4,
|
||||
runtime: &mut impl PhysicsAirRuntime,
|
||||
) {
|
||||
let mut info = runtime.construct_trajectory_launch_info();
|
||||
runtime.fill_skeleton_launch_info(&mut info);
|
||||
if frame.frames_since_jump_correction_2576 < 12 {
|
||||
let velocity =
|
||||
calculate_velocity_from_jump(frame, frame.frames_since_jump_correction_2576, runtime);
|
||||
info.set_start_velocity(velocity);
|
||||
} else if state.use_centre_of_mass_velocity {
|
||||
adjust_centre_of_mass_launch_info(state, world_acceleration, runtime, &mut info);
|
||||
}
|
||||
|
||||
// The caller always clears +268, including after the COM helper sets it.
|
||||
info.set_player_jumped(false);
|
||||
runtime.launch_trajectory(&info);
|
||||
}
|
||||
|
||||
fn adjust_centre_of_mass_launch_info<R: PhysicsAirRuntime>(
|
||||
state: &PhysicsAirState,
|
||||
world_acceleration: Vector4,
|
||||
runtime: &mut R,
|
||||
info: &mut R::LaunchInfo,
|
||||
) {
|
||||
runtime.fill_skeleton_launch_info(info);
|
||||
|
||||
let velocity = core::array::from_fn(|lane| {
|
||||
world_acceleration[lane].mul_add(FIXED_STEP, state.centre_of_mass_trajectory.velocity[lane])
|
||||
});
|
||||
info.set_start_velocity(velocity);
|
||||
info.set_player_jumped(true);
|
||||
|
||||
let animation_position = info.centre_of_mass_animation_position();
|
||||
let trajectory_offset = [0.0, COM_START_Y_OFFSET, 0.0, 0.0];
|
||||
let board_offset = [0.0, BOARD_START_Y_OFFSET, 0.0, 0.0];
|
||||
info.set_trajectory_start_position_override(core::array::from_fn(|lane| {
|
||||
animation_position[lane] + trajectory_offset[lane]
|
||||
}));
|
||||
info.set_board_position_override(core::array::from_fn(|lane| {
|
||||
animation_position[lane] + board_offset[lane]
|
||||
}));
|
||||
info.set_use_trajectory_start_position_override(true);
|
||||
info.set_trajectory_count(5);
|
||||
|
||||
let cone_angles = info.cone_angles();
|
||||
info.set_cone_angles([cone_angles[0] * CONE_SCALE, cone_angles[1] * CONE_SCALE]);
|
||||
}
|
||||
|
||||
fn evaluate_spin_curve(graph: &PointGraph<8>, input: f32) -> f32 {
|
||||
graph.evaluate(input)
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
//! Original TU3 trajectory launch calculations82D67D08/82D67320/82D682E8.
|
||||
use super::{LaunchInfo, QueryRequest, SelectorInput, SelectorSettings, Trajectory, math::*};
|
||||
|
||||
pub(super) fn adjust_velocity(
|
||||
info: &mut LaunchInfo,
|
||||
input: SelectorInput,
|
||||
s: &SelectorSettings,
|
||||
) -> bool {
|
||||
let n = input.ground_normal;
|
||||
let v = info.start_velocity;
|
||||
let mut normal = scale(n, dot(v, n));
|
||||
normal[1] = 0.0;
|
||||
let combined = add([0.0, v[1], 0.0, 0.0], normal);
|
||||
let residual = sub(v, combined);
|
||||
let lean = (input.directional_input - 0.25).max(-1.0).min(1.0);
|
||||
let aligned = normalize(sub(UP, scale(n, lean * s.vert_jump_align_max_angle)));
|
||||
if n[1] >= s.vert_jump_align_max_ground_normal_y {
|
||||
return false;
|
||||
}
|
||||
if normalize(combined)[1] > s.vert_jump_align_min_direction_y {
|
||||
//The second term really uses UNIT velocity, not the original speed.
|
||||
let left = scale(
|
||||
add(scale(aligned, length(combined)), residual),
|
||||
s.vert_jump_align_factor,
|
||||
);
|
||||
let right = scale(normalize(v), 1.0 - s.vert_jump_align_factor);
|
||||
info.start_velocity = scale(normalize(add(left, right)), length(v));
|
||||
true
|
||||
} else {
|
||||
if !info.player_jumped && input.previous_physics_state != 200 {
|
||||
let normal = scale(n, dot(v, n));
|
||||
info.start_velocity = add(
|
||||
scale(normal, s.natural_air_off_verts_scalar),
|
||||
sub(v, normal),
|
||||
);
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn candidate_velocities(info: LaunchInfo, s: &SelectorSettings) -> Vec<Vector> {
|
||||
let count = usize::from(info.trajectory_count.min(7));
|
||||
let mut velocities = Vec::with_capacity(count);
|
||||
if count == 0 {
|
||||
return velocities;
|
||||
}
|
||||
let v = info.start_velocity;
|
||||
velocities.push(v);
|
||||
let right = normalize(cross(UP, v));
|
||||
let forward = normalize(cross(right, UP));
|
||||
let speed = length(v);
|
||||
let cone_speed = if info.player_jumped {
|
||||
length(info.com_velocity)
|
||||
} else {
|
||||
speed
|
||||
};
|
||||
let radians = f32::from_bits(0x3c8e_fa35);
|
||||
let x = info.cone_angle_x * radians;
|
||||
let z = (s.cone_angle_z_vs_speed.evaluate(cone_speed * 0.05) * info.cone_angle_z) * radians;
|
||||
let cone_speed = cone_speed.max(s.speed_factor_min).min(s.speed_factor_max);
|
||||
let right = scale(right, crate::trigonometry::sin(x) * cone_speed);
|
||||
let forward = scale(forward, crate::trigonometry::sin(z) * cone_speed);
|
||||
if count > 1 {
|
||||
let step = f32::from_bits(0x40c9_0fdb) / (count - 1) as f32;
|
||||
let mut angle = 0.0;
|
||||
for _ in 1..count {
|
||||
let candidate = madd(
|
||||
forward,
|
||||
crate::trigonometry::cos(angle),
|
||||
madd(right, crate::trigonometry::sin(angle), v),
|
||||
);
|
||||
velocities.push(scale(normalize(candidate), length(candidate).min(speed)));
|
||||
angle += step;
|
||||
}
|
||||
}
|
||||
velocities
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(super) struct LaunchBatch {
|
||||
pub requests: Vec<QueryRequest>,
|
||||
pub velocities: Vec<Vector>,
|
||||
pub origin: Vector, //2864
|
||||
pub board_position: Vector, //2848
|
||||
pub local_board_position: Vector, //2832
|
||||
pub local_com_position: Vector, //2784
|
||||
pub com_displacement: Vector, //2768
|
||||
}
|
||||
pub(super) fn batch(info: LaunchInfo, input: SelectorInput, s: &SelectorSettings) -> LaunchBatch {
|
||||
let velocities = candidate_velocities(info, s);
|
||||
let ground = s
|
||||
.displacement_vs_ground_normal
|
||||
.evaluate(input.ground_normal[1].abs());
|
||||
let speed = s
|
||||
.displacement_vs_speed
|
||||
.evaluate(input.board_vertical_velocity * 0.1);
|
||||
let blend = speed.max(ground);
|
||||
let height = dot(
|
||||
sub(info.animation_com_position, input.contact_position),
|
||||
info.reckoning_transform[1],
|
||||
) - s.trajectory_radius;
|
||||
let displacement =
|
||||
height.min(s.trajectory_displacement) * (1.0 - blend) + s.trajectory_displacement * blend;
|
||||
let mut origin = sub(
|
||||
info.animation_com_position,
|
||||
scale(info.reckoning_transform[1], displacement),
|
||||
);
|
||||
let mut board_position = info.board_position;
|
||||
if info.use_position_override {
|
||||
origin = info.start_position_override;
|
||||
board_position = info.board_position_override;
|
||||
}
|
||||
let com_displacement = sub(info.animation_com_position, origin);
|
||||
let duration = max_time(info.start_velocity, s);
|
||||
let requests = velocities
|
||||
.iter()
|
||||
.map(|&velocity| QueryRequest {
|
||||
trajectory: Trajectory {
|
||||
position: madd(velocity, info.timestep, origin),
|
||||
velocity,
|
||||
acceleration: input.gravity,
|
||||
duration,
|
||||
},
|
||||
radius: s.trajectory_radius,
|
||||
start_error: s.trajectory_error_start,
|
||||
end_error: s.trajectory_error_end,
|
||||
})
|
||||
.collect();
|
||||
LaunchBatch {
|
||||
requests,
|
||||
velocities,
|
||||
origin,
|
||||
board_position,
|
||||
com_displacement,
|
||||
local_board_position: transform(info.reckoning_inverse, sub(info.board_position, origin)),
|
||||
local_com_position: transform(info.reckoning_inverse, com_displacement),
|
||||
}
|
||||
}
|
||||
///82D68698 deliberately uses the original -19.6 and -1/9.8 coefficients.
|
||||
fn max_time(velocity: Vector, s: &SelectorSettings) -> f32 {
|
||||
let square = velocity[1].mul_add(
|
||||
velocity[1],
|
||||
-(s.trajectory_max_drop * f32::from_bits(0xc19c_cccd)),
|
||||
);
|
||||
let root = if square == 0.0 {
|
||||
0.0
|
||||
} else {
|
||||
square * inverse_length(square)
|
||||
};
|
||||
((-velocity[1] - root) * f32::from_bits(0xbdd0_fac6)).min(s.trajectory_max_time)
|
||||
}
|
||||
|
||||
///tTrajectory::AdjustTrajectory82D609E0 changes velocity only.
|
||||
pub(super) fn adjust_trajectory(
|
||||
trajectory: &mut Trajectory,
|
||||
frame: i32,
|
||||
adjustment: Vector,
|
||||
maximum: f32,
|
||||
) {
|
||||
if frame <= 0 {
|
||||
return;
|
||||
}
|
||||
let correction = scale(adjustment, reciprocal(frame as f32 * STEP));
|
||||
let scalar = if dot(correction, correction) > maximum * maximum {
|
||||
maximum / length(correction)
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
trajectory.velocity = madd(correction, scalar, trajectory.velocity);
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
//! Independent PC arithmetic retaining the recovered refinement sequences.
|
||||
use crate::physics::native_arithmetic;
|
||||
pub(super) type Vector = [f32; 4];
|
||||
pub(super) type Transform = [Vector; 4];
|
||||
pub(super) const ZERO: Vector = [0.0; 4];
|
||||
pub(super) const UP: Vector = [0.0, 1.0, 0.0, 0.0];
|
||||
pub(super) const STEP: f32 = f32::from_bits(0x3c88_8889);
|
||||
pub(super) const IDENTITY: Transform = [[1.0, 0.0, 0.0, 0.0], UP, [0.0, 0.0, 1.0, 0.0], ZERO];
|
||||
pub(super) fn dot(a: Vector, b: Vector) -> f32 {
|
||||
native_arithmetic::dot3(a, b)
|
||||
}
|
||||
pub(super) fn add(a: Vector, b: Vector) -> Vector {
|
||||
core::array::from_fn(|i| a[i] + b[i])
|
||||
}
|
||||
pub(super) fn sub(a: Vector, b: Vector) -> Vector {
|
||||
core::array::from_fn(|i| a[i] - b[i])
|
||||
}
|
||||
pub(super) fn scale(a: Vector, s: f32) -> Vector {
|
||||
a.map(|v| v * s)
|
||||
}
|
||||
pub(super) fn madd(a: Vector, s: f32, b: Vector) -> Vector {
|
||||
core::array::from_fn(|i| a[i].mul_add(s, b[i]))
|
||||
}
|
||||
pub(super) fn cross(a: Vector, b: Vector) -> Vector {
|
||||
[
|
||||
(-a[2]).mul_add(b[1], a[1] * b[2]),
|
||||
(-a[0]).mul_add(b[2], a[2] * b[0]),
|
||||
(-a[1]).mul_add(b[0], a[0] * b[1]),
|
||||
0.0,
|
||||
]
|
||||
}
|
||||
pub(super) fn inverse_length(square: f32) -> f32 {
|
||||
let mut v = native_arithmetic::reciprocal_square_root_estimate(square);
|
||||
for _ in 0..2 {
|
||||
v = (v * 0.5).mul_add((-square).mul_add(v * v, 1.0), v);
|
||||
}
|
||||
v
|
||||
}
|
||||
pub(super) fn reciprocal(value: f32) -> f32 {
|
||||
let mut inverse = native_arithmetic::reciprocal_estimate(value);
|
||||
for _ in 0..2 {
|
||||
inverse = inverse.mul_add((-inverse).mul_add(value, 1.0), inverse);
|
||||
}
|
||||
inverse
|
||||
}
|
||||
///8296EBB0: squared-length gate, ONE refinement, clamped dot, native acos.
|
||||
pub(super) fn angle_between(a: Vector, b: Vector) -> f32 {
|
||||
let aa = dot(a, a);
|
||||
let bb = dot(b, b);
|
||||
if !(aa > f32::from_bits(0x38d1_b717) && bb > f32::from_bits(0x38d1_b717)) {
|
||||
return 0.0;
|
||||
}
|
||||
let unit = |v: Vector, square: f32| {
|
||||
let r = native_arithmetic::reciprocal_square_root_estimate(square);
|
||||
scale(v, (r * 0.5).mul_add((-square).mul_add(r * r, 1.0), r))
|
||||
};
|
||||
crate::trigonometry::acos(dot(unit(a, aa), unit(b, bb)).max(-1.0).min(1.0))
|
||||
}
|
||||
pub(super) fn length(a: Vector) -> f32 {
|
||||
let square = dot(a, a);
|
||||
if square == 0.0 {
|
||||
0.0
|
||||
} else {
|
||||
square * inverse_length(square)
|
||||
}
|
||||
}
|
||||
pub(super) fn normalize(a: Vector) -> Vector {
|
||||
let square = dot(a, a);
|
||||
let inverse = inverse_length(square);
|
||||
let length = if square == 0.0 { 0.0 } else { square * inverse };
|
||||
//830BD350's initializer82F826F8 broadcasts82181A88 (1e-6).
|
||||
if length > f32::from_bits(0x3586_37bd) {
|
||||
scale(a, inverse)
|
||||
} else {
|
||||
ZERO
|
||||
}
|
||||
}
|
||||
pub(super) fn transform(m: Transform, a: Vector) -> Vector {
|
||||
madd(m[2], a[2], madd(m[1], a[1], madd(m[0], a[0], m[3])))
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
//! TU3 trajectory selection82D67848/82D682E8/82D68800.
|
||||
//! Host world storage and batch ownership are independent of console layout.
|
||||
mod launch;
|
||||
mod math;
|
||||
mod prediction;
|
||||
mod query;
|
||||
mod scoring;
|
||||
mod selector;
|
||||
mod types;
|
||||
pub use prediction::{Prediction, QueryRequest, QueryResult, Trajectory};
|
||||
pub use query::{SurfaceHit, query_trajectory};
|
||||
pub use selector::{Selection, TrajectorySelector, WorldWithoutGrindEdges};
|
||||
pub use types::{LaunchInfo, SelectorInput, SelectorSettings};
|
||||
@@ -0,0 +1,90 @@
|
||||
//! tTrajectory and complete PredictionResults8276C9D8/8276DBE0.
|
||||
use super::math::{IDENTITY, STEP, Transform, UP, Vector, ZERO, reciprocal};
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct Trajectory {
|
||||
pub position: Vector, //0 metres
|
||||
pub velocity: Vector, //16 metres/second
|
||||
pub acceleration: Vector, //32 metres/second squared
|
||||
pub duration: f32, //48 seconds; -1 means unbounded
|
||||
}
|
||||
impl Trajectory {
|
||||
pub fn position_at(self, time: f32) -> Vector {
|
||||
let square = time * time;
|
||||
core::array::from_fn(|i| {
|
||||
(self.acceleration[i] * 0.5)
|
||||
.mul_add(square, self.velocity[i].mul_add(time, self.position[i]))
|
||||
})
|
||||
}
|
||||
pub fn velocity_at(self, time: f32) -> Vector {
|
||||
core::array::from_fn(|i| self.acceleration[i].mul_add(time, self.velocity[i]))
|
||||
}
|
||||
pub fn highest_position(self) -> (Vector, f32) {
|
||||
//82D2CCA0: downward velocity or nonnegative gravity has no apex.
|
||||
if self.velocity[1] < 0.0 || self.acceleration[1] >= 0.0 {
|
||||
(self.position, 0.0)
|
||||
} else {
|
||||
let t = self.velocity[1] * reciprocal(-self.acceleration[1]);
|
||||
(self.position_at(t), t)
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct QueryRequest {
|
||||
pub trajectory: Trajectory, //request144..207
|
||||
pub radius: f32, //208 metres
|
||||
pub start_error: f32, //212 multiplier of radius
|
||||
pub end_error: f32, //216 multiplier of radius
|
||||
}
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct QueryResult {
|
||||
pub contact_position: Vector, //0
|
||||
pub contact_normal: Vector, //16 first hit face normal
|
||||
pub landing_normal: Vector, //32 velocity-qualified nearby-face average
|
||||
pub contact_time: f32, //48 seconds; -1 is no contact
|
||||
pub contact_transform: Transform, //64 static world identity
|
||||
pub contact_frame: i32, //128
|
||||
pub surface: u32, //132 original surface data
|
||||
pub geometry: u32, //136 host geometry identity
|
||||
}
|
||||
impl QueryResult {
|
||||
pub const fn miss() -> Self {
|
||||
Self {
|
||||
contact_position: ZERO,
|
||||
contact_normal: UP,
|
||||
landing_normal: UP,
|
||||
contact_time: -1.0,
|
||||
contact_transform: IDENTITY,
|
||||
contact_frame: -1,
|
||||
surface: 0,
|
||||
geometry: 0,
|
||||
}
|
||||
}
|
||||
pub fn valid(self) -> bool {
|
||||
self.contact_time >= 0.0
|
||||
}
|
||||
///PredictionResults::GetLandingNormal82D2D9E8.
|
||||
pub fn suggested_normal(self) -> Vector {
|
||||
if self.valid() {
|
||||
self.landing_normal
|
||||
} else {
|
||||
UP
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct Prediction {
|
||||
pub result: QueryResult,
|
||||
pub request: QueryRequest,
|
||||
}
|
||||
impl Prediction {
|
||||
pub fn collision_position(self) -> Vector {
|
||||
self.request
|
||||
.trajectory
|
||||
.position_at(self.result.contact_time)
|
||||
}
|
||||
pub fn collision_velocity(self) -> Vector {
|
||||
self.request
|
||||
.trajectory
|
||||
.velocity_at(self.result.contact_frame as f32 * STEP)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
//! Complete trajectory-segment walk82770910 and result writer8276DBE0.
|
||||
//! The host supplies real nearest swept-line hits and intersecting triangles.
|
||||
use super::{QueryRequest, QueryResult, math::*};
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct SurfaceHit {
|
||||
pub position: Vector,
|
||||
pub normal: Vector,
|
||||
pub transform: Transform,
|
||||
pub surface: u32,
|
||||
pub geometry: u32,
|
||||
}
|
||||
|
||||
pub fn query_trajectory<E>(
|
||||
request: QueryRequest,
|
||||
mut line: impl FnMut(Vector, Vector, f32) -> Result<Option<SurfaceHit>, E>,
|
||||
mut nearby: impl FnMut(Vector, f32) -> Result<Vec<[Vector; 3]>, E>,
|
||||
) -> Result<QueryResult, E> {
|
||||
let trajectory = request.trajectory;
|
||||
let mut start_step = trajectory.duration;
|
||||
let mut end_step = trajectory.duration;
|
||||
if trajectory.acceleration[1] < 0.0 {
|
||||
start_step = step_size(
|
||||
trajectory.acceleration[1],
|
||||
request.start_error * request.radius,
|
||||
);
|
||||
end_step = step_size(
|
||||
trajectory.acceleration[1],
|
||||
request.end_error * request.radius,
|
||||
);
|
||||
}
|
||||
let minimum_square =
|
||||
((request.start_error * request.radius) * request.start_error) * request.radius;
|
||||
let mut time = 0.0;
|
||||
let mut start = trajectory.position_at(time);
|
||||
let mut end = trajectory.position_at(time + start_step);
|
||||
while trajectory.duration > time {
|
||||
let delta = sub(end, start);
|
||||
if dot(delta, delta) > minimum_square {
|
||||
//82770B40 rejects a segment only when all XYZ are tiny.
|
||||
if delta[..3]
|
||||
.iter()
|
||||
.any(|v| v.abs() > f32::from_bits(0x3780_0000))
|
||||
{
|
||||
if let Some(hit) = line(start, end, request.radius)? {
|
||||
let (contact_time, contact_frame) =
|
||||
time_at_contact(request, hit.position, time);
|
||||
let triangles = nearby(hit.position, request.radius)?;
|
||||
let velocity = trajectory.velocity_at(contact_frame as f32 * STEP);
|
||||
return Ok(QueryResult {
|
||||
contact_position: hit.position,
|
||||
contact_normal: hit.normal,
|
||||
landing_normal: average_landing_normal(&triangles, velocity),
|
||||
contact_time,
|
||||
contact_frame,
|
||||
contact_transform: hit.transform,
|
||||
surface: hit.surface,
|
||||
geometry: hit.geometry,
|
||||
});
|
||||
}
|
||||
}
|
||||
start = end;
|
||||
}
|
||||
let fraction = reciprocal(trajectory.duration) * time;
|
||||
let step = end_step.mul_add(fraction, (1.0 - fraction) * start_step);
|
||||
time += step;
|
||||
//The native walk evaluates beyond the horizon; it does not clamp end.
|
||||
end = trajectory.position_at(time + step);
|
||||
}
|
||||
Ok(QueryResult::miss())
|
||||
}
|
||||
|
||||
fn reciprocal(value: f32) -> f32 {
|
||||
let mut inverse = crate::physics::native_arithmetic::reciprocal_estimate(value);
|
||||
for _ in 0..2 {
|
||||
inverse = inverse.mul_add((-inverse).mul_add(value, 1.0), inverse);
|
||||
}
|
||||
inverse
|
||||
}
|
||||
fn step_size(gravity_y: f32, error_radius: f32) -> f32 {
|
||||
let square = reciprocal(gravity_y) * (-8.0 * error_radius);
|
||||
if square == 0.0 {
|
||||
0.0
|
||||
} else {
|
||||
square * inverse_length(square)
|
||||
}
|
||||
}
|
||||
///8276C558 writes both collision seconds and integer frame, including its
|
||||
///distinct negligible-gravity branch. A miss is handled by the caller.
|
||||
fn time_at_contact(request: QueryRequest, position: Vector, mut time: f32) -> (f32, i32) {
|
||||
const EPSILON: f32 = f32::from_bits(0x38d1_b717);
|
||||
let trajectory = request.trajectory;
|
||||
if EPSILON >= (reciprocal(2.0) * trajectory.acceleration[1]).abs() {
|
||||
let distance = length(sub(position, trajectory.position));
|
||||
if !(distance.abs() > EPSILON) {
|
||||
return (0.0, 0);
|
||||
}
|
||||
let speed = length(trajectory.velocity);
|
||||
if !(speed.abs() > EPSILON) {
|
||||
return (0.0, 0);
|
||||
}
|
||||
let frames = reciprocal(speed) * distance;
|
||||
return (frames * STEP, frames.ceil() as i32);
|
||||
}
|
||||
let mut closest_square = f32::MAX;
|
||||
let mut found = false;
|
||||
while trajectory.duration >= time {
|
||||
let delta = sub(position, trajectory.position_at(time));
|
||||
let square = dot(delta, delta);
|
||||
if closest_square >= square {
|
||||
closest_square = square;
|
||||
} else if square > closest_square {
|
||||
time -= STEP;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
time += STEP;
|
||||
}
|
||||
if !found {
|
||||
time = trajectory.duration;
|
||||
}
|
||||
//82F4E948 is ceiling (truncate then add1 for a positive remainder).
|
||||
(time, (reciprocal(STEP) * time).ceil() as i32)
|
||||
}
|
||||
///82771018, using at most64 triangles retained by82772028 in world order.
|
||||
fn average_landing_normal(triangles: &[[Vector; 3]], velocity: Vector) -> Vector {
|
||||
let mut accepted = Vec::with_capacity(64);
|
||||
let mut most_up = ZERO;
|
||||
let mut highest = -1.0;
|
||||
for triangle in triangles.iter().take(64) {
|
||||
let normal = cross(sub(triangle[1], triangle[0]), sub(triangle[2], triangle[0]));
|
||||
//This first stage uses Normalize, not NormalizeSafe.
|
||||
let normal = scale(normal, inverse_length(dot(normal, normal)));
|
||||
if normal[1] > 0.7 || dot(normal, velocity) <= 0.0 {
|
||||
let up = dot(normal, UP);
|
||||
if up > highest {
|
||||
highest = up;
|
||||
most_up = normal;
|
||||
}
|
||||
accepted.push(normal);
|
||||
}
|
||||
}
|
||||
if accepted.is_empty() {
|
||||
return UP;
|
||||
}
|
||||
let mut total = ZERO;
|
||||
for normal in accepted {
|
||||
if dot(most_up, normal) > 0.5 {
|
||||
total = add(total, normal);
|
||||
}
|
||||
}
|
||||
normalize(total)
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
//! TU3 scoring82D68FE8/82D68AF0/82D69790 for a world with no grind edges.
|
||||
//! That topology is supplied by the authored BoardWorld, not inferred from a
|
||||
//! failed collision. Every wall probe still queries real world geometry.
|
||||
use super::{
|
||||
Prediction, SelectorInput, SelectorSettings, SurfaceHit, launch::adjust_trajectory, math::*,
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub(super) struct Candidate {
|
||||
pub prediction: Prediction,
|
||||
pub start_velocity: Vector, //candidate64+0, before wall adjustment
|
||||
pub normal: Vector, //+16
|
||||
pub collision_velocity: Vector, //+32, before wall adjustment
|
||||
pub collision_position: Vector, //+48, before wall adjustment
|
||||
pub score: f32,
|
||||
pub wall_score: f32,
|
||||
pub wall_ride: bool,
|
||||
}
|
||||
pub(super) fn score(
|
||||
candidates: &mut [Candidate],
|
||||
pass: u16,
|
||||
adjusted_on_vert: bool,
|
||||
input: SelectorInput,
|
||||
s: &SelectorSettings,
|
||||
mut line: impl FnMut(Vector, Vector, f32) -> Result<Option<SurfaceHit>, String>,
|
||||
) -> Result<bool, String> {
|
||||
let mut all_miss = true; //selector9662, consumed by PhysicsAir COM latch
|
||||
let mut highest_wall = -1_000_000.0;
|
||||
for (index, c) in candidates.iter_mut().enumerate() {
|
||||
c.score = 0.0;
|
||||
c.wall_score = 0.0;
|
||||
c.wall_ride = false;
|
||||
if !c.prediction.result.valid() {
|
||||
continue;
|
||||
}
|
||||
all_miss = false;
|
||||
if c.prediction.result.contact_frame < s.minimum_trajectory_frames {
|
||||
continue;
|
||||
}
|
||||
let middle = index == 0 && pass == 1;
|
||||
let middle_bonus = if middle && !adjusted_on_vert {
|
||||
s.score_middle_bonus
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
c.collision_position = c.prediction.collision_position();
|
||||
c.normal = c.prediction.result.suggested_normal();
|
||||
c.prediction.result.contact_normal = c.normal;
|
||||
c.collision_velocity = c.prediction.collision_velocity();
|
||||
let force_dot = dot(c.normal, c.collision_velocity);
|
||||
let sideways = length(cross(c.normal, normalize(c.collision_velocity)));
|
||||
let scalar = s.landing_force_scalar.evaluate(force_dot.abs());
|
||||
let force_score = (force_dot * s.score_landing_force) * scalar;
|
||||
let direction_score = (sideways * s.score_landing_direction) * scalar;
|
||||
|
||||
//82D69C00 returns0 immediately for the actual empty edge query. Its
|
||||
//9552 flag remains clear.82D69D68 then evaluates the stock graph at
|
||||
//its native invalid-query distance1000; do not substitute a score.
|
||||
let grind_score = if input.flags_2476 & 0x0200_0000 == 0
|
||||
&& (input.grind_lock_distance > 0.5 || !middle)
|
||||
{
|
||||
s.grind_penalty_vs_distance.evaluate(1000.0)
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
if grind_score <= 0.0 {
|
||||
c.wall_score = wall_score(
|
||||
&mut c.prediction,
|
||||
grind_score < 0.0,
|
||||
s,
|
||||
&mut highest_wall,
|
||||
&mut line,
|
||||
)?;
|
||||
c.wall_ride = c.wall_score > 0.0;
|
||||
if c.wall_score < 0.0 {
|
||||
c.prediction.result.contact_normal = UP;
|
||||
}
|
||||
}
|
||||
let mut surface_score = 0.0;
|
||||
if grind_score <= 1.0 {
|
||||
match (c.prediction.result.surface >> 7) & 31 {
|
||||
6 => {
|
||||
surface_score = s.surface_unrideable_score;
|
||||
c.prediction.result.contact_normal = input.reference_up;
|
||||
}
|
||||
7 => {
|
||||
surface_score = s.surface_dont_align_score;
|
||||
c.prediction.result.contact_normal = input.reference_up;
|
||||
}
|
||||
8 => {
|
||||
surface_score = s.surface_dont_align_score;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
c.normal = c.prediction.result.contact_normal;
|
||||
let mut transition = 0.0;
|
||||
if adjusted_on_vert {
|
||||
let sign = if input.directional_input >= 0.5 {
|
||||
1.0
|
||||
} else {
|
||||
-1.0
|
||||
};
|
||||
let mut heading = input.heading_direction;
|
||||
heading[1] = 0.0;
|
||||
let since_apex = (c.prediction.result.contact_time - apex_time(c.prediction)).abs();
|
||||
let penalty = if since_apex >= s.minimum_time_after_apex {
|
||||
0.0
|
||||
} else {
|
||||
-500.0
|
||||
};
|
||||
let coefficient = ((1.0 - input.ground_normal[1].abs()) * sign) * s.score_transition;
|
||||
transition = dot(c.normal, normalize(heading)) * coefficient + penalty;
|
||||
}
|
||||
c.score = s
|
||||
.landing_time_bonus
|
||||
.evaluate(c.prediction.result.contact_time)
|
||||
+ (((((direction_score + 1.0) + force_score) + middle_bonus) + grind_score)
|
||||
+ transition);
|
||||
c.score += surface_score;
|
||||
}
|
||||
//AddWallRideScores runs after ALL candidates, using the final highest Y.
|
||||
for c in candidates {
|
||||
if c.prediction.result.valid()
|
||||
&& c.prediction.result.contact_frame >= s.minimum_trajectory_frames
|
||||
{
|
||||
if c.wall_score == 0.0 && c.prediction.collision_position()[1] > highest_wall {
|
||||
c.wall_score = 2000.0;
|
||||
}
|
||||
c.score += c.wall_score;
|
||||
}
|
||||
}
|
||||
Ok(all_miss)
|
||||
}
|
||||
|
||||
fn apex_time(p: Prediction) -> f32 {
|
||||
let v = p.request.trajectory.velocity[1];
|
||||
let g = p.request.trajectory.acceleration[1];
|
||||
if v < 0.0 || g >= 0.0 {
|
||||
0.0
|
||||
} else {
|
||||
v * reciprocal(-g)
|
||||
}
|
||||
}
|
||||
fn wall_score(
|
||||
p: &mut Prediction,
|
||||
reject_grind: bool,
|
||||
s: &SelectorSettings,
|
||||
highest: &mut f32,
|
||||
line: &mut impl FnMut(Vector, Vector, f32) -> Result<Option<SurfaceHit>, String>,
|
||||
) -> Result<f32, String> {
|
||||
let normal = p.result.contact_normal;
|
||||
if !(normal[1].abs() < 0.5) {
|
||||
return Ok(0.0);
|
||||
}
|
||||
let start = madd(normal, s.wall_ride_test_distance, p.result.contact_position);
|
||||
let end = add(start, [0.0, -10.0, 0.0, 0.0]);
|
||||
let Some(hit) = line(start, end, 0.0)? else {
|
||||
return Ok(0.0);
|
||||
};
|
||||
if !(dot(hit.normal, normal) < s.wall_ride_normal_dot_limit) {
|
||||
return Ok(0.0);
|
||||
}
|
||||
let height = (hit.position[1] - start[1]).abs();
|
||||
if reject_grind {
|
||||
return Ok(height * s.wall_ride_height_score - 1000.0);
|
||||
}
|
||||
let position = p.collision_position();
|
||||
let mut delta = sub(p.request.trajectory.position, position);
|
||||
let mut slope = delta[1];
|
||||
delta[1] = 0.0;
|
||||
let horizontal = length(delta);
|
||||
if horizontal > f32::from_bits(0x3c23_d70a) {
|
||||
slope *= reciprocal(horizontal);
|
||||
}
|
||||
if slope > s.wall_ride_angle_allow_landing {
|
||||
return Ok(height * s.wall_ride_height_score - 1000.0);
|
||||
}
|
||||
if height < s.wall_ride_minimum_height {
|
||||
return Ok(height - 1000.0);
|
||||
}
|
||||
let boost = s.wall_ride_boost.evaluate(height);
|
||||
if position[1] > *highest {
|
||||
*highest = position[1];
|
||||
}
|
||||
adjust_trajectory(
|
||||
&mut p.request.trajectory,
|
||||
p.result.contact_frame,
|
||||
[0.0, boost, 0.0, 0.0],
|
||||
s.maximum_trajectory_adjust,
|
||||
);
|
||||
Ok(height * s.wall_ride_height_score)
|
||||
}
|
||||
@@ -0,0 +1,313 @@
|
||||
//! Original selector Launch82D67848 and Update82D68800 with typed batch ownership.
|
||||
use super::{
|
||||
LaunchInfo, Prediction, QueryRequest, QueryResult, SelectorInput, SelectorSettings, SurfaceHit,
|
||||
Trajectory, launch,
|
||||
math::*,
|
||||
scoring::{self, Candidate},
|
||||
};
|
||||
|
||||
///The current authored world contains collision triangles and no grind edges.
|
||||
///Pass this only after querying that actual world's topology. It is not a
|
||||
///replacement for a grind result when a world contains grind primitives.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct WorldWithoutGrindEdges;
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Selection {
|
||||
pub candidate_index: usize, //9636
|
||||
pub prediction: Prediction, //owning copy at1696, pointer1680
|
||||
pub start_velocity: Vector, //2640
|
||||
pub landing_normal: Vector, //2656
|
||||
pub collision_velocity: Vector, //2672
|
||||
pub collision_position: Vector, //2688
|
||||
pub com_trajectory: Trajectory, //2704
|
||||
pub surface_category: u32, //9640
|
||||
pub wall_ride: bool, //9661
|
||||
}
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct TrajectorySelector {
|
||||
launch_info: Option<LaunchInfo>,
|
||||
batch: Option<launch::LaunchBatch>,
|
||||
candidates: Vec<Candidate>,
|
||||
selection: Option<Selection>,
|
||||
selected_index: Option<usize>,
|
||||
grind_locked_to_middle: bool, //9653
|
||||
grind_normal: Option<Vector>, //9656/2896
|
||||
pass: u16, //9632
|
||||
adjusted_on_vert: bool, //9651
|
||||
pending: bool, //9657
|
||||
valid: bool, //9658
|
||||
just_changed: bool, //9660
|
||||
all_predictions_missed: bool, //9662
|
||||
suggested_normal: Option<Vector>, //9655/2880
|
||||
}
|
||||
impl TrajectorySelector {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
pub fn pending(&self) -> bool {
|
||||
self.pending
|
||||
}
|
||||
pub fn valid(&self) -> bool {
|
||||
self.valid
|
||||
}
|
||||
pub fn just_changed(&self) -> bool {
|
||||
self.just_changed
|
||||
}
|
||||
pub fn all_predictions_missed(&self) -> bool {
|
||||
self.all_predictions_missed
|
||||
}
|
||||
pub fn suggested_normal(&self) -> Option<Vector> {
|
||||
self.suggested_normal
|
||||
}
|
||||
pub fn selection(&self) -> Option<&Selection> {
|
||||
self.selection.as_ref()
|
||||
}
|
||||
pub fn selected_index(&self) -> Option<usize> {
|
||||
self.selected_index
|
||||
}
|
||||
pub fn grind_locked_to_middle(&self) -> bool {
|
||||
self.grind_locked_to_middle
|
||||
}
|
||||
pub fn grind_normal(&self) -> Option<Vector> {
|
||||
self.grind_normal
|
||||
}
|
||||
pub fn launch_info(&self) -> Option<&LaunchInfo> {
|
||||
self.launch_info.as_ref()
|
||||
}
|
||||
pub fn requests(&self) -> &[QueryRequest] {
|
||||
self.batch.as_ref().map_or(&[], |b| b.requests.as_slice())
|
||||
}
|
||||
pub fn com_displacement(&self) -> Option<Vector> {
|
||||
self.batch.as_ref().map(|b| b.com_displacement)
|
||||
}
|
||||
pub fn local_com_position(&self) -> Option<Vector> {
|
||||
self.batch.as_ref().map(|b| b.local_com_position)
|
||||
}
|
||||
pub fn local_board_position(&self) -> Option<Vector> {
|
||||
self.batch.as_ref().map(|b| b.local_board_position)
|
||||
}
|
||||
pub fn board_position(&self) -> Option<Vector> {
|
||||
self.batch.as_ref().map(|b| b.board_position)
|
||||
}
|
||||
pub fn query_origin(&self) -> Option<Vector> {
|
||||
self.batch.as_ref().map(|b| b.origin)
|
||||
}
|
||||
///Update82D68800 always clears9660 even if no batch is ready.
|
||||
pub fn update_without_completion(&mut self) -> bool {
|
||||
self.just_changed = false;
|
||||
self.valid
|
||||
}
|
||||
///Ground's explicit cancellation writes9657 only; cached observations stay.
|
||||
pub fn cancel_pending(&mut self) {
|
||||
self.pending = false;
|
||||
}
|
||||
|
||||
///Reset82D67228 invalidates landing observations, while retaining the
|
||||
///in-flight request9657, winning result1680 and selected index9636.
|
||||
pub fn reset(&mut self) {
|
||||
self.valid = false;
|
||||
self.suggested_normal = None;
|
||||
self.grind_normal = None;
|
||||
self.just_changed = false;
|
||||
self.all_predictions_missed = false;
|
||||
self.pass = 0;
|
||||
self.grind_locked_to_middle = false;
|
||||
if let Some(selection) = &mut self.selection {
|
||||
selection.com_trajectory = Trajectory {
|
||||
position: ZERO,
|
||||
velocity: ZERO,
|
||||
acceleration: ZERO,
|
||||
duration: -1.0,
|
||||
};
|
||||
}
|
||||
//The reset's remaining slots are not represented by this selector's
|
||||
//player-owned, triangle-world observations.
|
||||
}
|
||||
|
||||
pub fn launch(
|
||||
&mut self,
|
||||
mut info: LaunchInfo,
|
||||
input: SelectorInput,
|
||||
s: &SelectorSettings,
|
||||
) -> Result<bool, String> {
|
||||
if self.pending {
|
||||
return Ok(false);
|
||||
}
|
||||
if info.trajectory_count == 0 {
|
||||
return Err("GetLaunchInfo supplied zero trajectory candidates".into());
|
||||
}
|
||||
self.pass = 0;
|
||||
self.valid = false;
|
||||
//Launch invalidates9636 but retains the cached winning result and COM.
|
||||
self.selected_index = None;
|
||||
self.grind_normal = None;
|
||||
self.adjusted_on_vert = launch::adjust_velocity(&mut info, input, s);
|
||||
self.launch_info = Some(info);
|
||||
self.launch_pass(input, s);
|
||||
Ok(true)
|
||||
}
|
||||
fn launch_pass(&mut self, input: SelectorInput, s: &SelectorSettings) {
|
||||
let info = self
|
||||
.launch_info
|
||||
.expect("Launch owns the packet before its batch");
|
||||
self.all_predictions_missed = false;
|
||||
if let Some(selection) = &mut self.selection {
|
||||
selection.wall_ride = false;
|
||||
}
|
||||
let batch = launch::batch(info, input, s);
|
||||
//Only velocities are written by InitTrajectoryInfo. Retain the other
|
||||
//candidate fields across passes until scoring writes a valid result.
|
||||
self.candidates
|
||||
.resize_with(self.candidates.len().max(batch.requests.len()), || {
|
||||
Candidate {
|
||||
prediction: Prediction {
|
||||
request: batch.requests[0],
|
||||
result: QueryResult::miss(),
|
||||
},
|
||||
start_velocity: ZERO,
|
||||
normal: ZERO,
|
||||
collision_velocity: ZERO,
|
||||
collision_position: ZERO,
|
||||
score: 0.0,
|
||||
wall_score: 0.0,
|
||||
wall_ride: false,
|
||||
}
|
||||
});
|
||||
for (index, c) in self
|
||||
.candidates
|
||||
.iter_mut()
|
||||
.take(batch.requests.len())
|
||||
.enumerate()
|
||||
{
|
||||
c.start_velocity = batch.velocities[index];
|
||||
c.prediction = Prediction {
|
||||
request: batch.requests[index],
|
||||
result: QueryResult::miss(),
|
||||
};
|
||||
}
|
||||
self.batch = Some(batch);
|
||||
self.pending = true;
|
||||
}
|
||||
|
||||
///Complete a real world query batch in order. A second pass is exposed by
|
||||
///pending()/requests(); host scheduling preserves its next Update boundary.
|
||||
pub fn complete_batch(
|
||||
&mut self,
|
||||
results: &[QueryResult],
|
||||
input: SelectorInput,
|
||||
s: &SelectorSettings,
|
||||
_topology: WorldWithoutGrindEdges,
|
||||
line: impl FnMut(Vector, Vector, f32) -> Result<Option<SurfaceHit>, String>,
|
||||
) -> Result<bool, String> {
|
||||
self.just_changed = false;
|
||||
if !self.pending {
|
||||
return Ok(self.valid);
|
||||
}
|
||||
let count = self.requests().len();
|
||||
if results.len() != count {
|
||||
return Err("Trajectory completion count differs from submitted batch".into());
|
||||
}
|
||||
self.just_changed = self.pass == 2;
|
||||
self.pending = false;
|
||||
self.pass += 1;
|
||||
for (c, &result) in self.candidates.iter_mut().zip(results) {
|
||||
c.prediction.result = result;
|
||||
}
|
||||
self.all_predictions_missed = scoring::score(
|
||||
&mut self.candidates[..count],
|
||||
self.pass,
|
||||
self.adjusted_on_vert,
|
||||
input,
|
||||
s,
|
||||
line,
|
||||
)?;
|
||||
let mut index = 0;
|
||||
let mut best = -100_000_000.0;
|
||||
for (i, c) in self.candidates.iter().take(count).enumerate() {
|
||||
if c.score > best {
|
||||
best = c.score;
|
||||
index = i;
|
||||
}
|
||||
}
|
||||
let c = self.candidates[index];
|
||||
self.selected_index = Some(index);
|
||||
//The copied COM trajectory is only recalculated on a valid-time result.
|
||||
let previous_com = self.selection.map(|v| v.com_trajectory);
|
||||
let mut selection = Selection {
|
||||
candidate_index: index,
|
||||
prediction: c.prediction,
|
||||
start_velocity: c.start_velocity,
|
||||
landing_normal: c.normal,
|
||||
collision_velocity: c.collision_velocity,
|
||||
collision_position: c.collision_position,
|
||||
//Ctor82D66DC0 initializes2704/2720/2736=zero,2752=-1.
|
||||
com_trajectory: previous_com.unwrap_or(Trajectory {
|
||||
position: ZERO,
|
||||
velocity: ZERO,
|
||||
acceleration: ZERO,
|
||||
duration: -1.0,
|
||||
}),
|
||||
surface_category: self.selection.map_or(0, |v| v.surface_category),
|
||||
wall_ride: c.wall_ride,
|
||||
};
|
||||
if c.prediction.result.contact_time < s.minimum_valid_time {
|
||||
self.suggested_normal = Some(c.prediction.result.contact_normal);
|
||||
self.valid = false;
|
||||
self.just_changed = false;
|
||||
self.selection = Some(selection);
|
||||
return Ok(false);
|
||||
}
|
||||
selection.surface_category = (c.prediction.result.surface >> 7) & 31;
|
||||
selection.com_trajectory = self.com_trajectory(selection, input, s);
|
||||
//No grind edges means native9652/9653 are false. AIPath is absent from
|
||||
//this player-owned runtime, so original9659 is false as well.
|
||||
self.grind_locked_to_middle = false;
|
||||
let second_pass = self.pass == 1
|
||||
&& count >= 2
|
||||
&& !(angle_between(selection.landing_normal, input.ground_normal)
|
||||
< s.minimum_normal_delta_second_pass);
|
||||
self.selection = Some(selection);
|
||||
self.valid = true;
|
||||
if second_pass {
|
||||
let info = self.launch_info.as_mut().expect("completed launch packet");
|
||||
info.start_velocity = selection.start_velocity;
|
||||
info.cone_angle_x = s.cone_x_second_pass;
|
||||
info.cone_angle_z = s.cone_z_second_pass;
|
||||
self.launch_pass(input, s);
|
||||
self.pass = 2;
|
||||
}
|
||||
Ok(self.valid)
|
||||
}
|
||||
fn com_trajectory(
|
||||
&self,
|
||||
selection: Selection,
|
||||
input: SelectorInput,
|
||||
s: &SelectorSettings,
|
||||
) -> Trajectory {
|
||||
let info = self.launch_info.expect("completed launch packet");
|
||||
let batch = self.batch.as_ref().expect("completed query batch");
|
||||
let mut trajectory = selection.prediction.request.trajectory;
|
||||
let scalar = s
|
||||
.landing_com_scalar_vs_slope
|
||||
.evaluate(selection.landing_normal[1]);
|
||||
let delta = sub(info.animation_com_position, trajectory.position);
|
||||
let height = dot(
|
||||
input.reference_up,
|
||||
sub(batch.origin, info.animation_com_position),
|
||||
)
|
||||
.abs()
|
||||
* scalar;
|
||||
let offset = scale(selection.landing_normal, height);
|
||||
if selection.prediction.result.contact_frame > 0 {
|
||||
trajectory.position = add(trajectory.position, delta);
|
||||
launch::adjust_trajectory(
|
||||
&mut trajectory,
|
||||
selection.prediction.result.contact_frame,
|
||||
sub(offset, delta),
|
||||
s.maximum_trajectory_adjust,
|
||||
);
|
||||
}
|
||||
trajectory
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
//! Launch data82D33448/82BE33D0 and the source-owned caller observations.
|
||||
use super::math::{IDENTITY, Transform, Vector, ZERO};
|
||||
use crate::point_graph::PointGraph;
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct LaunchInfo {
|
||||
pub reckoning_transform: Transform, //0=Reckoning816
|
||||
pub reckoning_inverse: Transform, //64=Reckoning944
|
||||
pub start_velocity: Vector, //128=Processed400 m/s
|
||||
pub com_velocity: Vector, //144=Processed704 m/s
|
||||
pub skeleton_vector_160: Vector, //160=Skeleton16208
|
||||
pub skeleton_vector_176: Vector, //176=Skeleton16240
|
||||
pub board_position: Vector, //192=Processed112 metres
|
||||
pub animation_com_position: Vector, //208=Processed592 metres
|
||||
pub start_position_override: Vector, //224 metres
|
||||
pub board_position_override: Vector, //240 metres
|
||||
pub cone_angle_x: f32, //256 degrees
|
||||
pub cone_angle_z: f32, //260 degrees
|
||||
pub timestep: f32, //264 seconds
|
||||
pub player_jumped: bool, //268
|
||||
pub use_position_override: bool, //269
|
||||
pub trajectory_count: u16, //270 GetLaunchInfo selects1 or7
|
||||
}
|
||||
impl Default for LaunchInfo {
|
||||
///Original ctor82D33448; Fill must supply current physical observations.
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
reckoning_transform: IDENTITY,
|
||||
reckoning_inverse: IDENTITY,
|
||||
start_velocity: ZERO,
|
||||
com_velocity: ZERO,
|
||||
skeleton_vector_160: ZERO,
|
||||
skeleton_vector_176: ZERO,
|
||||
board_position: ZERO,
|
||||
animation_com_position: ZERO,
|
||||
start_position_override: ZERO,
|
||||
board_position_override: ZERO,
|
||||
cone_angle_x: 0.0,
|
||||
cone_angle_z: 0.0,
|
||||
timestep: 0.0,
|
||||
player_jumped: false,
|
||||
use_position_override: false,
|
||||
trajectory_count: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct SelectorInput {
|
||||
pub gravity: Vector, //PhysicsWorld32 m/s^2
|
||||
pub ground_normal: Vector, //Processed464
|
||||
pub contact_position: Vector, //496 metres
|
||||
pub heading_direction: Vector, //512
|
||||
pub reference_up: Vector, //544
|
||||
pub board_vertical_velocity: f32, //404 m/s
|
||||
pub directional_input: f32, //2636
|
||||
pub previous_physics_state: u32, //2504
|
||||
pub flags_2472: u32,
|
||||
pub flags_2476: u32,
|
||||
pub offboard_flags_1776: u32,
|
||||
pub grind_lock_distance: f32, //active physics_mode80 metres
|
||||
}
|
||||
///Stock skater collection values; offsets refer to physics_trajectory unless stated.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SelectorSettings {
|
||||
pub cone_x: f32, //576 degrees, Skeleton GetLaunchInfo
|
||||
pub cone_z: f32, //568 degrees, Skeleton GetLaunchInfo
|
||||
pub trajectory_max_time: f32, //428 seconds
|
||||
pub trajectory_max_drop: f32, //63673FB6DA092B7E metres
|
||||
pub trajectory_error_start: f32, //432
|
||||
pub trajectory_error_end: f32, //436
|
||||
pub speed_factor_min: f32, //440 m/s
|
||||
pub speed_factor_max: f32, //444 m/s
|
||||
pub cone_angle_z_vs_speed: PointGraph<8>, //304
|
||||
pub cone_x_second_pass: f32, //572 degrees
|
||||
pub cone_z_second_pass: f32, //564 degrees
|
||||
pub landing_time_bonus: PointGraph<8>, //0 x16/y48
|
||||
pub landing_com_scalar_vs_slope: PointGraph<8>, //80 x96/y128
|
||||
pub landing_force_scalar: PointGraph<8>, //240
|
||||
pub grind_penalty_vs_distance: PointGraph<8>, //160 x176/y208
|
||||
pub score_middle_bonus: f32, //468
|
||||
pub score_landing_force: f32, //472
|
||||
pub score_landing_direction: f32, //476
|
||||
pub score_transition: f32, //456
|
||||
pub surface_unrideable_score: f32, //460
|
||||
pub surface_dont_align_score: f32, //464
|
||||
pub natural_air_off_verts_scalar: f32, //484
|
||||
pub minimum_valid_time: f32, //488 seconds
|
||||
pub minimum_time_after_apex: f32, //492 seconds
|
||||
pub minimum_normal_delta_second_pass: f32, //496 radians
|
||||
pub maximum_trajectory_adjust: f32, //500
|
||||
pub wall_ride_test_distance: f32, //416 metres
|
||||
pub wall_ride_minimum_height: f32, //420 metres
|
||||
pub wall_ride_angle_allow_landing: f32, //424
|
||||
pub wall_ride_height_score: f32, //452
|
||||
pub wall_ride_boost: PointGraph<4>, //368 x384/y400
|
||||
pub wall_ride_normal_dot_limit: f32, //physics global188/layout248
|
||||
pub displacement_vs_speed: PointGraph<8>, //physics_reckoning544
|
||||
pub displacement_vs_ground_normal: PointGraph<8>, //physics_reckoning608
|
||||
pub trajectory_radius: f32, //physics_reckoning C6710942E297D587 metres
|
||||
pub trajectory_displacement: f32, //physics_reckoning runtime attribute
|
||||
pub minimum_trajectory_frames: i32, //physics_reckoning runtime attribute
|
||||
pub vert_jump_align_factor: f32, //0B42DFBB7F7756A0
|
||||
pub vert_jump_align_max_ground_normal_y: f32, //94923D136EA2DA37
|
||||
pub vert_jump_align_min_direction_y: f32, //58BFC8D41216C7EE
|
||||
pub vert_jump_align_max_angle: f32, //723FF102068622CF
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
//! ControlAirLegExtension82BAF538 and its ascending/descending/preland helpers.
|
||||
use super::output::attributes::AnimationAttribute;
|
||||
use crate::{physics::native_arithmetic::dot3, point_graph::PointGraph};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Bone {
|
||||
Board,
|
||||
LeftToe,
|
||||
RightToe,
|
||||
}
|
||||
|
||||
pub struct Settings {
|
||||
pub going_up_speed: f32,
|
||||
pub going_down_speed: f32,
|
||||
pub minimum_height: f32,
|
||||
pub preland_velocity: f32,
|
||||
pub preland_final_height: f32,
|
||||
pub offboard_multiplier: f32,
|
||||
pub arm_extension: PointGraph<4>,
|
||||
}
|
||||
|
||||
///All vectors are preceding completed physical output in stock coordinates.
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct Physical {
|
||||
pub com_velocity: [f32; 4],
|
||||
pub com_position: [f32; 4],
|
||||
pub system_up: [f32; 4],
|
||||
pub right_toe: [f32; 4],
|
||||
pub left_toe: [f32; 4],
|
||||
pub animation_height: f32,
|
||||
pub offboard_316: bool,
|
||||
pub remaining_air_time: f32,
|
||||
}
|
||||
|
||||
///CreateInstance82BAF980 zeros mode8, distance12 and descending_time16.
|
||||
#[derive(Default)]
|
||||
pub struct State {
|
||||
mode: u32,
|
||||
height: f32,
|
||||
descending_time: f32,
|
||||
}
|
||||
impl State {
|
||||
pub fn needs_initial_attribute(&self) -> bool {
|
||||
self.mode == 0
|
||||
}
|
||||
pub fn needs_prelanding_query(&self, p: Physical) -> bool {
|
||||
self.mode == 2 || (self.mode == 1 && p.com_velocity[1] < 0.0)
|
||||
}
|
||||
///The first update seeds the height but does not also advance its new mode.
|
||||
pub fn update(
|
||||
&mut self,
|
||||
bone: Bone,
|
||||
p: Physical,
|
||||
settings: &Settings,
|
||||
dt: f32,
|
||||
initial_attribute: Option<AnimationAttribute>,
|
||||
prepare_to_land: bool,
|
||||
) -> [f32; 2] {
|
||||
let ascending = p.com_velocity[1] > 0.0;
|
||||
match self.mode {
|
||||
0 => {
|
||||
self.mode = if ascending { 1 } else { 2 };
|
||||
if let Some(attribute) = initial_attribute {
|
||||
if attribute.kind == 0 {
|
||||
//The queried union is initialized to zero before GetLastAttr.
|
||||
self.height = f32::from_bits(attribute.payload.0[0].unwrap_or(0));
|
||||
}
|
||||
} else {
|
||||
let toe = if p.offboard_316 || bone == Bone::RightToe {
|
||||
Some(p.right_toe)
|
||||
} else if bone == Bone::LeftToe {
|
||||
Some(p.left_toe)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
self.height = match toe {
|
||||
Some(toe) => dot3(
|
||||
std::array::from_fn(|i| p.com_position[i] - toe[i]),
|
||||
p.system_up,
|
||||
),
|
||||
None => p.animation_height,
|
||||
};
|
||||
}
|
||||
}
|
||||
1 if p.com_velocity[1] >= 0.0 => {
|
||||
//82BAFB60 is FNMSUBS, followed by FSEL against minimum1756.
|
||||
self.height = maximum(
|
||||
(-dt).mul_add(settings.going_up_speed, self.height),
|
||||
settings.minimum_height,
|
||||
);
|
||||
}
|
||||
1 | 2 => {
|
||||
self.mode = 2;
|
||||
if prepare_to_land {
|
||||
self.mode = 3;
|
||||
self.preland(bone, settings, dt);
|
||||
} else {
|
||||
//82BAFC88..FD0C: both dot products precede the fused blend.
|
||||
let closing = -dot3(p.com_velocity, p.system_up);
|
||||
let vertical = dot3(p.system_up, [0.0, 1.0, 0.0, 0.0]).abs();
|
||||
let speed = maximum(closing, 0.0)
|
||||
.mul_add(vertical, (1.0 - vertical) * settings.going_down_speed);
|
||||
let speed = minimum(speed, settings.going_down_speed);
|
||||
self.height =
|
||||
maximum((-speed).mul_add(dt, self.height), settings.minimum_height);
|
||||
}
|
||||
}
|
||||
3 => self.preland(bone, settings, dt),
|
||||
_ => {}
|
||||
}
|
||||
self.descending_time = if ascending {
|
||||
0.0
|
||||
} else {
|
||||
self.descending_time + dt
|
||||
};
|
||||
[
|
||||
self.height,
|
||||
settings.arm_extension.evaluate(self.descending_time),
|
||||
]
|
||||
}
|
||||
fn preland(&mut self, bone: Bone, settings: &Settings, dt: f32) {
|
||||
self.height = dt.mul_add(settings.preland_velocity, self.height);
|
||||
if bone != Bone::Board {
|
||||
self.height *= settings.offboard_multiplier;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
///82BAFDB8 checks the named FullExtension attribute after the physical query.
|
||||
pub fn prepare_to_land(
|
||||
physical_query: bool,
|
||||
full_extension: Option<AnimationAttribute>,
|
||||
p: Physical,
|
||||
s: &Settings,
|
||||
) -> bool {
|
||||
if !physical_query {
|
||||
return false;
|
||||
}
|
||||
let Some(attribute) = full_extension else {
|
||||
return true;
|
||||
};
|
||||
//GetLastAttr's scalar output remains initialized zero for nonfloat records.
|
||||
let extension = if attribute.kind == 0 {
|
||||
f32::from_bits(attribute.payload.0[0].unwrap_or(0))
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
if extension < s.preland_final_height - f32::from_bits(0x3dcccccd) {
|
||||
return true;
|
||||
}
|
||||
let required_time = (extension - p.animation_height) / s.preland_velocity;
|
||||
p.remaining_air_time - f32::from_bits(0x3d75c28f) < required_time
|
||||
}
|
||||
fn maximum(a: f32, b: f32) -> f32 {
|
||||
if a - b >= 0.0 { a } else { b }
|
||||
}
|
||||
fn minimum(a: f32, b: f32) -> f32 {
|
||||
if b - a >= 0.0 { a } else { b }
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
//! SettingBodyTilt Update82BA88A0 and CreateInstance82BA8B40.
|
||||
use crate::point_graph::PointGraph;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Settings {
|
||||
pub body_spin_factor: PointGraph<4>,
|
||||
pub ground_velocity: f32,
|
||||
pub ground_acceleration: f32,
|
||||
pub air_velocity: f32,
|
||||
pub air_acceleration: f32,
|
||||
}
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Physical {
|
||||
///PhysOutAnimation vector+0 X, consumed with mirrored stance sign.
|
||||
pub lateral_tilt: f32,
|
||||
///Expanded PhysOut bundle+8 scalar204; the curve consumes its magnitude.
|
||||
pub body_spin_speed: f32,
|
||||
pub filtered_category: u32,
|
||||
}
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct State {
|
||||
value: f32,
|
||||
velocity: f32,
|
||||
was_enabled: bool,
|
||||
}
|
||||
impl State {
|
||||
pub fn disable(&mut self) {self.was_enabled=false;}
|
||||
pub fn update(
|
||||
&mut self,
|
||||
enabled: bool,
|
||||
mirrored: bool,
|
||||
p: Physical,
|
||||
s: &Settings,
|
||||
) -> Option<f32> {
|
||||
if !self.was_enabled && enabled {
|
||||
self.value = 0.0;
|
||||
self.velocity = 0.0;
|
||||
}
|
||||
self.was_enabled = enabled;
|
||||
if !enabled {
|
||||
return None;
|
||||
}
|
||||
//82B97140 tests fullSkaterAnim15180 bit30. The nonmirrored branch
|
||||
//flips the sign bit of the source X component before multiplication.
|
||||
let tilt = if mirrored {
|
||||
p.lateral_tilt
|
||||
} else {
|
||||
f32::from_bits(p.lateral_tilt.to_bits() ^ 0x80000000)
|
||||
};
|
||||
let target = s.body_spin_factor.evaluate(p.body_spin_speed.abs()) * tilt;
|
||||
let (velocity, acceleration) = if p.filtered_category == 2 {
|
||||
(s.air_velocity, s.air_acceleration)
|
||||
} else {
|
||||
(s.ground_velocity, s.ground_acceleration)
|
||||
};
|
||||
let desired = bound(target - self.value, -velocity, velocity);
|
||||
let change = bound(desired - self.velocity, -acceleration, acceleration);
|
||||
self.velocity += change;
|
||||
self.value += self.velocity;
|
||||
Some(self.value)
|
||||
}
|
||||
}
|
||||
fn bound(value: f32, lo: f32, hi: f32) -> f32 {
|
||||
let lower = if lo - value >= 0.0 { lo } else { value };
|
||||
if hi - lower >= 0.0 { lower } else { hi }
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
//! Persistent channel timing from TU3 NewChannel82D1CC20, AddTime82D266A8,
|
||||
//! RemoveExpiredChannels82D1DA00 and EndChannelPrematurely82D1D6A8.
|
||||
use super::{clip_clock::AdvanceResult,output::attributes::AnimationAttribute};
|
||||
|
||||
#[derive(Clone,Copy,Debug,PartialEq)]
|
||||
pub struct ChannelSettings {
|
||||
pub priority:i32,
|
||||
pub keep_alive:bool,
|
||||
pub mirrored:bool,
|
||||
pub speed:f32,
|
||||
pub blend_in:f32,
|
||||
pub hold_during_blend_in:bool,
|
||||
pub blend_out:f32,
|
||||
pub hold_during_blend_out:bool,
|
||||
pub use_attributes:bool,
|
||||
}
|
||||
|
||||
#[derive(Clone,Debug)]
|
||||
pub struct ChannelPlayback {
|
||||
pub settings:ChannelSettings,
|
||||
pub weight:f32,
|
||||
pub influence:f32,
|
||||
blend_in_elapsed:f32,
|
||||
blend_out_elapsed:f32,
|
||||
resurrection_elapsed:f32,
|
||||
resurrection_duration:f32,
|
||||
flags:u32,
|
||||
}
|
||||
impl ChannelPlayback {
|
||||
pub fn new(settings:ChannelSettings)->Self {
|
||||
let flags=(u32::from(settings.keep_alive)<<28)
|
||||
|(u32::from(settings.hold_during_blend_in)<<31)
|
||||
|(u32::from(settings.hold_during_blend_out)<<30)
|
||||
|(u32::from(settings.blend_out>0.0)<<27)
|
||||
|(u32::from(settings.use_attributes)<<25);
|
||||
Self {settings,weight:0.0,influence:1.0,blend_in_elapsed:0.0,blend_out_elapsed:0.0,resurrection_elapsed:0.0,resurrection_duration:0.0,flags}
|
||||
}
|
||||
///Called before advancing any trees, in the native actor AddTime order.
|
||||
pub fn expired(&self)->bool {
|
||||
if self.flags&0x08000000!=0 {self.blend_out_elapsed>=self.settings.blend_out}
|
||||
else {self.flags&0x10000000==0&&self.flags&0x04000000!=0}
|
||||
}
|
||||
pub fn end(&mut self) {if self.blend_out_elapsed==0.0 {self.flags|=0x28000000;}}
|
||||
pub fn end_with(&mut self,seconds:f32,from_last_frame:bool) {
|
||||
if self.blend_out_elapsed==0.0 {
|
||||
self.settings.blend_out=seconds;self.settings.hold_during_blend_out=from_last_frame;
|
||||
self.flags=(self.flags&!0x40000000)|(u32::from(from_last_frame)<<30)|0x28000000;
|
||||
}
|
||||
}
|
||||
pub fn can_transition(&self,resurrect:bool)->bool {!(self.blend_out_elapsed>0.0)||resurrect}
|
||||
///StartTransition82D1D398..D47C preserves fade-in time/influence and
|
||||
///priority, replaces fade-out settings, and optionally restores a fading
|
||||
///channel from its existing coefficient rather than restarting at zero.
|
||||
pub fn transition(&mut self,settings:ChannelSettings,resurrect:bool) {
|
||||
if self.blend_out_elapsed>0.0&&resurrect {
|
||||
self.resurrection_duration=settings.blend_in;
|
||||
self.resurrection_elapsed=unit(1.0-self.blend_out_elapsed/self.settings.blend_out)*settings.blend_in;
|
||||
}
|
||||
self.blend_out_elapsed=0.0;
|
||||
self.settings.keep_alive=settings.keep_alive;
|
||||
self.settings.blend_out=settings.blend_out;
|
||||
self.settings.hold_during_blend_out=settings.hold_during_blend_out;
|
||||
self.settings.use_attributes=settings.use_attributes;
|
||||
self.flags=(self.flags&!0x7a000000)|(u32::from(settings.keep_alive)<<28)
|
||||
|(u32::from(settings.hold_during_blend_out)<<30)
|
||||
|(u32::from(settings.blend_out>0.0)<<27)|(u32::from(settings.use_attributes)<<25);
|
||||
}
|
||||
///Returns whether the channel child should advance. The base has already
|
||||
///advanced; fade weights use PRE-update elapsed times, then increment them.
|
||||
pub fn advance(&mut self,dt:f32,length:f32,time:f32)->bool {
|
||||
let mut fade_in=1.0;
|
||||
if self.settings.blend_in>0.0&&self.blend_in_elapsed<self.settings.blend_in {
|
||||
fade_in=unit(self.blend_in_elapsed/self.settings.blend_in);
|
||||
self.blend_in_elapsed+=dt;
|
||||
}
|
||||
let remaining=length-time;
|
||||
let remaining=if remaining>=0.0 {remaining}else{0.0};
|
||||
let hold_end=self.flags&0x40000000!=0;
|
||||
let ended=self.flags&0x04000000!=0;
|
||||
let fade_automatically=if hold_end {ended}else{remaining<self.settings.blend_out};
|
||||
let mut fade_out=1.0;
|
||||
if self.settings.blend_out>0.0&&(self.flags&0x20000000!=0||(self.flags&0x10000000==0&&fade_automatically)) {
|
||||
fade_out=unit(1.0-self.blend_out_elapsed/self.settings.blend_out);
|
||||
self.blend_out_elapsed+=dt;
|
||||
}
|
||||
let mut resurrection=1.0;
|
||||
if self.resurrection_duration>0.0&&self.resurrection_elapsed<self.resurrection_duration {
|
||||
resurrection=unit(self.resurrection_elapsed/self.resurrection_duration);
|
||||
self.resurrection_elapsed+=dt;
|
||||
}
|
||||
let fade=if fade_in-fade_out>=0.0 {fade_out}else{fade_in};
|
||||
let fade=if fade-resurrection>=0.0 {resurrection}else{fade};
|
||||
self.weight=unit(fade*self.influence);
|
||||
!(self.flags&0x80000000!=0&&self.blend_in_elapsed<self.settings.blend_in)
|
||||
&&!(hold_end&&self.blend_out_elapsed>0.0)
|
||||
}
|
||||
pub fn did_advance(&mut self,property:AdvanceResult) {
|
||||
self.flags=(self.flags&!0x04000000)|(u32::from(property.crossed_end)<<26);
|
||||
}
|
||||
pub fn merge_attributes(&self,base:Vec<AnimationAttribute>,channel:&[AnimationAttribute])->Vec<AnimationAttribute> {
|
||||
if !self.settings.use_attributes {return base;}
|
||||
//82B966D0: ordered union, channel wins equal names without scaling.
|
||||
let mut output=Vec::with_capacity(base.len()+channel.len());let mut b=0;let mut c=0;
|
||||
while b<base.len()&&c<channel.len() {
|
||||
match base[b].name.0.cmp(&channel[c].name.0) {
|
||||
core::cmp::Ordering::Less=>{output.push(base[b]);b+=1;},
|
||||
core::cmp::Ordering::Greater=>{output.push(channel[c]);c+=1;},
|
||||
core::cmp::Ordering::Equal=>{output.push(channel[c]);b+=1;c+=1;},
|
||||
}
|
||||
}
|
||||
output.extend_from_slice(&base[b..]);output.extend_from_slice(&channel[c..]);output
|
||||
}
|
||||
}
|
||||
fn unit(value:f32)->f32 {let low=if -value>=0.0 {0.0}else{value};if 1.0-low>=0.0 {low}else{1.0}}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
fn settings()->ChannelSettings {ChannelSettings {priority:0,keep_alive:false,mirrored:false,speed:1.0,blend_in:0.5,hold_during_blend_in:true,blend_out:0.5,hold_during_blend_out:false,use_attributes:false}}
|
||||
#[test]
|
||||
fn preincrement_fade_and_expiry_order_preserve_native_first_frame() {
|
||||
let mut channel=ChannelPlayback::new(settings());
|
||||
assert!(!channel.advance(0.25,2.0,0.0));assert_eq!(channel.weight,0.0);
|
||||
assert!(channel.advance(0.25,2.0,0.0));assert_eq!(channel.weight,0.5);
|
||||
channel.end();
|
||||
assert!(channel.advance(0.25,2.0,0.25));assert_eq!(channel.weight,1.0);assert!(!channel.expired());
|
||||
channel.advance(0.25,2.0,0.5);assert_eq!(channel.weight,0.5);assert!(channel.expired());
|
||||
}
|
||||
#[test]
|
||||
fn returning_channel_restores_from_existing_fade_without_zero_restart() {
|
||||
let mut settings=settings();settings.hold_during_blend_in=false;
|
||||
let mut channel=ChannelPlayback::new(settings);
|
||||
channel.advance(0.5,2.0,0.0);channel.end();channel.advance(0.25,2.0,0.5);
|
||||
assert!(!channel.can_transition(false));assert!(channel.can_transition(true));
|
||||
channel.transition(settings,true);channel.advance(0.1,2.0,0.0);
|
||||
assert_eq!(channel.weight,0.5);assert!(!channel.expired());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
//! Andale Clip timing: 0x82D25A00, 0x825902C0, 0x827B8BD8,
|
||||
//! 0x827B8C38, 0x827B8B90; attribute status 0x82D264C0.
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct ClipClock {
|
||||
pub frames: f32,
|
||||
pub fps: f32,
|
||||
pub base_speed: f32,
|
||||
pub speed: f32,
|
||||
pub length: f32,
|
||||
pub time: f32,
|
||||
pub previous_time: f32,
|
||||
/// Native +40; retained until pose evaluation commits previous time.
|
||||
pub loops_since_evaluation: u32,
|
||||
pub looping: bool,
|
||||
/// Clip flags word bit 30 / native object byte +49.
|
||||
pub phase_controlled: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub struct AdvanceResult {
|
||||
/// Native output byte +1. Exact equality with the end does not cross it.
|
||||
pub crossed_end: bool,
|
||||
/// Native output +4; unchanged on updates that do not cross the end.
|
||||
pub overshoot: f32,
|
||||
/// Native output +8, measured before wrapping, so it may be negative.
|
||||
pub remaining_before_wrap: f32,
|
||||
}
|
||||
|
||||
impl ClipClock {
|
||||
pub fn set_time(&mut self, time: f32) {
|
||||
self.time = time;
|
||||
self.previous_time = time;
|
||||
self.loops_since_evaluation = 0;
|
||||
}
|
||||
|
||||
/// Preserve the native multiply/divide order when changing playback rate.
|
||||
/// In particular, simplifying this to old_speed/new_speed changes rounding.
|
||||
pub fn set_speed(&mut self, speed: f32) {
|
||||
let new_rate = self.base_speed * speed;
|
||||
let old_rate = self.speed * self.base_speed;
|
||||
let frames_per_second = self.fps * self.base_speed;
|
||||
let inverse_new_rate = 1.0 / new_rate;
|
||||
self.time = (self.time * old_rate) * inverse_new_rate;
|
||||
self.previous_time = (self.previous_time * old_rate) * inverse_new_rate;
|
||||
self.length = (self.frames - 1.0) / (frames_per_second * speed);
|
||||
self.speed = speed;
|
||||
}
|
||||
|
||||
pub fn sample_time(&self) -> f32 {
|
||||
if self.length - self.time >= 0.0 {
|
||||
self.time
|
||||
} else {
|
||||
self.length
|
||||
}
|
||||
}
|
||||
|
||||
pub fn advance(&mut self, delta_seconds: f32, phase: f32, result: &mut AdvanceResult) {
|
||||
if self.phase_controlled {
|
||||
self.previous_time = self.time;
|
||||
let lower = if -phase >= 0.0 { 0.0 } else { phase };
|
||||
let phase = if 1.0 - lower >= 0.0 { lower } else { 1.0 };
|
||||
self.time = self.length * phase;
|
||||
self.loops_since_evaluation = u32::from(self.time < self.previous_time);
|
||||
// The native phase branch does not touch the AdvanceResult fields.
|
||||
return;
|
||||
}
|
||||
self.time += delta_seconds;
|
||||
result.remaining_before_wrap = self.length - self.time;
|
||||
if self.time.partial_cmp(&self.length) != Some(core::cmp::Ordering::Greater) {
|
||||
result.crossed_end = false;
|
||||
} else if self.looping {
|
||||
// Invalid lengths cannot make progress in the native subtraction
|
||||
// loop. Fail explicitly rather than hang or invent a replacement.
|
||||
assert!(
|
||||
self.length > 0.0 && self.time.is_finite(),
|
||||
"invalid looping clip clock"
|
||||
);
|
||||
while self.time > self.length {
|
||||
result.crossed_end = true;
|
||||
result.overshoot = self.time - self.length;
|
||||
let next = self.time - self.length;
|
||||
assert!(
|
||||
next < self.time,
|
||||
"clip clock cannot advance at this precision"
|
||||
);
|
||||
self.time = next;
|
||||
self.loops_since_evaluation = self.loops_since_evaluation.wrapping_add(1);
|
||||
}
|
||||
} else {
|
||||
result.crossed_end = true;
|
||||
result.overshoot = self.time - self.length;
|
||||
}
|
||||
}
|
||||
|
||||
/// 0x82D25C20/24, called when pose evaluation's update-history flag is set.
|
||||
/// Advancing time alone does not perform this commit in the elapsed mode.
|
||||
pub fn commit_evaluation(&mut self) {
|
||||
self.previous_time = self.time;
|
||||
self.loops_since_evaluation = 0;
|
||||
}
|
||||
|
||||
/// Return the native status byte used by GetAttribute/GetAttributes and
|
||||
/// the physics contact consumer. Times in the bank are normalized to length.
|
||||
pub fn attribute_status(&self, begin: f32, end: f32) -> u8 {
|
||||
if begin == -1.0 {
|
||||
return 6;
|
||||
}
|
||||
let begin = self.length * begin;
|
||||
let end = self.length * end;
|
||||
let intersects = if self.loops_since_evaluation == 0 {
|
||||
end > self.previous_time && begin <= self.time
|
||||
} else {
|
||||
end > self.previous_time || begin <= self.time
|
||||
};
|
||||
if !intersects {
|
||||
17
|
||||
} else if self.time < begin || self.time > end {
|
||||
9
|
||||
} else {
|
||||
5
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
//! Owned host execution for the recovered SetData worker.
|
||||
//!
|
||||
//! 828D7E08/828D7FD8 define batch boundaries and dependency collection; preparing
|
||||
//! a job is not submitting or completing it (828C88D8). This synchronous host
|
||||
//! adapter accepts only SetData jobs and already-materialized input buffers.
|
||||
//! It is not a replacement for the full ACS scheduler or its other engines.
|
||||
use super::{
|
||||
buffers::{BufferError, PoseBuffers},
|
||||
set_data::{self, SetDataCommand},
|
||||
};
|
||||
use std::num::NonZeroU16;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct JobId {
|
||||
owner: usize,
|
||||
index: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum QueueError {
|
||||
CommandCapacityExceeded,
|
||||
DependencyCapacityExceeded,
|
||||
/// Other engines must finish and supply their owned buffers before this
|
||||
/// SetData-only adapter is constructed; unresolved handles are rejected.
|
||||
UnavailableDependency,
|
||||
}
|
||||
|
||||
struct Job {
|
||||
bone_count: u16,
|
||||
commands: Vec<SetDataCommand>,
|
||||
dependencies: Vec<JobId>,
|
||||
}
|
||||
impl Job {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
bone_count: 0,
|
||||
commands: Vec::new(),
|
||||
dependencies: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SetDataQueue {
|
||||
owner: usize,
|
||||
buffers: PoseBuffers,
|
||||
max_work_units: NonZeroU16,
|
||||
prepared: Vec<Job>,
|
||||
current: Job,
|
||||
command_count: usize,
|
||||
}
|
||||
impl SetDataQueue {
|
||||
pub fn new(buffers: PoseBuffers, max_work_units: NonZeroU16) -> Self {
|
||||
Self {
|
||||
owner: super::buffers::new_owner(),
|
||||
buffers,
|
||||
max_work_units,
|
||||
prepared: Vec::new(),
|
||||
current: Job::new(),
|
||||
command_count: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// A returned job identity establishes neither execution nor output readiness.
|
||||
/// None corresponds to a null dependency; current-job references are removed.
|
||||
pub fn enqueue(
|
||||
&mut self,
|
||||
bone_count: u16,
|
||||
command: SetDataCommand,
|
||||
dependencies: &[Option<JobId>],
|
||||
) -> Result<JobId, QueueError> {
|
||||
if self.command_count == usize::from(u16::MAX) {
|
||||
return Err(QueueError::CommandCapacityExceeded);
|
||||
}
|
||||
let full = self.current.commands.len() == usize::from(self.max_work_units.get());
|
||||
let id = JobId {
|
||||
owner: self.owner,
|
||||
index: self.prepared.len() + usize::from(full),
|
||||
};
|
||||
let mut collected = if full {
|
||||
Vec::new()
|
||||
} else {
|
||||
self.current.dependencies.clone()
|
||||
};
|
||||
for dependency in dependencies.iter().flatten().copied() {
|
||||
if dependency == id {
|
||||
continue;
|
||||
}
|
||||
if dependency.owner != self.owner || dependency.index >= id.index {
|
||||
return Err(QueueError::UnavailableDependency);
|
||||
}
|
||||
if !collected.contains(&dependency) {
|
||||
if collected.len() == usize::from(u8::MAX) {
|
||||
return Err(QueueError::DependencyCapacityExceeded);
|
||||
}
|
||||
collected.push(dependency);
|
||||
}
|
||||
}
|
||||
if full {
|
||||
self.flush();
|
||||
}
|
||||
// The native job header is rewritten on EVERY enqueue. Its last count
|
||||
// applies to external copies for all records in that job.
|
||||
self.current.bone_count = bone_count;
|
||||
self.current.commands.push(command);
|
||||
self.current.dependencies = collected;
|
||||
self.command_count += 1;
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
/// Finalize pending metadata; never executes a worker or exposes its output.
|
||||
pub fn flush(&mut self) -> bool {
|
||||
if self.current.commands.is_empty() {
|
||||
return false;
|
||||
}
|
||||
self.prepared
|
||||
.push(std::mem::replace(&mut self.current, Job::new()));
|
||||
true
|
||||
}
|
||||
|
||||
pub fn submit(mut self) -> PendingSetData {
|
||||
self.flush();
|
||||
PendingSetData {
|
||||
owner: self.owner,
|
||||
buffers: self.buffers,
|
||||
jobs: self.prepared,
|
||||
}
|
||||
}
|
||||
|
||||
/// Safe because no workers have been started by this host adapter.
|
||||
pub fn cancel(self) -> PoseBuffers {
|
||||
self.buffers
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PendingSetData {
|
||||
owner: usize,
|
||||
buffers: PoseBuffers,
|
||||
jobs: Vec<Job>,
|
||||
}
|
||||
impl PendingSetData {
|
||||
/// Execute complete workers in preparation order. All accepted dependencies
|
||||
/// point to earlier jobs, so their writes precede dependent reads. No native
|
||||
/// concurrency ordering beyond those dependencies is asserted.
|
||||
pub fn complete(mut self) -> Result<CompletedSetData, ExecutionFailure> {
|
||||
for (index, job) in self.jobs.iter().enumerate() {
|
||||
if let Err(cause) = set_data::execute(&mut self.buffers, job.bone_count, &job.commands)
|
||||
{
|
||||
return Err(ExecutionFailure {
|
||||
buffers: self.buffers,
|
||||
job: JobId {
|
||||
owner: self.owner,
|
||||
index,
|
||||
},
|
||||
cause,
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(CompletedSetData {
|
||||
buffers: self.buffers,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ExecutionFailure {
|
||||
/// Diagnostic/recovery storage only; this is not a completed pose certificate.
|
||||
pub buffers: PoseBuffers,
|
||||
pub job: JobId,
|
||||
pub cause: BufferError,
|
||||
}
|
||||
|
||||
pub struct CompletedSetData {
|
||||
buffers: PoseBuffers,
|
||||
}
|
||||
impl CompletedSetData {
|
||||
pub fn buffers(&self) -> &PoseBuffers {
|
||||
&self.buffers
|
||||
}
|
||||
/// Reuse can begin only after completion releases ownership.
|
||||
pub fn into_buffers(self) -> PoseBuffers {
|
||||
self.buffers
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
//! Typed animation allocations. Slots identify whole allocations, never addresses.
|
||||
use crate::animation::output::{NativeMatrix, Sqt};
|
||||
use std::marker::PhantomData;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
pub(crate) fn new_owner() -> usize {
|
||||
static NEXT_OWNER: AtomicUsize = AtomicUsize::new(0);
|
||||
NEXT_OWNER
|
||||
.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |value| {
|
||||
value.checked_add(1)
|
||||
})
|
||||
.expect("animation storage identity exhausted")
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct Buffer<T> {
|
||||
slot: usize,
|
||||
owner: usize,
|
||||
marker: PhantomData<T>,
|
||||
}
|
||||
impl<T: Copy> Buffer<T> {
|
||||
pub fn at(self, first_bone: usize) -> BoneSlice<T> {
|
||||
BoneSlice {
|
||||
buffer: self,
|
||||
first_bone,
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct BoneSlice<T> {
|
||||
pub buffer: Buffer<T>,
|
||||
pub first_bone: usize,
|
||||
}
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum BufferError {
|
||||
UnknownBuffer,
|
||||
RangeOutsideAllocation,
|
||||
UninitializedBone {
|
||||
bone: usize,
|
||||
},
|
||||
/// The native copy helper's partially overlapping behavior is not recovered.
|
||||
UnsupportedPartialOverlap,
|
||||
}
|
||||
pub struct BoneBuffers<T> {
|
||||
owner: usize,
|
||||
allocations: Vec<Vec<Option<T>>>,
|
||||
}
|
||||
impl<T: Copy> Default for BoneBuffers<T> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
owner: new_owner(),
|
||||
allocations: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<T: Copy> BoneBuffers<T> {
|
||||
/// Allocate exactly the requested extent without inventing a bind pose.
|
||||
pub fn allocate(&mut self, count: usize) -> Buffer<T> {
|
||||
self.insert(vec![None; count])
|
||||
}
|
||||
pub fn initialized(&mut self, values: impl IntoIterator<Item = T>) -> Buffer<T> {
|
||||
self.insert(values.into_iter().map(Some).collect())
|
||||
}
|
||||
fn insert(&mut self, allocation: Vec<Option<T>>) -> Buffer<T> {
|
||||
let slot = self.allocations.len();
|
||||
self.allocations.push(allocation);
|
||||
Buffer {
|
||||
slot,
|
||||
owner: self.owner,
|
||||
marker: PhantomData,
|
||||
}
|
||||
}
|
||||
fn range(
|
||||
&self,
|
||||
source: BoneSlice<T>,
|
||||
count: usize,
|
||||
) -> Result<std::ops::Range<usize>, BufferError> {
|
||||
if source.buffer.owner != self.owner {
|
||||
return Err(BufferError::UnknownBuffer);
|
||||
}
|
||||
let allocation = self
|
||||
.allocations
|
||||
.get(source.buffer.slot)
|
||||
.ok_or(BufferError::UnknownBuffer)?;
|
||||
let end = source
|
||||
.first_bone
|
||||
.checked_add(count)
|
||||
.ok_or(BufferError::RangeOutsideAllocation)?;
|
||||
if end > allocation.len() {
|
||||
return Err(BufferError::RangeOutsideAllocation);
|
||||
}
|
||||
Ok(source.first_bone..end)
|
||||
}
|
||||
pub fn read(&self, source: BoneSlice<T>, count: usize) -> Result<Vec<T>, BufferError> {
|
||||
if count == 0 {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let range = self.range(source, count)?;
|
||||
range
|
||||
.map(|bone| {
|
||||
self.allocations[source.buffer.slot][bone]
|
||||
.ok_or(BufferError::UninitializedBone { bone })
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
pub(crate) fn copy(
|
||||
&mut self,
|
||||
source: BoneSlice<T>,
|
||||
destination: BoneSlice<T>,
|
||||
count: usize,
|
||||
) -> Result<(), BufferError> {
|
||||
if count == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
let from = self.range(source, count)?;
|
||||
let to = self.range(destination, count)?;
|
||||
if source.buffer.slot == destination.buffer.slot {
|
||||
if from == to {
|
||||
return Ok(());
|
||||
}
|
||||
if from.start < to.end && to.start < from.end {
|
||||
return Err(BufferError::UnsupportedPartialOverlap);
|
||||
}
|
||||
}
|
||||
// Copy validity alongside the value: native SET does not initialize
|
||||
// unwritten bones. Such exports remain unreadable, rather than zero-filled.
|
||||
for (from, to) in from.zip(to) {
|
||||
self.allocations[destination.buffer.slot][to] =
|
||||
self.allocations[source.buffer.slot][from];
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
#[derive(Default)]
|
||||
pub struct PoseBuffers {
|
||||
pub sqt: BoneBuffers<Sqt>,
|
||||
pub matrices: BoneBuffers<NativeMatrix>,
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
//! Typed animation-command storage and recovered workers.
|
||||
pub mod batch;
|
||||
pub mod buffers;
|
||||
pub mod set_data;
|
||||
@@ -0,0 +1,60 @@
|
||||
//! ACSsetData_job (TU3 0x828D80B8), expressed in bones and typed allocations.
|
||||
use super::buffers::{BoneBuffers, BoneSlice, BufferError, PoseBuffers};
|
||||
use crate::animation::output::{NativeMatrix, Sqt};
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum InternalUpdate<T> {
|
||||
/// Native opcode 12: byte-sized partial SET count.
|
||||
Set {
|
||||
source: BoneSlice<T>,
|
||||
bone_count: u8,
|
||||
},
|
||||
/// Every other opcode skips the SET copy, but still permits extraction.
|
||||
Preserve,
|
||||
}
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct CopyCommand<T> {
|
||||
pub update: InternalUpdate<T>,
|
||||
pub internal: BoneSlice<T>,
|
||||
pub external: Option<BoneSlice<T>>,
|
||||
}
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum SetDataCommand {
|
||||
Sqt(CopyCommand<Sqt>),
|
||||
Matrix(CopyCommand<NativeMatrix>),
|
||||
}
|
||||
/// 828D7E08 uses the low byte of numExtBones when the encoded byte is zero.
|
||||
pub fn resolve_set_count(encoded_count: u8, external_bone_count: u32) -> u8 {
|
||||
if encoded_count == 0 {
|
||||
external_bone_count as u8
|
||||
} else {
|
||||
encoded_count
|
||||
}
|
||||
}
|
||||
pub(crate) fn execute(
|
||||
buffers: &mut PoseBuffers,
|
||||
bone_count: u16,
|
||||
commands: &[SetDataCommand],
|
||||
) -> Result<(), BufferError> {
|
||||
for command in commands {
|
||||
match command {
|
||||
SetDataCommand::Sqt(command) => copy(&mut buffers.sqt, bone_count, command)?,
|
||||
SetDataCommand::Matrix(command) => copy(&mut buffers.matrices, bone_count, command)?,
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn copy<T: Copy>(
|
||||
buffers: &mut BoneBuffers<T>,
|
||||
skeleton_bones: u16,
|
||||
command: &CopyCommand<T>,
|
||||
) -> Result<(), BufferError> {
|
||||
if let InternalUpdate::Set { source, bone_count } = command.update {
|
||||
buffers.copy(source, command.internal, usize::from(bone_count))?;
|
||||
}
|
||||
if let Some(external) = command.external {
|
||||
// Read internal after SET, including writes through earlier aliases.
|
||||
buffers.copy(command.internal, external, usize::from(skeleton_bones))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
//! Stock riding crouch/absorption and auto-pump, TU3 82BB7DA0/82BB8818.
|
||||
use crate::point_graph::PointGraph;
|
||||
|
||||
#[derive(Clone,Debug)]
|
||||
pub struct Settings {
|
||||
pub maximum_height:PointGraph<8>,pub absorption_upforce:PointGraph<8>,pub pump_maxspeed:PointGraph<8>,
|
||||
pub minimum_height:f32,pub maximum_ratio:f32,pub maximum_delta_delta:f32,pub maximum_delta:f32,pub input_blend:f32,
|
||||
pub pump_vertical_speed:f32,pub maximum_crouch_from_deck:f32,pub skateboard_damping:f32,pub maximum_force:f32,pub maximum_ground_force:f32,pub absorption_factor:f32,
|
||||
pub auto_pump:AutoPumpSettings,
|
||||
}
|
||||
#[derive(Clone,Debug)]
|
||||
pub struct AutoPumpSettings {
|
||||
pub maximum_crouch:PointGraph<4>,pub sufficient_crouch:f32,pub standing_threshold:f32,pub rise_speed:f32,pub pump_speed:f32,
|
||||
pub potential_threshold:f32,pub potential_blend:f32,pub intent_magnitude_start:f32,pub intent_angle_region:f32,pub crouch_time:f32,pub crouch_speed:f32,
|
||||
}
|
||||
/// Resolved physical publication fields. Offsets stay explicit until the
|
||||
/// producer's domain identity is verified; callers must publish actual values.
|
||||
#[derive(Clone,Copy,Debug)]
|
||||
pub struct Physical {
|
||||
pub body_84:f32,pub body_164:f32,pub body_188:f32,
|
||||
pub force_516:f32,pub ground_force_520:f32,pub minimum_crouch_528:f32,pub deck_angle_532:f32,
|
||||
pub animation_height_72:f32,
|
||||
}
|
||||
#[derive(Clone,Copy,Debug)]
|
||||
pub struct Intents {
|
||||
pub auto_pump_angle:Option<f32>,pub auto_pump_magnitude:Option<f32>,pub crouch:Option<f32>,pub hard_turn_crouch:Option<f32>,pub manual:Option<f32>,
|
||||
/// SkaterMotionGraph interface+108, used by both manual-crouch branches.
|
||||
pub motion_flag_108:bool,
|
||||
}
|
||||
#[derive(Clone,Copy,Debug)]
|
||||
pub struct State {
|
||||
absorbed_velocity:f32,filtered_input:f32,pub fraction:f32,delta_fraction:f32,player_pumping:bool,
|
||||
auto_fraction:f32,auto_state:u32,auto_time:f32,previous_angle:f32,previous_magnitude:f32,filtered_potential:f32,pump_idle_time:f32,
|
||||
}
|
||||
#[derive(Clone,Copy,Debug)]
|
||||
pub struct Output {pub height:f32,pub new_auto_pump:bool,pub player_controlled_pump:bool}
|
||||
impl State {
|
||||
pub fn begin(physical:Physical,settings:&Settings)->Self {
|
||||
let maximum=settings.maximum_height.evaluate(physical.body_164);
|
||||
let fraction=1.0-unit((physical.animation_height_72-settings.minimum_height)/(maximum-settings.minimum_height));
|
||||
Self {absorbed_velocity:0.0,filtered_input:0.0,fraction,delta_fraction:0.0,player_pumping:false,auto_fraction:0.0,auto_state:0,auto_time:0.0,previous_angle:0.0,previous_magnitude:0.0,filtered_potential:0.0,pump_idle_time:0.5}
|
||||
}
|
||||
pub fn update(&mut self,p:Physical,intents:Intents,dt:f32,s:&Settings)->Output {
|
||||
let restoring=-s.absorption_upforce.evaluate(1.0-self.fraction);
|
||||
let mut board=bound(-p.force_516/s.maximum_force,-1.0,1.0);
|
||||
self.absorbed_velocity+=p.body_84;
|
||||
self.absorbed_velocity=s.skateboard_damping*self.absorbed_velocity;
|
||||
if self.absorbed_velocity<0.0 {board=-board;}
|
||||
let ground=bound(-p.ground_force_520/s.maximum_ground_force,-1.0,1.0);
|
||||
let mut absorption=if board<0.0 {if ground>0.0 {ground+board} else {board}} else {maximum(board,ground)};
|
||||
if absorption<0.0 {if !(absorption<restoring) {absorption=restoring;}} else {absorption+=restoring;}
|
||||
let angle=intents.auto_pump_angle.unwrap_or(0.0);
|
||||
let magnitude=intents.auto_pump_magnitude.unwrap_or(0.0);
|
||||
let a=&s.auto_pump;
|
||||
let started=self.previous_angle.abs()<a.intent_angle_region && self.previous_magnitude<a.intent_magnitude_start
|
||||
&& angle.abs()<a.intent_angle_region && magnitude>a.intent_magnitude_start;
|
||||
self.previous_angle=angle;self.previous_magnitude=magnitude;
|
||||
self.filtered_potential=p.body_188.mul_add(a.potential_blend,(1.0-a.potential_blend)*self.filtered_potential);
|
||||
let sufficient_potential=!(self.filtered_potential<a.potential_threshold);
|
||||
if started {self.auto_time=0.0;self.auto_state=1;}
|
||||
if self.auto_state==1 && self.auto_time>a.crouch_time {self.auto_state=2;}
|
||||
let new_auto_pump=(self.auto_state==1 || self.auto_state==2) && self.fraction>a.sufficient_crouch && sufficient_potential;
|
||||
if new_auto_pump {self.auto_state=3;}
|
||||
if (self.auto_state==2 || self.auto_state==3) && !(self.fraction>a.standing_threshold+p.minimum_crouch_528) {self.auto_state=0;}
|
||||
match self.auto_state {
|
||||
0=>self.auto_fraction=0.0,
|
||||
1=>{
|
||||
self.auto_fraction=minimum(a.maximum_crouch.evaluate(p.body_164),a.crouch_speed+self.auto_fraction);
|
||||
self.auto_time=minimum(a.crouch_time+1.0,dt+self.auto_time);
|
||||
},
|
||||
2=>self.auto_fraction=nonnegative(self.auto_fraction-a.rise_speed),
|
||||
3=>self.auto_fraction=nonnegative(self.auto_fraction-a.pump_speed),
|
||||
_=>{},
|
||||
}
|
||||
let mut crouch=intents.crouch.unwrap_or(0.0);
|
||||
if !intents.motion_flag_108 {crouch=maximum(crouch,intents.hard_turn_crouch.unwrap_or(0.0));}
|
||||
if intents.motion_flag_108 && intents.manual.is_some_and(|value|value<0.0) {crouch=maximum(crouch,f32::from_bits(0x3ecccccd));}
|
||||
let blend=unit(s.input_blend);
|
||||
self.filtered_input=maximum(crouch,self.auto_fraction).mul_add(blend,(1.0-blend)*self.filtered_input);
|
||||
if self.filtered_input.abs()<f32::from_bits(0x3ba3d70a) {self.filtered_input=0.0;}
|
||||
let mut delta=s.absorption_factor*absorption;
|
||||
if crouch>0.0 || self.auto_fraction>0.0 || (self.player_pumping && self.fraction>p.minimum_crouch_528) {
|
||||
self.player_pumping=true;
|
||||
delta=maximum(minimum(1.0,maximum(p.minimum_crouch_528,self.filtered_input))-self.fraction,-s.maximum_delta);
|
||||
self.pump_idle_time=0.0;
|
||||
} else {
|
||||
self.player_pumping=false;
|
||||
// Native literal820849C8, deliberately not graph delta time.
|
||||
self.pump_idle_time+=f32::from_bits(0x3c888889);
|
||||
}
|
||||
let player_controlled_pump=self.pump_idle_time<0.5;
|
||||
let minimum_delta=-(s.pump_maxspeed.evaluate(1.0-self.fraction)*s.pump_vertical_speed);
|
||||
let maximum_delta=s.pump_maxspeed.evaluate(self.fraction)*s.pump_vertical_speed;
|
||||
let delta=bound(delta,minimum_delta,maximum_delta);
|
||||
self.delta_fraction=bound(delta,self.delta_fraction-s.maximum_delta_delta,s.maximum_delta_delta+self.delta_fraction);
|
||||
self.fraction=bound(self.fraction+self.delta_fraction,p.minimum_crouch_528,1.0);
|
||||
let from_deck=s.maximum_crouch_from_deck*p.deck_angle_532;
|
||||
if from_deck>self.fraction {self.fraction=from_deck;}
|
||||
let height_ratio=unit((-s.maximum_ratio).mul_add(self.fraction,1.0));
|
||||
let maximum_height=s.maximum_height.evaluate(p.body_164);
|
||||
Output {height:(maximum_height-s.minimum_height).mul_add(height_ratio,s.minimum_height),new_auto_pump,player_controlled_pump}
|
||||
}
|
||||
}
|
||||
fn minimum(a:f32,b:f32)->f32 {if a-b>=0.0 {b} else {a}}
|
||||
fn maximum(a:f32,b:f32)->f32 {if a-b>=0.0 {a} else {b}}
|
||||
fn bound(v:f32,lo:f32,hi:f32)->f32 {minimum(hi,maximum(lo,v))}
|
||||
fn nonnegative(v:f32)->f32 {if -v>=0.0 {0.0} else {v}}
|
||||
fn unit(v:f32)->f32 {minimum(1.0,nonnegative(v))}
|
||||
@@ -0,0 +1,71 @@
|
||||
//! SkeletonIK::BlendTransforms82BEEEB8.
|
||||
use super::{
|
||||
math::{interpolate, interpolate_affine},
|
||||
status::{LimbStatus, Mode},
|
||||
transforms::{LimbBinding, LimbFrames},
|
||||
};
|
||||
use crate::physics::{
|
||||
skeleton_animation_record::{
|
||||
AnimationPartTransform as Transform, compose_affine, transform_point,
|
||||
},
|
||||
skeleton_root::orthonormalize,
|
||||
};
|
||||
|
||||
pub fn update(
|
||||
statuses: &[LimbStatus; 4],
|
||||
frames: &mut [LimbFrames; 4],
|
||||
bindings: &[LimbBinding; 4],
|
||||
physical_board: &Transform,
|
||||
animation_to_world: &Transform,
|
||||
animated_board: &Transform,
|
||||
) {
|
||||
// IK160 supplies only the physical board rotation. Translation comes from
|
||||
// the actual animation record's board transformed through IK32.
|
||||
let mut board = *physical_board;
|
||||
board[3] = transform_point(animation_to_world, animated_board[3]);
|
||||
for ((status, frame), binding) in statuses.iter().zip(frames).zip(bindings) {
|
||||
match status.mode {
|
||||
Mode::Disabled => continue,
|
||||
Mode::OnDeck | Mode::External | Mode::Local => {}
|
||||
}
|
||||
if status.board_blend > 0.0 {
|
||||
let target = compose_affine(&board, &frame.board);
|
||||
frame.world = board_blend(&frame.world, &target, status.board_blend);
|
||||
if binding.parent_part.is_some() {
|
||||
let target = compose_affine(&board, &frame.parent_board);
|
||||
frame.parent_world = board_blend(&frame.parent_world, &target, status.board_blend);
|
||||
}
|
||||
}
|
||||
if matches!(status.mode, Mode::External | Mode::Local) {
|
||||
if status.external_blend >= 1.0 {
|
||||
frame.world = frame.external_world;
|
||||
frame.parent_world = frame.external_parent_world;
|
||||
} else {
|
||||
// The native external stage also updates the retained parent
|
||||
// frame for a limb without a parent, including at zero weight.
|
||||
frame.world =
|
||||
interpolate_affine(&frame.world, &frame.external_world, status.external_blend);
|
||||
frame.parent_world = interpolate_affine(
|
||||
&frame.parent_world,
|
||||
&frame.external_parent_world,
|
||||
status.external_blend,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn board_blend(original: &Transform, target: &Transform, weight: f32) -> Transform {
|
||||
let translation = core::array::from_fn(|lane| {
|
||||
target[3][lane].mul_add(weight, original[3][lane] * (1.0 - weight))
|
||||
});
|
||||
// Each source frame is orthonormalized independently in Z/Y/X order before
|
||||
// the matrix interpolation call; translation is replaced afterwards.
|
||||
let mut a = orthonormalize(*original);
|
||||
let mut b = orthonormalize(*target);
|
||||
a[3] = [0.0; 4];
|
||||
b[3] = [0.0; 4];
|
||||
let (mut output, _) = interpolate(&a, &b, weight);
|
||||
output[3] = translation;
|
||||
output
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
//! Foot contact history and correction82BEFC80, helpers82BF04C8/82BF0558.
|
||||
use super::{
|
||||
status::{LimbStatus, Mode},
|
||||
transforms::LimbFrames,
|
||||
};
|
||||
use crate::physics::skeleton_animation_record::{
|
||||
AnimationPartTransform as Transform, transform_point,
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct FootContact {
|
||||
/// SkeletonIK3104:0 pending,1 previous query hit,2 previous query missed.
|
||||
pub query_state: u32,
|
||||
pub position: [f32; 4],
|
||||
pub desired_offset: f32,
|
||||
pub offset: f32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct ContactState {
|
||||
pub feet: [FootContact; 2],
|
||||
///3184 persists until Reset;3185 is cleared at each UpdateContact entry.
|
||||
pub support_failed: bool,
|
||||
pub support_failed_this_update: bool,
|
||||
}
|
||||
|
||||
pub struct ContactInput<'a> {
|
||||
pub flags_2468: u32,
|
||||
pub contact_bone: usize,
|
||||
pub foot_bones: [usize; 2],
|
||||
pub hips_world_position: [f32; 4],
|
||||
pub inverse_board: &'a Transform,
|
||||
pub board: &'a Transform,
|
||||
/// Actual cached line tests from Processed960/1008. Current observations
|
||||
/// replace the retained query only AFTER the previous query is consumed.
|
||||
pub current_contacts: [Option<[f32; 4]>; 2],
|
||||
}
|
||||
|
||||
impl ContactState {
|
||||
pub fn update(
|
||||
&mut self,
|
||||
statuses: &[LimbStatus; 4],
|
||||
frames: &mut [LimbFrames; 4],
|
||||
input: ContactInput<'_>,
|
||||
) {
|
||||
self.support_failed_this_update = false;
|
||||
let requires_support = input.flags_2468 & 0x4200_0000 != 0;
|
||||
for foot in 0..2 {
|
||||
let contact = &mut self.feet[foot];
|
||||
let frame = &mut frames[foot];
|
||||
let mut failed = false;
|
||||
if statuses[foot].mode == Mode::OnDeck && input.contact_bone == input.foot_bones[foot] {
|
||||
match contact.query_state {
|
||||
1 => {
|
||||
let local = transform_point(input.inverse_board, contact.position);
|
||||
let height = local[1] + f32::from_bits(0x3DA8_F5C3);
|
||||
if height <= f32::from_bits(0x3E99_999A) {
|
||||
contact.desired_offset = if input.flags_2468 & 0x0180_0000 == 0 {
|
||||
limit_offset(height)
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
approach(contact);
|
||||
let distance = distance_to_contact(
|
||||
contact,
|
||||
frame,
|
||||
input.board,
|
||||
input.inverse_board,
|
||||
);
|
||||
//82BEFFB4..FFCC: fix modest penetration immediately;
|
||||
//larger separations retain the bounded transition.
|
||||
if distance < 0.0 && distance > f32::from_bits(0xBE99_999A) {
|
||||
contact.offset -= distance;
|
||||
}
|
||||
add_offset(frame, input.board, contact.offset);
|
||||
failed = requires_support
|
||||
&& distance > f32::from_bits(0x3CF5_C28F)
|
||||
&& contact.offset < f32::from_bits(0xBC23_D70A);
|
||||
} else {
|
||||
contact.desired_offset = limit_offset(contact.desired_offset);
|
||||
approach(contact);
|
||||
add_offset(frame, input.board, contact.offset);
|
||||
failed = requires_support
|
||||
|| input.hips_world_position[1] > f32::from_bits(0x3F33_3333);
|
||||
}
|
||||
}
|
||||
0 | 2 => {
|
||||
contact.desired_offset = limit_offset(contact.desired_offset);
|
||||
approach(contact);
|
||||
add_offset(frame, input.board, contact.offset);
|
||||
failed = contact.query_state == 2 && requires_support;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if let Some(position) = input.current_contacts[foot] {
|
||||
contact.position = position;
|
||||
contact.query_state = 1;
|
||||
} else {
|
||||
contact.query_state = 2;
|
||||
}
|
||||
} else {
|
||||
contact.query_state = 0;
|
||||
contact.desired_offset = 0.0;
|
||||
approach(contact);
|
||||
add_offset(frame, input.board, contact.offset);
|
||||
}
|
||||
if failed {
|
||||
self.support_failed = true;
|
||||
self.support_failed_this_update = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
///82BF0558: compare both positions in inverse-board space, retaining the
|
||||
///separate point transforms and the .025 foot-surface distance adjustment.
|
||||
fn distance_to_contact(
|
||||
contact: &FootContact,
|
||||
frame: &LimbFrames,
|
||||
board: &Transform,
|
||||
inverse: &Transform,
|
||||
) -> f32 {
|
||||
let local_contact = transform_point(inverse, contact.position);
|
||||
let delta = contact_offset(board, contact.offset);
|
||||
let foot = core::array::from_fn(|i| frame.world[3][i] + delta[i]);
|
||||
let local_foot = transform_point(inverse, foot);
|
||||
(local_foot[1] - f32::from_bits(0x3CCC_CCCD)) - local_contact[1]
|
||||
}
|
||||
|
||||
fn add_offset(frame: &mut LimbFrames, board: &Transform, offset: f32) {
|
||||
let delta = contact_offset(board, offset);
|
||||
for (i, value) in delta.into_iter().enumerate() {
|
||||
frame.world[3][i] += value;
|
||||
frame.parent_world[3][i] += value;
|
||||
}
|
||||
}
|
||||
fn contact_offset(board: &Transform, offset: f32) -> [f32; 4] {
|
||||
core::array::from_fn(|i| {
|
||||
let x = board[0][i] * 0.0;
|
||||
let y = board[1][i].mul_add(offset, x);
|
||||
board[2][i].mul_add(0.0, y)
|
||||
})
|
||||
}
|
||||
fn select(test: f32, nonnegative: f32, negative: f32) -> f32 {
|
||||
if test >= 0.0 { nonnegative } else { negative }
|
||||
}
|
||||
fn limit_offset(value: f32) -> f32 {
|
||||
let min = f32::from_bits(0xBDCC_CCCD);
|
||||
let max = f32::from_bits(0x3DCC_CCCD);
|
||||
let lower = select(min - value, min, value);
|
||||
select(max - lower, lower, max)
|
||||
}
|
||||
fn approach(contact: &mut FootContact) {
|
||||
let min = f32::from_bits(0xBC75_C28F);
|
||||
let max = f32::from_bits(0x3C75_C28F);
|
||||
let delta = contact.desired_offset - contact.offset;
|
||||
let lower = select(min - delta, min, delta);
|
||||
contact.offset = select(max - lower, lower, max) + contact.offset;
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
//! SkeletonIK::UpdateDriveFrames82BF0198 and its solve/mapping chain.
|
||||
use super::{
|
||||
math::{Vector, cross, length, normalize},
|
||||
status::{LimbStatus, Mode},
|
||||
transforms::{LimbBinding, LimbFrames, inverse_rigid},
|
||||
two_bone::{self, AngleLimits, SolveResult},
|
||||
};
|
||||
use crate::physics::{
|
||||
native_arithmetic::dot3,
|
||||
skeleton_animation_record::{
|
||||
AnimationPartTransform as Transform, compose_affine, transform_point,
|
||||
},
|
||||
skeleton_root::orthonormalize,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Geometry {
|
||||
/// SkeletonData4704: nearest ancestor represented by a physical part.
|
||||
pub parents: [Option<usize>; 24],
|
||||
/// SkeletonData1536: native cofactor inverse of the authored part frames.
|
||||
pub inverse_part_frames: [Transform; 24],
|
||||
}
|
||||
impl Geometry {
|
||||
/// SkeletonData Init82BD6C40 builds these inverses once from the real
|
||||
/// PhysicsParamBoneData frames. It does not assume an orthogonal basis.
|
||||
pub fn new(
|
||||
parents: [Option<usize>; 24],
|
||||
part_frames: &[Transform; 24],
|
||||
) -> Result<Self, String> {
|
||||
for part in 0..24 {
|
||||
let mut parent = parents[part];
|
||||
let mut visited = [false; 24];
|
||||
visited[part] = true;
|
||||
while let Some(index) = parent {
|
||||
if index >= 24 || visited[index] {
|
||||
return Err(format!(
|
||||
"Invalid physical ancestor chain for IK part {part}"
|
||||
));
|
||||
}
|
||||
visited[index] = true;
|
||||
parent = parents[index];
|
||||
}
|
||||
}
|
||||
Ok(Self {
|
||||
parents,
|
||||
inverse_part_frames: part_frames.map(|frame| super::math::inverse_affine(&frame)),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn validate_limbs(&self, bindings: &[LimbBinding; 4]) -> Result<(), String> {
|
||||
for binding in bindings {
|
||||
let end = binding.parent_part.unwrap_or(binding.part);
|
||||
if binding.part >= 24
|
||||
|| end >= 24
|
||||
|| self.parents[end].and_then(|p| self.parents[p]).is_none()
|
||||
{
|
||||
return Err(format!(
|
||||
"IK part {end} has no two-bone physical ancestor chain"
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(
|
||||
statuses: &[LimbStatus; 4],
|
||||
frames: &mut [LimbFrames; 4],
|
||||
bindings: &[LimbBinding; 4],
|
||||
geometry: &Geometry,
|
||||
original_animation: &[Transform; 24],
|
||||
drives: &mut [Transform; 24],
|
||||
world_to_animation: &Transform,
|
||||
limits: AngleLimits,
|
||||
) -> [bool; 4] {
|
||||
let mut updated = [false; 4];
|
||||
for limb in 0..4 {
|
||||
if statuses[limb].mode == Mode::Disabled {
|
||||
continue;
|
||||
}
|
||||
let binding = bindings[limb];
|
||||
let target = compose_affine(world_to_animation, &frames[limb].world);
|
||||
let parent_target = binding
|
||||
.parent_part
|
||||
.map(|_| compose_affine(world_to_animation, &frames[limb].parent_world));
|
||||
let driven = binding.parent_part.unwrap_or(binding.part);
|
||||
let driven_target = parent_target.as_ref().unwrap_or(&target);
|
||||
let endpoint = transform_point(driven_target, geometry.inverse_part_frames[driven][3]);
|
||||
let offset = subtract(endpoint, original_animation[driven][3]);
|
||||
//82BF01DC..1F4: these are maximum root-to-target distances passed
|
||||
//through f1 into82BD41B0, not animation blend weights.
|
||||
let maximum_distance = if limb < 2 {
|
||||
f32::from_bits(0x3F4C_CCCD)
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
if let Some(residual) = solve_drives(
|
||||
driven,
|
||||
offset,
|
||||
maximum_distance,
|
||||
geometry,
|
||||
original_animation,
|
||||
drives,
|
||||
limits,
|
||||
) {
|
||||
drives[binding.part] = target;
|
||||
add_translation(&mut drives[binding.part], residual);
|
||||
// The source also adds the same returned delta to retained world
|
||||
//targets. Do not insert an extra rotation absent from82BF0458.
|
||||
add_translation(&mut frames[limb].world, residual);
|
||||
if let Some(parent) = binding.parent_part {
|
||||
drives[parent] = parent_target.unwrap();
|
||||
add_translation(&mut drives[parent], residual);
|
||||
add_translation(&mut frames[limb].parent_world, residual);
|
||||
}
|
||||
updated[limb] = true;
|
||||
}
|
||||
}
|
||||
updated
|
||||
}
|
||||
|
||||
///82BF0FA8, using original animation joint centers and mutable volume frames.
|
||||
fn solve_drives(
|
||||
end_part: usize,
|
||||
offset: Vector,
|
||||
maximum_distance: f32,
|
||||
geometry: &Geometry,
|
||||
original: &[Transform; 24],
|
||||
drives: &mut [Transform; 24],
|
||||
limits: AngleLimits,
|
||||
) -> Option<Vector> {
|
||||
let middle_part = geometry.parents[end_part]?;
|
||||
let root_part = geometry.parents[middle_part]?;
|
||||
let [root, middle, end] = [
|
||||
original[root_part][3],
|
||||
original[middle_part][3],
|
||||
original[end_part][3],
|
||||
];
|
||||
let requested: Vector = core::array::from_fn(|i| end[i] + offset[i]);
|
||||
let mut target = clamp_point(requested, root, maximum_distance);
|
||||
let mut solved_middle = [0.0; 4];
|
||||
if two_bone::solve(
|
||||
root,
|
||||
middle,
|
||||
end,
|
||||
&mut solved_middle,
|
||||
&mut target,
|
||||
limits,
|
||||
true,
|
||||
2,
|
||||
) == SolveResult::Invalid
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let residual = subtract(target, requested);
|
||||
let original_normal = cross(subtract(root, middle), subtract(end, middle));
|
||||
let solved_normal = cross(
|
||||
subtract(root, solved_middle),
|
||||
subtract(target, solved_middle),
|
||||
);
|
||||
let normal = normalize_safe(core::array::from_fn(|i| {
|
||||
original_normal[i] + solved_normal[i]
|
||||
}));
|
||||
let middle_mapping = line_mapping(middle, end, normal, solved_middle, target, normal)?;
|
||||
let root_mapping = line_mapping(root, middle, normal, root, solved_middle, normal)?;
|
||||
drives[root_part] = orthonormalize(compose_affine(&root_mapping, &drives[root_part]));
|
||||
drives[middle_part] = orthonormalize(compose_affine(&middle_mapping, &drives[middle_part]));
|
||||
Some(residual)
|
||||
}
|
||||
|
||||
///82BD41B0 clamps a point relative to the original chain root using scalar
|
||||
///division after the two-refinement length; it is not limit_length's reciprocal.
|
||||
fn clamp_point(point: Vector, origin: Vector, maximum: f32) -> Vector {
|
||||
let delta = subtract(point, origin);
|
||||
let distance = length(delta);
|
||||
if maximum >= distance {
|
||||
point
|
||||
} else {
|
||||
let scale = maximum / distance;
|
||||
core::array::from_fn(|i| delta[i].mul_add(scale, origin[i]))
|
||||
}
|
||||
}
|
||||
|
||||
///82BF1F20, two82BF1D28 frames and target * rigidInverse(original).
|
||||
pub(super) fn line_mapping(
|
||||
start: Vector,
|
||||
end: Vector,
|
||||
normal: Vector,
|
||||
new_start: Vector,
|
||||
new_end: Vector,
|
||||
new_normal: Vector,
|
||||
) -> Option<Transform> {
|
||||
let original = line_frame(start, end, normal)?;
|
||||
let target = line_frame(new_start, new_end, new_normal)?;
|
||||
Some(compose_affine(&target, &inverse_rigid(&original)))
|
||||
}
|
||||
|
||||
fn line_frame(start: Vector, end: Vector, normal: Vector) -> Option<Transform> {
|
||||
let line = subtract(end, start);
|
||||
let across = cross(line, normal);
|
||||
let third = cross(across, line);
|
||||
let x = normalize_safe(across);
|
||||
let z = normalize_safe(third);
|
||||
let y = normalize_safe(line);
|
||||
let product = (dot3(z, z) * dot3(x, x)) * dot3(y, y);
|
||||
//830BD300 <-821647E0, initializer82F82690.
|
||||
if product > f32::from_bits(0x3780_0000) {
|
||||
Some([x, y, z, start])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn normalize_safe(value: Vector) -> Vector {
|
||||
let magnitude = length(value);
|
||||
let normalized = normalize(value);
|
||||
//830BD350 <-82181A88; no arbitrary fallback axis for a degenerate line.
|
||||
if magnitude > f32::from_bits(0x3586_37BD) {
|
||||
normalized
|
||||
} else {
|
||||
[0.0; 4]
|
||||
}
|
||||
}
|
||||
fn subtract(a: Vector, b: Vector) -> Vector {
|
||||
core::array::from_fn(|i| a[i] - b[i])
|
||||
}
|
||||
fn add_translation(frame: &mut Transform, offset: Vector) {
|
||||
for lane in 0..4 {
|
||||
frame[3][lane] += offset[lane];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
//! SkeletonIK::CalculateExternalIKOffset82BEE390.
|
||||
use super::{
|
||||
math::{axis_rotation, cross, length, limit_length, reciprocal},
|
||||
status::{LimbStatus, Mode},
|
||||
transforms::{LimbBinding, LimbFrames, inverse_rigid},
|
||||
};
|
||||
use crate::{
|
||||
physics::skeleton_animation_record::{
|
||||
AnimationPartTransform as Transform, IDENTITY, compose_affine, transform_point,
|
||||
},
|
||||
trigonometry::asin,
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct ExternalTarget {
|
||||
/// SkeletonIK544 /608; setters distinguish the supplied coordinate space.
|
||||
pub world_position: [f32; 4],
|
||||
pub animation_position: [f32; 4],
|
||||
/// SkeletonIK736,800,828. The normal is measured against world up.
|
||||
pub normal: [f32; 4],
|
||||
pub normal_set: bool,
|
||||
pub normal_blend: f32,
|
||||
}
|
||||
|
||||
pub fn update(
|
||||
target: &mut ExternalTarget,
|
||||
status: &mut LimbStatus,
|
||||
frames: &mut LimbFrames,
|
||||
binding: LimbBinding,
|
||||
animation: &[Transform; 24],
|
||||
animation_to_world: &Transform,
|
||||
world_to_animation: &Transform,
|
||||
) {
|
||||
let animated = &animation[binding.part];
|
||||
let original = compose_affine(animation_to_world, animated);
|
||||
let mut rotation = IDENTITY;
|
||||
if target.normal_blend > 0.0 {
|
||||
// Native v16 is the fixed Y basis82139A20, not the animated limb's up.
|
||||
let axis = cross([0.0, 1.0, 0.0, 0.0], target.normal);
|
||||
let magnitude = length(axis);
|
||||
if magnitude > 0.05 {
|
||||
let inverse = reciprocal(magnitude, 2);
|
||||
let axis = axis.map(|v| inverse * v);
|
||||
let angle = asin(magnitude);
|
||||
let angle = if -0.71 - angle >= 0.0 { -0.71 } else { angle };
|
||||
let angle = if 0.71 - angle >= 0.0 { angle } else { 0.71 };
|
||||
rotation = axis_rotation(axis, angle * target.normal_blend);
|
||||
}
|
||||
if !target.normal_set {
|
||||
let next = target.normal_blend - 0.2;
|
||||
target.normal_blend = if -next >= 0.0 { 0.0 } else { next };
|
||||
}
|
||||
}
|
||||
if status.mode == Mode::Local {
|
||||
let weight = status.target_blend;
|
||||
let desired: [f32; 4] = core::array::from_fn(|lane| {
|
||||
(target.animation_position[lane] - animated[3][lane]).mul_add(
|
||||
weight,
|
||||
status.external_target_local_delta[lane] * (1.0 - weight),
|
||||
)
|
||||
});
|
||||
let change = limit_length(
|
||||
core::array::from_fn(|lane| desired[lane] - status.external_target_local_delta[lane]),
|
||||
0.025,
|
||||
);
|
||||
for lane in 0..4 {
|
||||
status.external_target_local_delta[lane] += change[lane];
|
||||
// The source publishes desired before its history limit. Do not
|
||||
// replace this with the separately limited retained delta.
|
||||
target.animation_position[lane] = animated[3][lane] + desired[lane];
|
||||
}
|
||||
target.world_position = transform_point(animation_to_world, target.animation_position);
|
||||
} else {
|
||||
target.animation_position = transform_point(world_to_animation, target.world_position);
|
||||
}
|
||||
let offset = limit_length(
|
||||
core::array::from_fn(|lane| target.world_position[lane] - original[3][lane]),
|
||||
0.7,
|
||||
);
|
||||
let mut from_origin = IDENTITY;
|
||||
from_origin[3] = original[3].map(|v| f32::from_bits(v.to_bits() ^ 0x8000_0000));
|
||||
let rotated = compose_affine(&rotation, &from_origin);
|
||||
let mut to_origin = IDENTITY;
|
||||
to_origin[3] = core::array::from_fn(|lane| original[3][lane] + offset[lane]);
|
||||
let adjustment = compose_affine(&to_origin, &rotated);
|
||||
frames.external_world = compose_affine(&adjustment, &original);
|
||||
if let Some(parent) = binding.parent_part {
|
||||
let relative = compose_affine(&inverse_rigid(animated), &animation[parent]);
|
||||
frames.external_parent_world = compose_affine(&frames.external_world, &relative);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
//! Matrix interpolation used by SkeletonIK, TU3 82BD3150/82BD2E60.
|
||||
//! The shared Xenon estimate primitives remain host approximations pending
|
||||
//! hardware comparison; the recovered branch and refinement order is retained.
|
||||
use crate::{
|
||||
input::angle::atan,
|
||||
physics::{
|
||||
board_motion_output::inverse_length_squared,
|
||||
native_arithmetic::{dot3, reciprocal_estimate},
|
||||
skeleton_animation_record::{AnimationPartTransform as Transform, IDENTITY},
|
||||
},
|
||||
trigonometry::sin_cos,
|
||||
};
|
||||
|
||||
pub(super) type Vector = [f32; 4];
|
||||
|
||||
pub(super) fn reciprocal(value: f32, refinements: usize) -> f32 {
|
||||
let mut inverse = reciprocal_estimate(value);
|
||||
for _ in 0..refinements {
|
||||
let error = (-inverse).mul_add(value, 1.0);
|
||||
inverse = inverse.mul_add(error, inverse);
|
||||
}
|
||||
inverse
|
||||
}
|
||||
|
||||
pub(super) fn length(value: Vector) -> f32 {
|
||||
let squared = dot3(value, value);
|
||||
let result = squared * inverse_length_squared(squared, 2);
|
||||
if squared == 0.0 { 0.0 } else { result }
|
||||
}
|
||||
|
||||
pub(super) fn normalize(value: Vector) -> Vector {
|
||||
let inverse = inverse_length_squared(dot3(value, value), 2);
|
||||
value.map(|v| v * inverse)
|
||||
}
|
||||
|
||||
pub(super) fn cross(left: Vector, right: Vector) -> Vector {
|
||||
[
|
||||
(-left[2]).mul_add(right[1], left[1] * right[2]),
|
||||
(-left[0]).mul_add(right[2], left[2] * right[0]),
|
||||
(-left[1]).mul_add(right[0], left[0] * right[1]),
|
||||
(-left[3]).mul_add(right[3], left[3] * right[3]),
|
||||
]
|
||||
}
|
||||
|
||||
///82BD3D90. The very short-vector branch preserves the original fourth lane.
|
||||
pub(super) fn limit_length(value: Vector, limit: f32) -> Vector {
|
||||
let magnitude = length(value);
|
||||
if !(magnitude >= f32::from_bits(0x3780_0000)) {
|
||||
return value;
|
||||
}
|
||||
let capped = if limit - magnitude >= 0.0 {
|
||||
magnitude
|
||||
} else {
|
||||
limit
|
||||
};
|
||||
let inverse = reciprocal(magnitude, 2);
|
||||
value.map(|v| (v * capped) * inverse)
|
||||
}
|
||||
|
||||
///82BD2E60's axis/angle outputs. The unused screw-pivot output is omitted.
|
||||
pub(super) fn rotation_axis_angle(rotation: &Transform) -> (Vector, f32) {
|
||||
let skew = [
|
||||
rotation[1][2] - rotation[2][1],
|
||||
rotation[2][0] - rotation[0][2],
|
||||
rotation[0][1] - rotation[1][0],
|
||||
0.0,
|
||||
];
|
||||
let sine_twice = length(skew);
|
||||
let cosine_twice = ((rotation[0][0] + rotation[1][1]) + rotation[2][2]) - 1.0;
|
||||
let mut axis = if sine_twice > 0.0 {
|
||||
let inverse = reciprocal(sine_twice, 2) * 1.0;
|
||||
skew.map(|v| v * inverse)
|
||||
} else {
|
||||
[0.0; 4]
|
||||
};
|
||||
let ratio = sine_twice.mul_add(reciprocal(cosine_twice, 1), 0.0);
|
||||
let mut angle = atan(ratio);
|
||||
if cosine_twice < 0.0 {
|
||||
angle += f32::from_bits(0x4049_0FDB).copysign(sine_twice);
|
||||
}
|
||||
if cosine_twice == 0.0 {
|
||||
angle = f32::from_bits(0x3FC9_0FDB).copysign(sine_twice);
|
||||
}
|
||||
// Literal word 820CFC28, loaded with lvlx. Do not replace this with a
|
||||
// conventional numerical tolerance.
|
||||
if sine_twice <= f32::from_bits(0x0020_0000) && cosine_twice <= 0.0 {
|
||||
axis = half_turn_axis(rotation);
|
||||
}
|
||||
(axis, angle)
|
||||
}
|
||||
|
||||
///827B7400 selects the greatest diagonal. Its selected component is squared
|
||||
/// before normalization (e.g. vmulfp at827B7488), unlike a common textbook
|
||||
/// matrix-to-axis conversion. Preserve the native formula and tie ordering.
|
||||
fn half_turn_axis(rotation: &Transform) -> Vector {
|
||||
let selected = if rotation[0][0] > rotation[1][1] {
|
||||
if rotation[0][0] > rotation[2][2] {
|
||||
0
|
||||
} else {
|
||||
2
|
||||
}
|
||||
} else if rotation[1][1] > rotation[2][2] {
|
||||
1
|
||||
} else {
|
||||
2
|
||||
};
|
||||
let mut axis = [0.0; 4];
|
||||
for lane in 0..3 {
|
||||
axis[lane] = if lane == selected {
|
||||
let diagonal = 1.0 + rotation[selected][selected];
|
||||
diagonal * diagonal
|
||||
} else {
|
||||
rotation[selected][lane] + rotation[lane][selected]
|
||||
};
|
||||
}
|
||||
axis[3] = axis[0];
|
||||
normalize(axis)
|
||||
}
|
||||
|
||||
///Rodrigues construction in82BD34E0..3548. The native permutation duplicates
|
||||
/// each column's X component into W; W is not a fabricated affine zero.
|
||||
pub(super) fn axis_rotation(axis: Vector, angle: f32) -> Transform {
|
||||
let (sine, cosine) = sin_cos(angle);
|
||||
let complement = 1.0 - cosine;
|
||||
let [x, y, z, _] = axis;
|
||||
let sx = sine * x;
|
||||
let sy = sine * y;
|
||||
let sz = sine * z;
|
||||
let tx = complement * x;
|
||||
let ty = complement * y;
|
||||
let tz = complement * z;
|
||||
let xx = tx.mul_add(x, cosine);
|
||||
let yy = ty.mul_add(y, cosine);
|
||||
let zz = tz.mul_add(z, cosine);
|
||||
let xy_positive = tx.mul_add(y, sz);
|
||||
let xy_negative = ty * x - sz;
|
||||
let xz_positive = tz.mul_add(x, sy);
|
||||
let xz_negative = tx * z - sy;
|
||||
let yz_positive = ty.mul_add(z, sx);
|
||||
let yz_negative = tz * y - sx;
|
||||
[
|
||||
[xx, xy_positive, xz_negative, xx],
|
||||
[xy_negative, yy, yz_positive, xy_negative],
|
||||
[xz_positive, yz_negative, zz, xz_positive],
|
||||
[0.0; 4],
|
||||
]
|
||||
}
|
||||
|
||||
///82BD3150 returns the remaining rotation angle as well as the blended frame.
|
||||
pub(super) fn interpolate(a: &Transform, b: &Transform, weight: f32) -> (Transform, f32) {
|
||||
if weight >= 1.0 {
|
||||
return (*b, 0.0);
|
||||
}
|
||||
let mut relative = IDENTITY;
|
||||
for column in 0..3 {
|
||||
relative[column] = core::array::from_fn(|lane| {
|
||||
let x = a[0][column] * b[0][lane];
|
||||
let y = a[1][column].mul_add(b[1][lane], x);
|
||||
a[2][column].mul_add(b[2][lane], y)
|
||||
});
|
||||
}
|
||||
let (axis, angle) = rotation_axis_angle(&relative);
|
||||
if weight <= 0.0 {
|
||||
return (*a, angle);
|
||||
}
|
||||
let mut output = IDENTITY;
|
||||
if angle < f32::from_bits(0x3D0E_FA35) {
|
||||
for column in 0..3 {
|
||||
output[column] = normalize(core::array::from_fn(|lane| {
|
||||
(b[column][lane] - a[column][lane]).mul_add(weight, a[column][lane])
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
let rotation = axis_rotation(axis, angle * weight);
|
||||
for column in 0..3 {
|
||||
output[column] = core::array::from_fn(|lane| {
|
||||
let x = a[column][0] * rotation[0][lane];
|
||||
let y = a[column][1].mul_add(rotation[1][lane], x);
|
||||
a[column][2].mul_add(rotation[2][lane], y)
|
||||
});
|
||||
}
|
||||
}
|
||||
output[3] = core::array::from_fn(|lane| (b[3][lane] - a[3][lane]).mul_add(weight, a[3][lane]));
|
||||
(output, angle - angle * weight)
|
||||
}
|
||||
|
||||
///82E0A570 overrides the interpolation helper's translation, including at
|
||||
/// endpoint weights. Preserve its weighted-sum rounding instead of delta lerp.
|
||||
pub(super) fn interpolate_affine(a: &Transform, b: &Transform, weight: f32) -> Transform {
|
||||
let translation =
|
||||
core::array::from_fn(|lane| a[3][lane].mul_add(1.0 - weight, b[3][lane] * weight));
|
||||
let (mut output, _) = interpolate(a, b, weight);
|
||||
output[3] = translation;
|
||||
output
|
||||
}
|
||||
/// General inverse expanded at8296FB50..FD14 and SkeletonData Init82BD6E9C.
|
||||
/// The transpose permutation retains the second cofactor in the fourth lane.
|
||||
pub fn inverse_affine(frame: &Transform) -> Transform {
|
||||
let cofactors = [
|
||||
cross(frame[1], frame[2]),
|
||||
cross(frame[2], frame[0]),
|
||||
cross(frame[0], frame[1]),
|
||||
];
|
||||
let inverse = reciprocal(dot3(frame[0], cofactors[0]), 2);
|
||||
let mut result: Transform = core::array::from_fn(|column| {
|
||||
if column == 3 {
|
||||
[0.0; 4]
|
||||
} else {
|
||||
[
|
||||
cofactors[0][column] * inverse,
|
||||
cofactors[1][column] * inverse,
|
||||
cofactors[2][column] * inverse,
|
||||
cofactors[1][column] * inverse,
|
||||
]
|
||||
}
|
||||
});
|
||||
let negative = frame[3].map(|v| f32::from_bits(v.to_bits() ^ 0x8000_0000));
|
||||
result[3] = core::array::from_fn(|lane| {
|
||||
let x = negative[0] * result[0][lane];
|
||||
let y = negative[1].mul_add(result[1][lane], x);
|
||||
negative[2].mul_add(result[2][lane], y)
|
||||
});
|
||||
result
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
//! Native skeleton IK, TU3 GeneralUpdate82BDCA38 and its six update stages.
|
||||
pub mod blend;
|
||||
pub mod contact;
|
||||
pub mod drive;
|
||||
pub mod external;
|
||||
mod physical_solve;
|
||||
pub mod post_contact;
|
||||
pub mod post_physics;
|
||||
mod math;
|
||||
pub use math::inverse_affine;
|
||||
pub mod settings;
|
||||
pub mod state;
|
||||
pub mod status;
|
||||
pub mod transforms;
|
||||
pub mod two_bone;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -0,0 +1,72 @@
|
||||
//! SkeletonIK::SolvePhysical82BF1480. Physical joint centers, not drive targets.
|
||||
use super::{
|
||||
drive::{Geometry, line_mapping, normalize_safe},
|
||||
math::cross,
|
||||
two_bone::{self, AngleLimits, SolveResult},
|
||||
};
|
||||
use crate::physics::{
|
||||
skeleton_animation_record::{compose_affine, transform_point},
|
||||
skeleton_body::SkeletonBody,
|
||||
};
|
||||
|
||||
pub(super) fn solve(
|
||||
body: &mut SkeletonBody,
|
||||
geometry: &Geometry,
|
||||
end_part: usize,
|
||||
offset: [f32; 4],
|
||||
) -> bool {
|
||||
let Some(middle_part) = geometry.parents[end_part] else {
|
||||
return false;
|
||||
};
|
||||
let Some(root_part) = geometry.parents[middle_part] else {
|
||||
return false;
|
||||
};
|
||||
let point = |part: usize| {
|
||||
transform_point(
|
||||
&body.record.pose[part],
|
||||
geometry.inverse_part_frames[part][3],
|
||||
)
|
||||
};
|
||||
let [root, middle, end] = [point(root_part), point(middle_part), point(end_part)];
|
||||
let mut target = core::array::from_fn(|i| end[i] + offset[i]);
|
||||
let mut solved_middle = [0.0; 4];
|
||||
//Literal0 and180, no original-angle override and recursion budget1.
|
||||
//The source requires return0; its extended/invalid paths BOTH reject.
|
||||
if two_bone::solve(
|
||||
root,
|
||||
middle,
|
||||
end,
|
||||
&mut solved_middle,
|
||||
&mut target,
|
||||
AngleLimits {
|
||||
minimum_degrees: 0.0,
|
||||
maximum_degrees: 180.0,
|
||||
},
|
||||
false,
|
||||
1,
|
||||
) != SolveResult::Solved
|
||||
{
|
||||
return false;
|
||||
}
|
||||
let original_normal = cross(sub(root, middle), sub(end, middle));
|
||||
let solved_normal = cross(sub(root, solved_middle), sub(target, solved_middle));
|
||||
let normal = normalize_safe(core::array::from_fn(|i| {
|
||||
original_normal[i] + solved_normal[i]
|
||||
}));
|
||||
let Some(mid_map) = line_mapping(middle, end, normal, solved_middle, target, normal) else {
|
||||
return false;
|
||||
};
|
||||
let Some(root_map) = line_mapping(root, middle, normal, root, solved_middle, normal) else {
|
||||
return false;
|
||||
};
|
||||
let middle_frame = compose_affine(&mid_map, &body.record.pose[middle_part]);
|
||||
let root_frame = compose_affine(&root_map, &body.record.pose[root_part]);
|
||||
//Unlike pre-physics drive solving, no extra orthonormalization precedes
|
||||
//the setter. The live body normalizes; the record keeps these frames.
|
||||
body.set_part_transform(middle_part, middle_frame);
|
||||
body.set_part_transform(root_part, root_frame);
|
||||
true
|
||||
}
|
||||
fn sub(a: [f32; 4], b: [f32; 4]) -> [f32; 4] {
|
||||
core::array::from_fn(|i| a[i] - b[i])
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
//! SkeletonIK::CheckFootAgainstBoard82BF0D70, including raised deck ends.
|
||||
use super::{math::reciprocal, settings::Settings, transforms::inverse_rigid};
|
||||
use crate::physics::{
|
||||
native_arithmetic::vector_min,
|
||||
skeleton_animation_record::{AnimationPartTransform as Transform, transform_point},
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct SettingsPost {
|
||||
///physics_animation/default layout780.
|
||||
pub minimum_board_up: f32,
|
||||
///Global212's authored layout412/416, selected by the wipeout argument.
|
||||
pub wipeout_height: f32,
|
||||
pub riding_height: f32,
|
||||
}
|
||||
|
||||
pub(super) fn target(
|
||||
board: &Transform,
|
||||
foot: [f32; 4],
|
||||
adjacent: [f32; 4],
|
||||
animated_board_position: [f32; 4],
|
||||
wipeout: bool,
|
||||
settings: &Settings,
|
||||
post: &SettingsPost,
|
||||
) -> Option<[f32; 4]> {
|
||||
let inverse = inverse_rigid(board);
|
||||
let local_foot = transform_point(&inverse, foot);
|
||||
let local_adjacent = transform_point(&inverse, adjacent);
|
||||
let mut midpoint: [f32; 4] =
|
||||
core::array::from_fn(|i| (local_foot[i] + local_adjacent[i]) * 0.5);
|
||||
//vrlimi mask4 preserves the foot's Y; X/Z test both foot and midpoint.
|
||||
midpoint[1] = local_foot[1];
|
||||
let x = vector_min(local_foot[0].abs(), midpoint[0].abs());
|
||||
let z = vector_min(local_foot[2].abs(), midpoint[2].abs());
|
||||
if x >= settings.deck_half_width + settings.post_ik_padding[0]
|
||||
|| z >= settings.deck_total_half_length + settings.post_ik_padding[2]
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let mut height = if wipeout {
|
||||
post.wipeout_height
|
||||
} else {
|
||||
post.riding_height
|
||||
};
|
||||
if z > settings.deck_half_length {
|
||||
let distance = z - settings.deck_half_length;
|
||||
let end = settings.deck_total_half_length - settings.deck_half_length;
|
||||
let distance = if end - distance >= 0.0 { distance } else { end };
|
||||
//Constructor82BED418 caches tan(clamp(authored degrees*.01745...)).
|
||||
let angle = settings.deck_front_angle_degrees * f32::from_bits(0x3C8E_FA35);
|
||||
let angle = if 0.01 - angle >= 0.0 { 0.01 } else { angle };
|
||||
let maximum = f32::from_bits(0x3FC7_C82D);
|
||||
let angle = if maximum - angle >= 0.0 {
|
||||
angle
|
||||
} else {
|
||||
maximum
|
||||
};
|
||||
height = distance.mul_add(tangent(angle), height);
|
||||
}
|
||||
if midpoint[1] >= height || midpoint[1] <= height - f32::from_bits(0x3E99_999A) {
|
||||
return None;
|
||||
}
|
||||
let mut result = animated_board_position;
|
||||
result[1] = height;
|
||||
Some(transform_point(board, result))
|
||||
}
|
||||
|
||||
///Scalar constructor path through original8252D980. Its argument is already
|
||||
///clamped to [.01,1.5607964]; coefficients come from822FE3C0..E0.
|
||||
fn tangent(angle: f32) -> f32 {
|
||||
let quadrant = (angle * f32::from_bits(0x3F22_F983)).round_ties_even();
|
||||
let reduced = (-quadrant).mul_add(f32::from_bits(0x3FC9_0FDB), angle);
|
||||
let reduced = (-quadrant).mul_add(f32::from_bits(0x2E85_A309), reduced);
|
||||
let squared = reduced * reduced;
|
||||
let denominator = squared.mul_add(f32::from_bits(0x3505_BBA8), f32::from_bits(0xB9A3_7B25));
|
||||
let denominator = squared.mul_add(denominator, f32::from_bits(0x3CD2_3CF5));
|
||||
let denominator = squared.mul_add(denominator, f32::from_bits(0xBEEE_F582));
|
||||
let denominator = squared.mul_add(denominator, 1.0);
|
||||
let numerator = squared.mul_add(f32::from_bits(0xB795_D5B9), f32::from_bits(0x3B60_7415));
|
||||
let numerator = squared.mul_add(numerator, f32::from_bits(0xBE08_95AF));
|
||||
let numerator = reduced.mul_add(squared * numerator, reduced);
|
||||
//vcmpbfp's two high result bits select corresponding sign/exponent bits.
|
||||
//The bounded constructor argument preserves these bits in both operands;
|
||||
//retain the actual bit-select rather than replacing it by a boolean branch.
|
||||
let bound = f32::from_bits(0x3980_0000);
|
||||
let mask = (if reduced > bound { 0x8000_0000 } else { 0 })
|
||||
| (if reduced < -bound { 0x4000_0000 } else { 0 });
|
||||
let denominator = f32::from_bits((denominator.to_bits() & !mask) | (1.0f32.to_bits() & mask));
|
||||
let numerator = f32::from_bits((numerator.to_bits() & !mask) | (reduced.to_bits() & mask));
|
||||
if quadrant.abs() as i32 & 1 == 0 {
|
||||
numerator * reciprocal(denominator, 2)
|
||||
} else {
|
||||
denominator * reciprocal(-numerator, 2)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn original_tangent_rational_spans_both_constructor_quadrants() {
|
||||
//Independent mathematical values test argument reduction and rational
|
||||
//operand order, without claiming original hardware estimate parity.
|
||||
for angle in [0.01f32, 0.25, 0.75, 1.0, 1.5607964] {
|
||||
let actual = tangent(angle);
|
||||
let reference = f64::from(angle).tan() as f32;
|
||||
assert!(
|
||||
(actual - reference).abs() < 3e-5 * reference.abs().max(1.0),
|
||||
"{angle}: {actual} vs {reference}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
//! PostWipeoutCheck's feet82BF0680, hands82BF0C28 and external82BEDC10.
|
||||
use super::{
|
||||
drive::Geometry,
|
||||
physical_solve,
|
||||
post_contact::{self, SettingsPost},
|
||||
settings::Settings,
|
||||
state::{FootIkState, LIMBS},
|
||||
};
|
||||
use crate::physics::{
|
||||
native_arithmetic::dot3,
|
||||
skeleton_animation_record::{AnimationPartTransform as Transform, compose_affine},
|
||||
skeleton_body::SkeletonBody,
|
||||
};
|
||||
|
||||
pub struct Input<'a> {
|
||||
pub state_id: u32,
|
||||
pub category_id: u32,
|
||||
///Actual BoardBody868, used by original state200/201 hand/foot gates.
|
||||
pub board_body_flag_868: bool,
|
||||
pub wipeout: bool,
|
||||
pub flags_2468: u32,
|
||||
pub flags_2484: u32,
|
||||
///Processed2664 and2520, read only when2468bit18 is set.
|
||||
pub value_2664: f32,
|
||||
pub state_2520: u32,
|
||||
pub world_to_animation: &'a Transform,
|
||||
///Actual board GetPartTransform, replaced by wobble-adjusted record when sampled.
|
||||
pub board: &'a Transform,
|
||||
}
|
||||
|
||||
pub fn update(
|
||||
state: &mut FootIkState,
|
||||
body: &mut SkeletonBody,
|
||||
geometry: &Geometry,
|
||||
settings: &Settings,
|
||||
post: &SettingsPost,
|
||||
input: Input<'_>,
|
||||
) -> [bool; 4] {
|
||||
let mut updated = [false; 4];
|
||||
if input.state_id != 702 {
|
||||
let airborne = matches!(input.state_id, 200 | 201);
|
||||
if (!airborne || input.board_body_flag_868) && input.category_id != 500 {
|
||||
feet(state, body, geometry, settings, post, &input, &mut updated);
|
||||
}
|
||||
if airborne && input.board_body_flag_868 {
|
||||
for limb in 2..4 {
|
||||
if state.frames[limb].within_contact_bounds {
|
||||
state.frames[limb].world =
|
||||
compose_affine(input.board, &state.frames[limb].board);
|
||||
let part = LIMBS[limb].part;
|
||||
let offset = sub(state.frames[limb].world[3], body.record.pose[part][3]);
|
||||
if physical_solve::solve(body, geometry, part, offset) {
|
||||
body.set_part_transform(part, state.frames[limb].world);
|
||||
updated[limb] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//Always run the external-target pass, including state702.
|
||||
for limb in 0..2 {
|
||||
if !(state.limbs[limb].external_blend <= 0.0) {
|
||||
let binding = LIMBS[limb];
|
||||
let adjacent = binding.parent_part.unwrap();
|
||||
let offset = sub(
|
||||
state.frames[limb].parent_world[3],
|
||||
body.record.pose[adjacent][3],
|
||||
);
|
||||
if physical_solve::solve(body, geometry, adjacent, offset) {
|
||||
body.set_part_transform(binding.part, state.frames[limb].world);
|
||||
body.set_part_transform(adjacent, state.frames[limb].parent_world);
|
||||
updated[limb] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
for limb in 0..4 {
|
||||
state.external_targets[limb].normal_set = false;
|
||||
state.limbs[limb].external_target_set = false;
|
||||
state.limbs[limb].local_target_set = false;
|
||||
}
|
||||
updated
|
||||
}
|
||||
|
||||
fn feet(
|
||||
state: &mut FootIkState,
|
||||
body: &mut SkeletonBody,
|
||||
geometry: &Geometry,
|
||||
settings: &Settings,
|
||||
post: &SettingsPost,
|
||||
input: &Input<'_>,
|
||||
updated: &mut [bool; 4],
|
||||
) {
|
||||
let mut effective_board = *input.board;
|
||||
if input.flags_2484 & (1 << 21) != 0 {
|
||||
effective_board[0] = effective_board[0].map(|v| v * -1.0);
|
||||
effective_board[1] = effective_board[1].map(|v| v * -1.0);
|
||||
}
|
||||
let up = effective_board[1];
|
||||
let up_y = up[0] * input.world_to_animation[0][1];
|
||||
let up_y = up[1].mul_add(input.world_to_animation[1][1], up_y);
|
||||
let up_y = up[2].mul_add(input.world_to_animation[2][1], up_y);
|
||||
let constrained = input.flags_2468 & (1 << 18) != 0;
|
||||
if input.flags_2484 & (1 << 20) != 0
|
||||
|| post.minimum_board_up > up_y
|
||||
|| constrained && input.value_2664 > f32::from_bits(0x3D23_D70A)
|
||||
|| constrained && input.state_2520 == 500
|
||||
{
|
||||
return;
|
||||
}
|
||||
for limb in 0..2 {
|
||||
let binding = LIMBS[limb];
|
||||
let adjacent = binding.parent_part.unwrap();
|
||||
if state.frames[limb].within_contact_bounds && !input.wipeout {
|
||||
state.frames[limb].world = compose_affine(input.board, &state.frames[limb].board);
|
||||
state.frames[limb].parent_world =
|
||||
compose_affine(input.board, &state.frames[limb].parent_board);
|
||||
let offset = sub(
|
||||
state.frames[limb].parent_world[3],
|
||||
body.record.pose[adjacent][3],
|
||||
);
|
||||
if physical_solve::solve(body, geometry, adjacent, offset) {
|
||||
body.set_part_transform(binding.part, state.frames[limb].world);
|
||||
body.set_part_transform(adjacent, state.frames[limb].parent_world);
|
||||
updated[limb] = true;
|
||||
}
|
||||
} else if !state.limbs[limb].external_target_set {
|
||||
let mut foot = body.record.pose[binding.part];
|
||||
if dot3(foot[1], up) <= 0.0 {
|
||||
continue;
|
||||
}
|
||||
let Some(target) = post_contact::target(
|
||||
&effective_board,
|
||||
foot[3],
|
||||
body.record.pose[adjacent][3],
|
||||
state.limbs[limb].part_position,
|
||||
input.wipeout,
|
||||
settings,
|
||||
post,
|
||||
) else {
|
||||
continue;
|
||||
};
|
||||
let distance = dot3(up, sub(target, foot[3]));
|
||||
let offset = up.map(|v| v * distance);
|
||||
if physical_solve::solve(body, geometry, adjacent, offset) {
|
||||
foot[3] = target;
|
||||
body.set_part_transform(binding.part, foot);
|
||||
let mut adjacent_frame = body.record.pose[adjacent];
|
||||
for lane in 0..4 {
|
||||
adjacent_frame[3][lane] += offset[lane];
|
||||
}
|
||||
body.set_part_transform(adjacent, adjacent_frame);
|
||||
updated[limb] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fn sub(a: [f32; 4], b: [f32; 4]) -> [f32; 4] {
|
||||
core::array::from_fn(|i| a[i] - b[i])
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
//! Authored IK parameters, resolved against the stock class layout.
|
||||
use super::status::BlendSettings;
|
||||
use super::two_bone::AngleLimits;
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Settings {
|
||||
/// physics_animation/default layout876/880, consumed by82BF0FA8.
|
||||
pub angle_limits: AngleLimits,
|
||||
pub blend: BlendSettings,
|
||||
/// physics_skeletonik layout0/48/64/80.
|
||||
pub post_ik_padding: [f32; 4],
|
||||
pub contact_bounds: [f32; 4],
|
||||
pub foot_on_deck_padding: [f32; 4],
|
||||
pub wipeout_feet_offset: f32,
|
||||
///82BED418 retrieves physicsdeck hashes EE1FD008/1DCB828B/932190E7.
|
||||
pub deck_half_width: f32,
|
||||
pub deck_half_length: f32,
|
||||
pub deck_total_half_length: f32,
|
||||
/// physicsdeck.DeckFrontEndAngle, hashF9B732CA; retained authored degrees.
|
||||
pub deck_front_angle_degrees: f32,
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
//! Persistent SkeletonIK state and the GeneralUpdate82BDCA38 stage order.
|
||||
use super::{
|
||||
blend,
|
||||
contact::{ContactInput, ContactState},
|
||||
drive::{self, Geometry},
|
||||
external::{self, ExternalTarget},
|
||||
settings::Settings,
|
||||
status::{self, LimbStatus, Mode},
|
||||
transforms::{self, LimbBinding, LimbFrames},
|
||||
};
|
||||
use crate::physics::skeleton_animation_record::AnimationPartTransform as Transform;
|
||||
|
||||
///82BED688 binds the toe targets to foot/toe volumes and the hand targets to
|
||||
///their hand volumes. The optional second volume is distinct from chain parents.
|
||||
pub const LIMBS: [LimbBinding; 4] = [
|
||||
LimbBinding {
|
||||
part: 15,
|
||||
parent_part: Some(16),
|
||||
},
|
||||
LimbBinding {
|
||||
part: 19,
|
||||
parent_part: Some(20),
|
||||
},
|
||||
LimbBinding {
|
||||
part: 3,
|
||||
parent_part: None,
|
||||
},
|
||||
LimbBinding {
|
||||
part: 7,
|
||||
parent_part: None,
|
||||
},
|
||||
];
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FootIkState {
|
||||
pub limbs: [LimbStatus; 4],
|
||||
pub frames: [LimbFrames; 4],
|
||||
pub external_targets: [ExternalTarget; 4],
|
||||
pub contacts: ContactState,
|
||||
feet_enabled: bool,
|
||||
}
|
||||
impl Default for FootIkState {
|
||||
fn default() -> Self {
|
||||
//82BED780 resets the retained histories and enables feet; it is not
|
||||
//the wipeout-entry solve82BF1B90, which disables feet after its update.
|
||||
Self {
|
||||
limbs: [LimbStatus::default(); 4],
|
||||
frames: [LimbFrames::default(); 4],
|
||||
external_targets: [ExternalTarget::default(); 4],
|
||||
contacts: ContactState::default(),
|
||||
feet_enabled: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct UpdateInput<'a> {
|
||||
pub flags_2468: u32,
|
||||
pub flags_2472: u32,
|
||||
pub flags_2480: u32,
|
||||
/// DataIn32/96; both are actual Skeleton root transforms.
|
||||
pub animation_to_world: &'a Transform,
|
||||
pub world_to_animation: &'a Transform,
|
||||
/// DataIn160, from Skeleton12496. This is independently produced from
|
||||
///DataIn352 (Skeleton12304) used by foot contact correction.
|
||||
pub physical_board: &'a Transform,
|
||||
pub contact_board: &'a Transform,
|
||||
pub inverse_contact_board: &'a Transform,
|
||||
/// SkeletonData animation record: physical volume frames after board offset.
|
||||
pub animation: &'a [Transform; 24],
|
||||
/// ProcessData14160: original selected globals, before volume composition.
|
||||
pub original_animation: &'a [Transform; 24],
|
||||
/// Actual board-parented targets after their type5 volume composition.
|
||||
pub targets: &'a [Transform; 4],
|
||||
pub hips_world_position: [f32; 4],
|
||||
pub contact_bone: usize,
|
||||
pub foot_bones: [usize; 2],
|
||||
pub current_contacts: [Option<[f32; 4]>; 2],
|
||||
}
|
||||
|
||||
impl FootIkState {
|
||||
pub fn reset(&mut self) {
|
||||
*self = Self::default();
|
||||
}
|
||||
|
||||
/// Ground::Enter writes (*(Skeleton+16432))+464 directly.
|
||||
pub fn enable_feet(&mut self, enabled: bool) {
|
||||
self.feet_enabled = enabled;
|
||||
}
|
||||
pub fn feet_enabled(&self) -> bool {
|
||||
self.feet_enabled
|
||||
}
|
||||
|
||||
/// Ground collision handling can also set the per-update3185 flag.
|
||||
pub fn mark_support_failed_this_update(&mut self) {
|
||||
self.contacts.support_failed_this_update = true;
|
||||
}
|
||||
|
||||
/// The mutable output is Skeleton12624 consumed by SkeletonDrives. The
|
||||
///original animation and collision-volume record must remain separate.
|
||||
pub fn update(
|
||||
&mut self,
|
||||
input: UpdateInput<'_>,
|
||||
geometry: &Geometry,
|
||||
settings: &Settings,
|
||||
drives: &mut [Transform; 24],
|
||||
) -> [bool; 4] {
|
||||
for limb in 0..4 {
|
||||
self.frames[limb].target = input.targets[limb];
|
||||
}
|
||||
status::update_modes(&mut self.limbs, self.feet_enabled, input.flags_2472);
|
||||
status::update_blends(
|
||||
&mut self.limbs,
|
||||
settings.deck_half_width,
|
||||
settings.deck_total_half_length,
|
||||
input.flags_2480,
|
||||
&settings.blend,
|
||||
);
|
||||
let inverse_animation_board = transforms::inverse_rigid(&input.animation[0]);
|
||||
for limb in 0..4 {
|
||||
self.frames[limb].within_contact_bounds = false;
|
||||
if matches!(self.limbs[limb].mode, Mode::External | Mode::Local) {
|
||||
external::update(
|
||||
&mut self.external_targets[limb],
|
||||
&mut self.limbs[limb],
|
||||
&mut self.frames[limb],
|
||||
LIMBS[limb],
|
||||
input.animation,
|
||||
input.animation_to_world,
|
||||
input.world_to_animation,
|
||||
);
|
||||
}
|
||||
transforms::prepare_animation_target(
|
||||
&mut self.limbs[limb],
|
||||
&mut self.frames[limb],
|
||||
LIMBS[limb],
|
||||
input.animation,
|
||||
input.animation_to_world,
|
||||
&inverse_animation_board,
|
||||
settings.contact_bounds,
|
||||
);
|
||||
}
|
||||
blend::update(
|
||||
&self.limbs,
|
||||
&mut self.frames,
|
||||
&LIMBS,
|
||||
input.physical_board,
|
||||
input.animation_to_world,
|
||||
&input.animation[0],
|
||||
);
|
||||
self.contacts.update(
|
||||
&self.limbs,
|
||||
&mut self.frames,
|
||||
ContactInput {
|
||||
flags_2468: input.flags_2468,
|
||||
contact_bone: input.contact_bone,
|
||||
foot_bones: input.foot_bones,
|
||||
hips_world_position: input.hips_world_position,
|
||||
inverse_board: input.inverse_contact_board,
|
||||
board: input.contact_board,
|
||||
current_contacts: input.current_contacts,
|
||||
},
|
||||
);
|
||||
drive::update(
|
||||
&self.limbs,
|
||||
&mut self.frames,
|
||||
&LIMBS,
|
||||
geometry,
|
||||
input.original_animation,
|
||||
drives,
|
||||
input.world_to_animation,
|
||||
settings.angle_limits,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
//! Limb mode transitions82BEDCF8 and blend weights82BEEC00.
|
||||
//! Named S2 UpdateStatus/UpdateBlendValues corroborate field identities;
|
||||
//! TU3 flags and cached settings determine the implementation.
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Mode {
|
||||
Disabled,
|
||||
OnDeck,
|
||||
External,
|
||||
Local,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct LimbStatus {
|
||||
pub mode: Mode,
|
||||
/// SkeletonIK+468+4*limb.
|
||||
pub board_blend: f32,
|
||||
/// SkeletonIK+484+4*limb.
|
||||
pub external_blend: f32,
|
||||
pub target_blend: f32,
|
||||
pub external_target_set: bool,
|
||||
pub local_target_set: bool,
|
||||
pub external_target_local_delta: [f32; 4],
|
||||
/// CalculateInitialPartTransforms retains the target in animation-deck space.
|
||||
pub part_position: [f32; 4],
|
||||
}
|
||||
impl Default for LimbStatus {
|
||||
fn default() -> Self {
|
||||
//82BED780 resets mode, both weights, target blend, target flags and
|
||||
//the complete local delta/part-position vectors to zero.
|
||||
Self {
|
||||
mode: Mode::Disabled,
|
||||
board_blend: 0.0,
|
||||
external_blend: 0.0,
|
||||
target_blend: 0.0,
|
||||
external_target_set: false,
|
||||
local_target_set: false,
|
||||
external_target_local_delta: [0.0; 4],
|
||||
part_position: [0.0; 4],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// All four limbs in native order: left foot, right foot, left hand, right hand.
|
||||
/// Foot enable is SkeletonIK464; reset82BED780 initializes it to true.
|
||||
pub fn update_modes(limbs: &mut [LimbStatus; 4], feet_enabled: bool, flags_2472: u32) {
|
||||
let enabled = [
|
||||
feet_enabled,
|
||||
feet_enabled,
|
||||
flags_2472 & 0x80 != 0,
|
||||
flags_2472 & 0x100 != 0,
|
||||
];
|
||||
for (limb, enabled) in limbs.iter_mut().zip(enabled) {
|
||||
let fallback = if enabled {
|
||||
Mode::OnDeck
|
||||
} else {
|
||||
Mode::Disabled
|
||||
};
|
||||
match limb.mode {
|
||||
Mode::Disabled => {
|
||||
limb.board_blend = 0.0;
|
||||
if limb.external_target_set {
|
||||
limb.mode = Mode::External;
|
||||
limb.external_blend = limb.target_blend;
|
||||
} else if limb.local_target_set {
|
||||
limb.mode = Mode::Local;
|
||||
limb.external_blend = limb.target_blend;
|
||||
limb.external_target_local_delta = [0.0; 4];
|
||||
} else {
|
||||
limb.mode = fallback;
|
||||
limb.external_blend = 0.0;
|
||||
}
|
||||
}
|
||||
Mode::OnDeck => {
|
||||
if limb.external_target_set {
|
||||
limb.mode = Mode::External;
|
||||
limb.external_blend = limb.target_blend;
|
||||
} else if limb.local_target_set {
|
||||
limb.mode = Mode::Local;
|
||||
limb.external_blend = limb.target_blend;
|
||||
limb.external_target_local_delta = [0.0; 4];
|
||||
} else if limb.board_blend == 0.0 {
|
||||
limb.mode = fallback;
|
||||
limb.external_blend = 0.0;
|
||||
}
|
||||
}
|
||||
Mode::External => {
|
||||
if !limb.external_target_set {
|
||||
limb.mode = Mode::Local;
|
||||
}
|
||||
}
|
||||
Mode::Local => {
|
||||
if limb.external_target_set {
|
||||
limb.mode = Mode::External;
|
||||
limb.external_blend = limb.target_blend;
|
||||
}
|
||||
// This is a second independent test, even after switching to
|
||||
//External above. Preserve the native zero-target-blend case.
|
||||
if !limb.local_target_set && limb.external_blend == 0.0 {
|
||||
limb.mode = fallback;
|
||||
limb.external_target_local_delta = [0.0; 4];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct BlendSettings {
|
||||
/// physics_skeletonik cached layout16/32.
|
||||
pub hand_inner_padding: [f32; 4],
|
||||
pub hand_outer_padding: [f32; 4],
|
||||
/// Cached layout84 and88; native increments per update, without dt.
|
||||
pub external_blend_step: f32,
|
||||
pub board_blend_step: f32,
|
||||
}
|
||||
|
||||
/// Board dimensions are the actual constructor-derived half width and total
|
||||
/// half length, not the board's visual bounds or cached generic dimensions.
|
||||
pub fn update_blends(
|
||||
limbs: &mut [LimbStatus; 4],
|
||||
deck_half_width: f32,
|
||||
deck_total_half_length: f32,
|
||||
flags_2480: u32,
|
||||
settings: &BlendSettings,
|
||||
) {
|
||||
let base = [deck_half_width, 0.0, deck_total_half_length, 0.0];
|
||||
let inner: [f32; 4] = core::array::from_fn(|i| base[i] + settings.hand_inner_padding[i]);
|
||||
let outer: [f32; 4] = core::array::from_fn(|i| inner[i] + settings.hand_outer_padding[i]);
|
||||
let step = settings.board_blend_step;
|
||||
for (index, limb) in limbs.iter_mut().enumerate() {
|
||||
match limb.mode {
|
||||
Mode::Disabled => {}
|
||||
Mode::External => {
|
||||
limb.external_blend = limb.target_blend;
|
||||
limb.board_blend = positive(limb.board_blend - step);
|
||||
}
|
||||
Mode::Local => {
|
||||
limb.external_blend = if limb.local_target_set {
|
||||
1.0
|
||||
} else {
|
||||
positive(limb.external_blend - settings.external_blend_step)
|
||||
};
|
||||
limb.board_blend = positive(limb.board_blend - step);
|
||||
}
|
||||
Mode::OnDeck if index < 2 => {
|
||||
let target = if flags_2480 & 0x0800_0000 != 0 {
|
||||
0.0
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
approach(&mut limb.board_blend, target, step);
|
||||
}
|
||||
Mode::OnDeck => {
|
||||
let p = limb.part_position.map(f32::abs);
|
||||
if limb.board_blend > 0.0 || p[0] < outer[0] && p[1] < outer[1] && p[2] < outer[2] {
|
||||
let mut target = 1.0;
|
||||
// Native order X, Z, Y; fsel preserves equality and NaNs.
|
||||
for axis in [0, 2, 1] {
|
||||
if p[axis] > inner[axis] {
|
||||
let candidate =
|
||||
1.0 - (p[axis] - inner[axis]) / settings.hand_outer_padding[axis];
|
||||
if axis == 0 {
|
||||
target = candidate;
|
||||
} else if axis == 2 {
|
||||
target = select(target - candidate, candidate, target);
|
||||
} else {
|
||||
target = select(candidate - target, target, candidate);
|
||||
}
|
||||
}
|
||||
}
|
||||
target = positive(target);
|
||||
target = select(1.0 - target, target, 1.0);
|
||||
approach(&mut limb.board_blend, target, step);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fn select(condition: f32, nonnegative: f32, negative: f32) -> f32 {
|
||||
if condition >= 0.0 {
|
||||
nonnegative
|
||||
} else {
|
||||
negative
|
||||
}
|
||||
}
|
||||
fn positive(value: f32) -> f32 {
|
||||
select(-value, 0.0, value)
|
||||
}
|
||||
fn approach(value: &mut f32, target: f32, step: f32) {
|
||||
let delta = target - *value;
|
||||
let delta = select(-step - delta, -step, delta);
|
||||
*value = select(step - delta, delta, step) + *value;
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
use super::{
|
||||
contact::{ContactInput, ContactState},
|
||||
math::{interpolate, inverse_affine},
|
||||
status::{LimbStatus, Mode},
|
||||
transforms::LimbFrames,
|
||||
two_bone::{self, AngleLimits, SolveResult},
|
||||
};
|
||||
use crate::physics::skeleton_animation_record::{IDENTITY, transform_point};
|
||||
|
||||
#[test]
|
||||
fn inverse_preserves_points_for_scaled_and_sheared_authored_frames() {
|
||||
let frame = [
|
||||
[2.0, 0.0, 0.0, 0.0],
|
||||
[0.5, 3.0, 0.0, 0.0],
|
||||
[0.0, 0.25, 4.0, 0.0],
|
||||
[7.0, -2.0, 5.0, 0.0],
|
||||
];
|
||||
let point = [0.3, -0.7, 1.2, 0.0];
|
||||
let recovered = transform_point(&inverse_affine(&frame), transform_point(&frame, point));
|
||||
for lane in 0..3 {
|
||||
assert!((recovered[lane] - point[lane]).abs() < 2e-6);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frame_interpolation_uses_rotation_and_translation_at_the_same_weight() {
|
||||
let mut target = IDENTITY;
|
||||
target[0] = [0.0, 1.0, 0.0, 0.0];
|
||||
target[1] = [-1.0, 0.0, 0.0, 0.0];
|
||||
target[3] = [8.0, 2.0, -4.0, 0.0];
|
||||
let (half, remaining) = interpolate(&IDENTITY, &target, 0.5);
|
||||
assert!((half[0][0] - core::f32::consts::FRAC_1_SQRT_2).abs() < 2e-6);
|
||||
assert!((half[0][1] - core::f32::consts::FRAC_1_SQRT_2).abs() < 2e-6);
|
||||
assert_eq!(&half[3][..3], &[4.0, 1.0, -2.0]);
|
||||
assert!((remaining - core::f32::consts::FRAC_PI_4).abs() < 2e-6);
|
||||
assert_eq!(interpolate(&IDENTITY, &target, 1.0), (target, 0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reachable_two_bone_target_keeps_both_segment_lengths() {
|
||||
let root = [0.0; 4];
|
||||
let middle = [0.3, 0.4, 0.0, 0.0];
|
||||
let end = [0.6, 0.0, 0.0, 0.0];
|
||||
let mut target = [0.7, 0.0, 0.0, 0.0];
|
||||
let mut solved = [0.0; 4];
|
||||
let result = two_bone::solve(
|
||||
root,
|
||||
middle,
|
||||
end,
|
||||
&mut solved,
|
||||
&mut target,
|
||||
AngleLimits {
|
||||
minimum_degrees: 5.0,
|
||||
maximum_degrees: 175.0,
|
||||
},
|
||||
true,
|
||||
2,
|
||||
);
|
||||
assert_eq!(result, SolveResult::Solved);
|
||||
assert_eq!(&target[..3], &[0.7, 0.0, 0.0]);
|
||||
let distance = |a: [f32; 4], b: [f32; 4]| {
|
||||
((a[0] - b[0]).powi(2) + (a[1] - b[1]).powi(2) + (a[2] - b[2]).powi(2)).sqrt()
|
||||
};
|
||||
assert!((distance(root, solved) - 0.5).abs() < 2e-5, "{solved:?}");
|
||||
assert!((distance(solved, target) - 0.5).abs() < 2e-5, "{solved:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn foot_queries_affect_the_following_update_and_loss_sets_support_history() {
|
||||
let mut contacts = ContactState::default();
|
||||
let mut statuses = [LimbStatus::default(); 4];
|
||||
statuses[0].mode = Mode::OnDeck;
|
||||
let mut frames = [LimbFrames::default(); 4];
|
||||
let input = |hit| ContactInput {
|
||||
flags_2468: 0x0200_0000,
|
||||
contact_bone: 10,
|
||||
foot_bones: [10, 20],
|
||||
hips_world_position: [0.0; 4],
|
||||
inverse_board: &IDENTITY,
|
||||
board: &IDENTITY,
|
||||
current_contacts: [hit, None],
|
||||
};
|
||||
contacts.update(&statuses, &mut frames, input(None));
|
||||
assert!(!contacts.support_failed_this_update);
|
||||
assert_eq!(contacts.feet[0].query_state, 2);
|
||||
contacts.update(&statuses, &mut frames, input(Some([0.0; 4])));
|
||||
assert!(contacts.support_failed_this_update);
|
||||
assert!(contacts.support_failed);
|
||||
assert_eq!(contacts.feet[0].query_state, 1);
|
||||
statuses[0].mode = Mode::Disabled;
|
||||
contacts.update(&statuses, &mut frames, input(None));
|
||||
assert!(!contacts.support_failed_this_update);
|
||||
assert!(contacts.support_failed);
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
//! Animated portion of CalculateInitialPartTransforms82BEDF08.
|
||||
use super::status::{LimbStatus, Mode};
|
||||
use crate::physics::skeleton_animation_record::{
|
||||
AnimationPartTransform as Transform, IDENTITY, compose_affine,
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct LimbBinding {
|
||||
/// SkeletonIK1120: the physics volume driven by this target.
|
||||
pub part: usize,
|
||||
/// SkeletonIK1136: optional adjacent volume receiving the same adjustment.
|
||||
pub parent_part: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct LimbFrames {
|
||||
/// SkeletonIK864, from the actual reparented animation target.
|
||||
pub target: Transform,
|
||||
/// SkeletonIK1152; animated world frame, then blended/contact-adjusted.
|
||||
pub world: Transform,
|
||||
/// SkeletonIK1408: externally supplied target in world space.
|
||||
pub external_world: Transform,
|
||||
/// SkeletonIK1664: target relative to the animated board.
|
||||
pub board: Transform,
|
||||
/// SkeletonIK2240, optional adjacent volume in world space.
|
||||
pub parent_world: Transform,
|
||||
/// SkeletonIK2496: adjacent volume's external target.
|
||||
pub external_parent_world: Transform,
|
||||
/// SkeletonIK2752: adjacent volume relative to the animated board.
|
||||
pub parent_board: Transform,
|
||||
/// SkeletonIK3120: animated target lies within the cached contact box.
|
||||
pub within_contact_bounds: bool,
|
||||
}
|
||||
impl Default for LimbFrames {
|
||||
fn default() -> Self {
|
||||
//82BED780 initializes every retained affine frame to identity.
|
||||
Self {
|
||||
target: IDENTITY,
|
||||
world: IDENTITY,
|
||||
external_world: IDENTITY,
|
||||
board: IDENTITY,
|
||||
parent_world: IDENTITY,
|
||||
external_parent_world: IDENTITY,
|
||||
parent_board: IDENTITY,
|
||||
within_contact_bounds: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Prepare the animated frames after this limb's CalculateExternalIKOffset
|
||||
/// stage. External/local modes retain their separately calculated frames.
|
||||
/// The full controller calls both stages in native limb order.
|
||||
pub fn prepare_animation_target(
|
||||
status: &mut LimbStatus,
|
||||
frames: &mut LimbFrames,
|
||||
binding: LimbBinding,
|
||||
animation: &[Transform; 24],
|
||||
animation_to_world: &Transform,
|
||||
inverse_animation_board: &Transform,
|
||||
contact_bounds: [f32; 4],
|
||||
) {
|
||||
if status.mode != Mode::OnDeck && !(status.board_blend > 0.0) && status.external_blend >= 1.0 {
|
||||
return;
|
||||
}
|
||||
let animated = &animation[binding.part];
|
||||
frames.board = compose_affine(inverse_animation_board, &frames.target);
|
||||
status.part_position = frames.board[3];
|
||||
frames.world = compose_affine(animation_to_world, animated);
|
||||
// Native comparisons reject only a strict bound violation; NaNs do not
|
||||
// become a host-side exclusion. The fourth stored lane is excluded.
|
||||
frames.within_contact_bounds = (0..3).all(|i| {
|
||||
!(status.part_position[i] > contact_bounds[i]
|
||||
|| -contact_bounds[i] > status.part_position[i])
|
||||
});
|
||||
if let Some(parent) = binding.parent_part {
|
||||
let relative = compose_affine(&inverse_rigid(animated), &animation[parent]);
|
||||
frames.parent_board = compose_affine(&frames.board, &relative);
|
||||
frames.parent_world = compose_affine(&frames.world, &relative);
|
||||
}
|
||||
}
|
||||
|
||||
///82BEDF5C..DFEC and E1B4..E260: rigid inverse, transpose XYZ and zero W;
|
||||
/// translation uses negative position with Z multiply, Y FMA, then X FMA.
|
||||
/// This deliberately does not introduce a general scale/shear inverse.
|
||||
pub fn inverse_rigid(source: &Transform) -> Transform {
|
||||
let mut result = IDENTITY;
|
||||
for axis in 0..3 {
|
||||
result[axis] = [source[0][axis], source[1][axis], source[2][axis], 0.0];
|
||||
}
|
||||
let translation = source[3].map(|v| 0.0 - v);
|
||||
result[3] = core::array::from_fn(|i| {
|
||||
let z = translation[2] * result[2][i];
|
||||
let y = translation[1].mul_add(result[1][i], z);
|
||||
translation[0].mul_add(result[0][i], y)
|
||||
});
|
||||
result
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
//! Math::_TwoBoneIKSolve8296F8C0, TU3 complete reachable/recursive/invalid path.
|
||||
//! Plane construction uses the native general inverse, not a rigid transpose.
|
||||
use super::math::{Vector, cross, inverse_affine, length, normalize, reciprocal};
|
||||
use crate::{
|
||||
physics::{
|
||||
native_arithmetic::{dot3, vector_max, vector_min},
|
||||
skeleton_animation_record::{AnimationPartTransform as Transform, transform_point},
|
||||
},
|
||||
trigonometry::{acos, cos, sin_cos},
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum SolveResult {
|
||||
Solved,
|
||||
Extended,
|
||||
Invalid,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct AngleLimits {
|
||||
pub minimum_degrees: f32,
|
||||
pub maximum_degrees: f32,
|
||||
}
|
||||
|
||||
const EPSILON: f32 = f32::from_bits(0x3A03_126F); //830BD4B0 <-821A0318
|
||||
const TO_DEGREES: f32 = f32::from_bits(0x4265_2EE1); //830BD390
|
||||
const TO_RADIANS: f32 = f32::from_bits(0x3C8E_FA35); //830BD360
|
||||
|
||||
pub fn solve(
|
||||
root: Vector,
|
||||
middle: Vector,
|
||||
end: Vector,
|
||||
solved_middle: &mut Vector,
|
||||
target: &mut Vector,
|
||||
limits: AngleLimits,
|
||||
override_maximum_with_original: bool,
|
||||
recursion_budget: u32,
|
||||
) -> SolveResult {
|
||||
if recursion_budget == 0 {
|
||||
*target = end;
|
||||
*solved_middle = middle;
|
||||
return SolveResult::Invalid;
|
||||
}
|
||||
let upper = subtract(middle, root);
|
||||
let lower = subtract(end, middle);
|
||||
let upper_length = length(upper);
|
||||
let lower_length = length(lower);
|
||||
if EPSILON > upper_length || EPSILON > lower_length {
|
||||
*solved_middle = middle;
|
||||
return SolveResult::Invalid;
|
||||
}
|
||||
let requested_delta = subtract(*target, root);
|
||||
let forward = normalize(subtract(end, root));
|
||||
let normal = normalize(cross(upper, lower));
|
||||
let original_basis = [forward, cross(forward, normal), normal, root];
|
||||
let target_forward = normalize(requested_delta);
|
||||
let target_lateral = cross(target_forward, normal);
|
||||
let target_normal = cross(target_lateral, target_forward);
|
||||
let target_basis = [target_forward, target_lateral, target_normal, root];
|
||||
let original_inverse = inverse_affine(&original_basis);
|
||||
let target_inverse = inverse_affine(&target_basis);
|
||||
// Each point is transformed separately, retaining the original origin's
|
||||
// rounding; do not replace these with pre-subtracted vectors.
|
||||
let local_root = transform_point(&original_inverse, root);
|
||||
let local_middle = transform_point(&original_inverse, middle);
|
||||
let local_end = transform_point(&original_inverse, end);
|
||||
let local_target = transform_point(&target_inverse, *target);
|
||||
let target_delta = subtract(local_target, local_root);
|
||||
let planar_squared =
|
||||
target_delta[0].mul_add(target_delta[0], target_delta[1] * target_delta[1]);
|
||||
let distance = square_root(planar_squared);
|
||||
let upper_squared = upper_length * upper_length;
|
||||
let lower_squared = lower_length * lower_length;
|
||||
let twice_upper = 2.0 * upper_length;
|
||||
let twice_product = twice_upper * lower_length;
|
||||
let sum_squared = upper_squared + lower_squared;
|
||||
let negative_lower = lower.map(|v| f32::from_bits(v.to_bits() ^ 0x8000_0000));
|
||||
let negative_length = f32::from_bits(lower_length.to_bits() ^ 0x8000_0000);
|
||||
let original_angle =
|
||||
acos(reciprocal(upper_length * negative_length, 2) * dot3(upper, negative_lower))
|
||||
* TO_DEGREES;
|
||||
let mut maximum = limits.maximum_degrees;
|
||||
if original_angle > maximum && override_maximum_with_original {
|
||||
maximum = original_angle;
|
||||
}
|
||||
let minimum_distance =
|
||||
square_root(sum_squared - twice_product * cos(limits.minimum_degrees * TO_RADIANS));
|
||||
let maximum_distance = square_root(sum_squared - twice_product * cos(maximum * TO_RADIANS));
|
||||
if minimum_distance > distance || distance > maximum_distance {
|
||||
if distance > upper_length + lower_length && maximum == 180.0 {
|
||||
let direction = normalize(target_delta);
|
||||
let middle_local = scaled_add(direction, upper_length, local_root);
|
||||
let end_local = scaled_add(direction, lower_length, middle_local);
|
||||
*solved_middle = transform_point(&target_basis, middle_local);
|
||||
*target = transform_point(&target_basis, end_local);
|
||||
return SolveResult::Extended;
|
||||
}
|
||||
let distance = if distance > maximum_distance {
|
||||
maximum_distance - EPSILON
|
||||
} else {
|
||||
minimum_distance + EPSILON
|
||||
};
|
||||
*target = scaled_add(normalize(requested_delta), distance, root);
|
||||
return solve(
|
||||
root,
|
||||
middle,
|
||||
end,
|
||||
solved_middle,
|
||||
target,
|
||||
limits,
|
||||
override_maximum_with_original,
|
||||
recursion_budget - 1,
|
||||
);
|
||||
}
|
||||
let cosine = clamp_native_cosine(reciprocal(twice_product, 2) * (sum_squared - planar_squared));
|
||||
let interior_angle = acos(cosine) * TO_DEGREES;
|
||||
if limits.minimum_degrees >= interior_angle {
|
||||
*target = scaled_add(normalize(requested_delta), minimum_distance + EPSILON, root);
|
||||
return solve(
|
||||
root,
|
||||
middle,
|
||||
end,
|
||||
solved_middle,
|
||||
target,
|
||||
limits,
|
||||
override_maximum_with_original,
|
||||
recursion_budget - 1,
|
||||
);
|
||||
}
|
||||
let mut target_angle = acos(clamp_native_cosine(
|
||||
reciprocal(distance, 2) * target_delta[0],
|
||||
));
|
||||
if local_middle[1] < 0.0 && local_target[1] > 0.0
|
||||
|| local_middle[1] > 0.0 && local_target[1] < 0.0
|
||||
{
|
||||
target_angle *= -1.0;
|
||||
}
|
||||
// The target heading is an addend *inside* this acos in the TU3 body
|
||||
// (vmaddfp82970088). Preserve it rather than fitting a textbook IK formula.
|
||||
let ratio = reciprocal(twice_upper * distance, 2).mul_add(
|
||||
(upper_squared + planar_squared) - lower_squared,
|
||||
target_angle,
|
||||
);
|
||||
let angle = acos(clamp_native_cosine(ratio));
|
||||
let original_delta = subtract(local_end, local_root);
|
||||
let positive = candidate(
|
||||
original_delta,
|
||||
angle,
|
||||
upper_length,
|
||||
local_root,
|
||||
&target_basis,
|
||||
);
|
||||
let negative = candidate(
|
||||
original_delta,
|
||||
f32::from_bits(0x40C9_0FDB) - angle,
|
||||
upper_length,
|
||||
local_root,
|
||||
&target_basis,
|
||||
);
|
||||
*solved_middle = if local_end[1] > local_middle[1] {
|
||||
negative
|
||||
} else {
|
||||
positive
|
||||
};
|
||||
SolveResult::Solved
|
||||
}
|
||||
|
||||
///The source uses830BD310=0 and830BD430=1 for these clamps. The lower
|
||||
///constant is not minus one; its initializer82F82718 loads82165A10.
|
||||
fn clamp_native_cosine(value: f32) -> f32 {
|
||||
vector_min(1.0, vector_max(0.0, value))
|
||||
}
|
||||
|
||||
fn square_root(squared: f32) -> f32 {
|
||||
let inverse = crate::physics::board_motion_output::inverse_length_squared(squared, 2);
|
||||
let value = squared * inverse;
|
||||
if squared == 0.0 { 0.0 } else { value }
|
||||
}
|
||||
fn subtract(a: Vector, b: Vector) -> Vector {
|
||||
core::array::from_fn(|i| a[i] - b[i])
|
||||
}
|
||||
fn scaled_add(a: Vector, scale: f32, b: Vector) -> Vector {
|
||||
core::array::from_fn(|i| a[i].mul_add(scale, b[i]))
|
||||
}
|
||||
|
||||
fn candidate(
|
||||
delta: Vector,
|
||||
angle: f32,
|
||||
upper_length: f32,
|
||||
root: Vector,
|
||||
basis: &Transform,
|
||||
) -> Vector {
|
||||
let (sine, cosine) = sin_cos(angle);
|
||||
let rotation = [
|
||||
[cosine, sine, 0.0, cosine],
|
||||
[-sine, cosine, 0.0, -sine],
|
||||
[0.0, 0.0, 1.0, 0.0],
|
||||
[0.0; 4],
|
||||
];
|
||||
let rotated = normalize(transform_point(&rotation, delta));
|
||||
transform_point(basis, scaled_add(rotated, upper_length, root))
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
//! Deck acceleration conditioning82DF0508 and IsBumped82BA7310, original TU3.
|
||||
//! Matrices are the raw physical deck and Reckoning ground frames; the effective
|
||||
//! animation deck is a different source. No render transform participates here.
|
||||
use crate::physics::{native_arithmetic, skeleton_animation_record::AnimationPartTransform};
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Input {
|
||||
/// PhysOutDeck0..48, actual Part6 transform (82C02AD8..2B20).
|
||||
pub deck: AnimationPartTransform,
|
||||
/// PhysOutGround0..48, Reckoning752..800 (82DB70E0..7100).
|
||||
pub ground: AnimationPartTransform,
|
||||
/// PhysOutMotion112, BoardBody528 from observed deck velocity differences.
|
||||
pub world_acceleration: [f32; 4],
|
||||
}
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Settings {
|
||||
/// Globals344 anim_motion/bumps layout1700, scale_x_acc.
|
||||
pub scale_x_acc: f32,
|
||||
/// Same collection layout1768, min_bump_mag.
|
||||
pub min_bump_mag: f32,
|
||||
}
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Output {
|
||||
pub acceleration: [f32; 4],
|
||||
pub bumped: bool,
|
||||
}
|
||||
|
||||
pub fn publish(input: Input, settings: &Settings) -> Output {
|
||||
let acceleration = condition(input);
|
||||
Output {
|
||||
acceleration,
|
||||
bumped: is_bumped(acceleration, settings),
|
||||
}
|
||||
}
|
||||
|
||||
///82DF0508: remove deck-local Y, rotate back to world, express in the
|
||||
///ground frame, then remove ground-local Y. Both original stores are required.
|
||||
pub fn condition(input: Input) -> [f32; 4] {
|
||||
let mut deck_local = inverse_rotate(input.deck, input.world_acceleration);
|
||||
deck_local[1] = 0.0; //82DF05C4
|
||||
let world = rotate(input.deck, deck_local);
|
||||
let mut ground_local = inverse_rotate(input.ground, world);
|
||||
ground_local[1] = 0.0; //82DF0600
|
||||
ground_local
|
||||
}
|
||||
|
||||
///82BA7310 scales X only, then compares the refined length strictly > threshold.
|
||||
///Independent Rust estimates are used; this is not a bit-exact Xenon claim.
|
||||
pub fn is_bumped(mut acceleration: [f32; 4], settings: &Settings) -> bool {
|
||||
acceleration[0] *= settings.scale_x_acc;
|
||||
let squared = native_arithmetic::dot3(acceleration, acceleration);
|
||||
let mut reciprocal = native_arithmetic::reciprocal_square_root_estimate(squared);
|
||||
for _ in 0..2 {
|
||||
let square = reciprocal * reciprocal;
|
||||
let half = reciprocal * 0.5;
|
||||
let error = (-squared).mul_add(square, 1.0);
|
||||
reciprocal = half.mul_add(error, reciprocal);
|
||||
}
|
||||
let magnitude = if squared == 0.0 {
|
||||
0.0
|
||||
} else {
|
||||
squared * reciprocal
|
||||
};
|
||||
magnitude > settings.min_bump_mag
|
||||
}
|
||||
|
||||
fn inverse_rotate(frame: AnimationPartTransform, vector: [f32; 4]) -> [f32; 4] {
|
||||
core::array::from_fn(|lane| {
|
||||
frame[lane][2].mul_add(
|
||||
vector[2],
|
||||
frame[lane][1].mul_add(vector[1], frame[lane][0] * vector[0]),
|
||||
)
|
||||
})
|
||||
}
|
||||
fn rotate(frame: AnimationPartTransform, vector: [f32; 4]) -> [f32; 4] {
|
||||
core::array::from_fn(|lane| {
|
||||
frame[2][lane].mul_add(
|
||||
vector[2],
|
||||
frame[1][lane].mul_add(vector[1], frame[0][lane] * vector[0]),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn pitched_deck_is_projected_before_the_distinct_ground_frame() {
|
||||
// Deck Y points world Z. Ground X points world Z. World Z is removed
|
||||
// before changing frame; removing only final Y would incorrectly keep it.
|
||||
let deck = [
|
||||
[1., 0., 0., 0.],
|
||||
[0., 0., 1., 0.],
|
||||
[0., -1., 0., 0.],
|
||||
[7., 8., 9., 0.],
|
||||
];
|
||||
let ground = [
|
||||
[0., 0., 1., 0.],
|
||||
[0., 1., 0., 0.],
|
||||
[-1., 0., 0., 0.],
|
||||
[0.; 4],
|
||||
];
|
||||
let result = condition(Input {
|
||||
deck,
|
||||
ground,
|
||||
world_acceleration: [2., 3., 4., 0.],
|
||||
});
|
||||
assert_eq!(&result[..3], &[0., 0., -2.]);
|
||||
}
|
||||
#[test]
|
||||
fn bump_uses_only_scaled_horizontal_magnitude_and_strict_threshold() {
|
||||
let settings = Settings {
|
||||
scale_x_acc: 2.,
|
||||
min_bump_mag: 10.,
|
||||
};
|
||||
assert!(!is_bumped([0.; 4], &settings));
|
||||
assert!(!is_bumped([0., 0., 10., 0.], &settings));
|
||||
assert!(is_bumped([6., 0., 0., 0.], &settings));
|
||||
assert!(!is_bumped([0., 0., 0., 999.], &settings));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
//! FilterMotionGraphIntent TU3 82BB1730/17D0/19F0 and 82BB1B78.
|
||||
//! Coefficients and limits are per graph update; only the ramp uses elapsed time.
|
||||
use crate::input::graph_intents::apply_filter;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct Settings {
|
||||
pub starting_value: f32,
|
||||
pub default_value: f32,
|
||||
pub scale: f32,
|
||||
pub filters: [u32; 4],
|
||||
pub ramp_time: Option<f32>,
|
||||
pub blend_rising: f32,
|
||||
pub blend_falling: f32,
|
||||
pub blend_out: Option<f32>,
|
||||
pub clamp_velocity: Option<f32>,
|
||||
pub clamp_acceleration: Option<f32>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub struct State {
|
||||
pub elapsed: f32,
|
||||
pub previous_delta: f32,
|
||||
pub value: f32,
|
||||
}
|
||||
|
||||
impl State {
|
||||
/// Begin publishes startingValue directly, without scaling or filtering.
|
||||
pub fn begin(&mut self, settings: &Settings) -> f32 {
|
||||
self.elapsed = 0.0;
|
||||
self.previous_delta = 0.0;
|
||||
self.value = settings.starting_value;
|
||||
self.value
|
||||
}
|
||||
|
||||
/// Stance is the live animation component's fakie/mirrored getter pair.
|
||||
pub fn update(
|
||||
&mut self,
|
||||
settings: &Settings,
|
||||
input: Option<f32>,
|
||||
dt: f32,
|
||||
stance: (bool, bool),
|
||||
) -> f32 {
|
||||
self.elapsed += dt;
|
||||
let mut target = settings.scale * input.unwrap_or(settings.default_value);
|
||||
for (kind, enabled) in settings.filters.into_iter().zip([true, true, stance.0, stance.1]) {
|
||||
if enabled && kind <= 7 {
|
||||
target = apply_filter(target, kind);
|
||||
}
|
||||
}
|
||||
let mut blend = if target < self.value {
|
||||
settings.blend_falling
|
||||
} else {
|
||||
settings.blend_rising
|
||||
};
|
||||
if input.is_some() {
|
||||
if let Some(time) = settings.ramp_time {
|
||||
blend = ramp(self.elapsed, time) * blend;
|
||||
}
|
||||
} else if let Some(out) = settings.blend_out {
|
||||
blend = out;
|
||||
}
|
||||
// 82BB194C..1968: separate target product, then fmadds, then subtract.
|
||||
let weighted_target = blend * target;
|
||||
let candidate = (1.0 - blend).mul_add(self.value, weighted_target);
|
||||
let mut delta = candidate - self.value;
|
||||
if let Some(limit) = settings.clamp_acceleration {
|
||||
delta = limit_delta(delta, self.previous_delta - limit, self.previous_delta + limit);
|
||||
}
|
||||
if let Some(limit) = settings.clamp_velocity {
|
||||
delta = limit_delta(delta, -limit, limit);
|
||||
}
|
||||
self.previous_delta = delta;
|
||||
self.value = delta + self.value;
|
||||
self.value
|
||||
}
|
||||
}
|
||||
|
||||
fn limit_delta(value: f32, lower: f32, upper: f32) -> f32 {
|
||||
// Preserve the original subtract/select ordering, including unordered input.
|
||||
let selected = if lower - value >= 0.0 { lower } else { value };
|
||||
if upper - selected >= 0.0 { selected } else { upper }
|
||||
}
|
||||
|
||||
fn ramp(elapsed: f32, time: f32) -> f32 {
|
||||
// 82BA8720 uses signed integer comparisons of float bits in lower_bound.
|
||||
// Constructor 82BB1570..15BC supplies precisely (0,0),(rampTime,1).
|
||||
let keys = [0.0f32, time];
|
||||
let mut begin = 0usize;
|
||||
let mut count = 2usize;
|
||||
while count != 0 {
|
||||
let step = count / 2;
|
||||
let middle = begin + step;
|
||||
if (keys[middle].to_bits() as i32) < (elapsed.to_bits() as i32) {
|
||||
begin = middle + 1;
|
||||
count -= step + 1;
|
||||
} else {
|
||||
count = step;
|
||||
}
|
||||
}
|
||||
match begin {
|
||||
0 => 0.0,
|
||||
2 => 1.0,
|
||||
_ => ((1.0f32 - 0.0) / (time - 0.0)).mul_add(elapsed - 0.0, 0.0),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "intent_filter/tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,66 @@
|
||||
use super::*;
|
||||
|
||||
fn settings() -> Settings {
|
||||
Settings {
|
||||
starting_value: 0.0, default_value: 0.0, scale: 1.0, filters: [0; 4],
|
||||
ramp_time: None, blend_rising: 0.5, blend_falling: 0.25,
|
||||
blend_out: Some(0.125), clamp_velocity: None, clamp_acceleration: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn starting_value_bypasses_scale_and_filters_and_reentry_resets_history() {
|
||||
let mut s = State { elapsed: 4.0, previous_delta: 9.0, value: 7.0 };
|
||||
let mut p = settings();
|
||||
p.starting_value = 2.0;
|
||||
p.scale = 3.0;
|
||||
p.filters = [1; 4];
|
||||
assert_eq!(s.begin(&p), 2.0);
|
||||
assert_eq!(s.elapsed, 0.0);
|
||||
assert_eq!(s.previous_delta, 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_input_uses_blend_out_without_ramp_and_zero_is_present() {
|
||||
let mut p = settings();
|
||||
p.ramp_time = Some(8.0);
|
||||
let mut s = State { value: 1.0, ..State::default() };
|
||||
assert_eq!(s.update(&p, None, 1.0, (false, false)), 0.875);
|
||||
s = State { value: 1.0, ..State::default() };
|
||||
assert_eq!(s.update(&p, Some(0.0), 1.0, (false, false)), 0.96875);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn acceleration_precedes_velocity_and_limits_are_not_scaled_by_dt() {
|
||||
let mut p = settings();
|
||||
p.blend_rising = 1.0;
|
||||
p.clamp_acceleration = Some(0.25);
|
||||
p.clamp_velocity = Some(0.125);
|
||||
let mut s = State::default();
|
||||
assert_eq!(s.update(&p, Some(4.0), 0.0, (false, false)), 0.125);
|
||||
assert_eq!(s.update(&p, Some(4.0), 3.0, (false, false)), 0.25);
|
||||
assert_eq!(s.update(&p, Some(-4.0), 0.0, (false, false)), 0.125);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scale_and_ordered_filters_use_live_stance() {
|
||||
let mut p = settings();
|
||||
p.blend_rising = 1.0;
|
||||
p.blend_falling = 1.0;
|
||||
p.scale = 2.0;
|
||||
p.filters = [1, 2, 3, 1];
|
||||
let mut s = State::default();
|
||||
assert_eq!(s.update(&p, Some(0.125), 0.0, (false, false)), 0.25);
|
||||
assert_eq!(s.update(&p, Some(0.125), 0.0, (true, true)), -0.75);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ramp_has_native_endpoint_and_signed_bit_ordering() {
|
||||
assert_eq!(ramp(0.0, 2.0), 0.0);
|
||||
assert_eq!(ramp(0.5, 2.0), 0.25);
|
||||
assert_eq!(ramp(2.0, 2.0), 1.0);
|
||||
assert_eq!(ramp(3.0, 2.0), 1.0);
|
||||
assert_eq!(ramp(-1.0, 2.0), 0.0);
|
||||
assert_eq!(ramp(0.0, 0.0), 0.0);
|
||||
assert_eq!(ramp(1.0, 0.0), 1.0);
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
//! Original Skate 3 TU3 KickTurnSteering: constructor82BAE730,
|
||||
//! Begin82BAE430 and Update82BAE4A8. Curves are Globals324 anim_motion/kickturn.
|
||||
use crate::point_graph::PointGraph;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Settings {
|
||||
pub spin: PointGraph<8>,
|
||||
pub height: PointGraph<8>,
|
||||
}
|
||||
|
||||
/// Authored behavior fields28/32; constructor82BC7218 defaults both to0.5.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct Parameters {
|
||||
pub max_height: f32,
|
||||
pub spin_scale: f32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct State {
|
||||
animation_length: f32,
|
||||
elapsed: f32,
|
||||
first_update: bool,
|
||||
previous_height: f32,
|
||||
last_nonzero_intent: f32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct Output {
|
||||
/// Published first, to the actual MotionGraph Balance attribute.
|
||||
pub balance: f32,
|
||||
pub spin: f32,
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
animation_length: 0.0,
|
||||
elapsed: 0.0,
|
||||
first_update: true,
|
||||
previous_height: 0.0,
|
||||
last_nonzero_intent: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn begin(&mut self, settings: &Settings) {
|
||||
*self = Self::new();
|
||||
//82BAE480 seeds the unscaled curve value, before applying max_height.
|
||||
self.previous_height = settings.height.evaluate(0.0);
|
||||
}
|
||||
|
||||
pub fn needs_animation_length(&self) -> bool {
|
||||
self.first_update
|
||||
}
|
||||
|
||||
/// The caller first applies pending animation parameters, then reads the
|
||||
/// base animation tree's length. No time is added on that first update.
|
||||
pub fn capture_animation_length(&mut self, length: f32) {
|
||||
self.animation_length = length;
|
||||
}
|
||||
|
||||
pub fn update(
|
||||
&mut self,
|
||||
dt: f32,
|
||||
intent: f32,
|
||||
parameters: Parameters,
|
||||
settings: &Settings,
|
||||
) -> Output {
|
||||
if !self.first_update {
|
||||
self.elapsed += dt;
|
||||
}
|
||||
let phase = bound(self.elapsed / self.animation_length, 1.0);
|
||||
let spin_curve = settings.spin.evaluate(phase);
|
||||
let height_curve = settings.height.evaluate(phase);
|
||||
//82BAE660 fmadds, followed by separate82BAE670 fmuls. TU3 fixes
|
||||
//the blend coefficient at0.5; the S2 debug alternatives are absent.
|
||||
let sum = height_curve.mul_add(parameters.max_height, self.previous_height);
|
||||
let height = bound(sum * 0.5, parameters.max_height);
|
||||
self.previous_height = height;
|
||||
if intent != 0.0 {
|
||||
self.last_nonzero_intent = intent;
|
||||
}
|
||||
let sign = if self.last_nonzero_intent >= 0.0 {
|
||||
1.0
|
||||
} else {
|
||||
-1.0
|
||||
};
|
||||
let output = Output {
|
||||
balance: -height,
|
||||
spin: (parameters.spin_scale * sign) * spin_curve,
|
||||
};
|
||||
self.first_update = false;
|
||||
output
|
||||
}
|
||||
}
|
||||
|
||||
//Source fsel(-value,0,value), then fsel(max-lower,lower,max).
|
||||
//Preserves source unordered handling: a NaN selects the upper bound.
|
||||
fn bound(value: f32, maximum: f32) -> f32 {
|
||||
let lower = if -value >= 0.0 { 0.0 } else { value };
|
||||
if maximum - lower >= 0.0 {
|
||||
lower
|
||||
} else {
|
||||
maximum
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests/kickturn.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,148 @@
|
||||
//! Original TU3 PhysicsConditioner::CalcLandingQuality, 0x82DE61D0.
|
||||
use crate::{
|
||||
physics::{board_motion_output::inverse_length_squared, native_arithmetic},
|
||||
point_graph::PointGraph,
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Settings {
|
||||
pub twist_spin: PointGraph<4>,
|
||||
pub side_speed: PointGraph<4>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Input {
|
||||
/// PhysicsConditioner +144 and +148, after its filtered-state update.
|
||||
pub previous_filtered_state: u32,
|
||||
pub filtered_state: u32,
|
||||
/// PhysOut_Collision +32. Do not substitute a predicted landing normal.
|
||||
pub ground_normal: [f32; 4],
|
||||
/// PhysOut_SkateboardMotion +80 and +273.
|
||||
pub deck_velocity: [f32; 4],
|
||||
pub flipped: bool,
|
||||
/// PhysOut_SkateboardReckoning +32: physical deck PART world Z from
|
||||
/// 82C02AD8/82C02B1C. This is not the conditioned Reckoning ground frame.
|
||||
pub reckoning_forward: [f32; 4],
|
||||
/// PhysOut_Air +204.
|
||||
pub air_spin: f32,
|
||||
}
|
||||
|
||||
/// Reset values are explicitly written by 0x82DE3F38.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub struct Output {
|
||||
pub landing_adjust_80: f32,
|
||||
pub sideways_speed_84: f32,
|
||||
pub forward_speed_88: f32,
|
||||
pub spin_92: f32,
|
||||
pub landing_type_96: u32,
|
||||
pub landing_data_167: bool,
|
||||
}
|
||||
|
||||
impl Output {
|
||||
/// Nonlanding frames preserve every field, including the valid-data latch.
|
||||
pub fn update(&mut self, input: Input, settings: &Settings) {
|
||||
if input.previous_filtered_state == 1 || input.filtered_state != 1 {
|
||||
return;
|
||||
}
|
||||
let velocity = planar(input.deck_velocity, input.ground_normal);
|
||||
let forward = planar(input.reckoning_forward, input.ground_normal);
|
||||
let speed = length(velocity);
|
||||
let forward_length = length(forward);
|
||||
let mut forward_speed = dot(velocity, forward).abs();
|
||||
let mut side_speed = dot(cross(forward, input.ground_normal), input.deck_velocity).abs();
|
||||
let mut kind = 0;
|
||||
let mut spin = 0.0;
|
||||
if forward_length * speed < f32::from_bits(0x3727_c5ac) {
|
||||
forward_speed = 0.0;
|
||||
side_speed = 0.0;
|
||||
} else {
|
||||
let normalized_velocity = scale(velocity, reciprocal(speed));
|
||||
let normalized_forward = scale(forward, reciprocal(forward_length));
|
||||
let mut orientation = cross(normalized_forward, normalized_velocity)[1];
|
||||
if input.flipped {
|
||||
orientation = -orientation;
|
||||
}
|
||||
let angular_speed = -input.air_spin;
|
||||
let spin_input = (angular_speed.abs() - 0.1) * 0.14492753;
|
||||
let spin_curve = settings.twist_spin.evaluate(upper_one(spin_input));
|
||||
let side_input = (side_speed - 0.1) * 0.1010101;
|
||||
let side_curve = settings.side_speed.evaluate(upper_one(side_input));
|
||||
if speed >= 2.0 {
|
||||
let heading = if input.flipped {
|
||||
scale(normalized_forward, -1.0)
|
||||
} else {
|
||||
normalized_forward
|
||||
};
|
||||
let toward = dot(heading, normalized_velocity) >= 0.0;
|
||||
let magnitude;
|
||||
if angular_speed.abs() <= 0.1 {
|
||||
kind = 1;
|
||||
magnitude = side_curve;
|
||||
} else {
|
||||
let product = angular_speed * orientation;
|
||||
kind = if (toward && product <= 0.0) || (!toward && product > 0.0) {
|
||||
2
|
||||
} else {
|
||||
1
|
||||
};
|
||||
magnitude = if spin_curve - side_curve >= -0.0 {
|
||||
spin_curve
|
||||
} else {
|
||||
side_curve
|
||||
};
|
||||
}
|
||||
spin = if angular_speed >= -0.0 {
|
||||
magnitude
|
||||
} else {
|
||||
-magnitude
|
||||
};
|
||||
if orientation.abs() < 0.2 && (kind != 2 || !(spin.abs() > 0.3)) {
|
||||
kind = if orientation.abs() >= 0.05 { 3 } else { 0 };
|
||||
spin = 0.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
*self = Self {
|
||||
landing_adjust_80: spin,
|
||||
sideways_speed_84: side_speed,
|
||||
forward_speed_88: forward_speed,
|
||||
spin_92: spin,
|
||||
landing_type_96: kind,
|
||||
landing_data_167: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn dot(a: [f32; 4], b: [f32; 4]) -> f32 {
|
||||
native_arithmetic::dot3(a, b)
|
||||
}
|
||||
fn scale(a: [f32; 4], b: f32) -> [f32; 4] {
|
||||
a.map(|x| x * b)
|
||||
}
|
||||
fn planar(a: [f32; 4], normal: [f32; 4]) -> [f32; 4] {
|
||||
let projected = scale(normal, dot(normal, a));
|
||||
std::array::from_fn(|i| a[i] - projected[i])
|
||||
}
|
||||
fn cross(a: [f32; 4], b: [f32; 4]) -> [f32; 4] {
|
||||
[
|
||||
(-a[2]).mul_add(b[1], a[1] * b[2]),
|
||||
(-a[0]).mul_add(b[2], a[2] * b[0]),
|
||||
(-a[1]).mul_add(b[0], a[0] * b[1]),
|
||||
0.0,
|
||||
]
|
||||
}
|
||||
fn length(a: [f32; 4]) -> f32 {
|
||||
let square = dot(a, a);
|
||||
let value = square * inverse_length_squared(square, 2);
|
||||
if square == 0.0 { 0.0 } else { value }
|
||||
}
|
||||
fn reciprocal(value: f32) -> f32 {
|
||||
let mut estimate = native_arithmetic::reciprocal_estimate(value);
|
||||
for _ in 0..2 {
|
||||
estimate = estimate.mul_add((-estimate).mul_add(value, 1.0), estimate);
|
||||
}
|
||||
estimate
|
||||
}
|
||||
fn upper_one(value: f32) -> f32 {
|
||||
if 1.0 - value >= -0.0 { value } else { 1.0 }
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
//! Animation timing and event semantics recovered from Skate 3 TU3.
|
||||
pub mod clip_clock;
|
||||
pub mod commands;
|
||||
pub mod output;
|
||||
pub mod skeleton_input;
|
||||
pub mod playback;
|
||||
pub mod playback_parameters;
|
||||
pub mod playback_clip;
|
||||
pub mod playback_attributes;
|
||||
pub mod playback_tree;
|
||||
pub mod phase_blend;
|
||||
pub mod pose_blend;
|
||||
pub mod playback_transition;
|
||||
pub mod crouching;
|
||||
pub mod pose_sample;
|
||||
pub mod pose_trajectory;
|
||||
pub mod pose_add;
|
||||
pub mod pose_mirror;
|
||||
pub mod body_tilt;
|
||||
pub mod air_leg_extension;
|
||||
pub mod landing_quality;
|
||||
pub mod riding_fakie;
|
||||
pub mod channel_playback;
|
||||
pub mod pumping_channel;
|
||||
pub mod physical_feedback;
|
||||
pub mod ground_acceleration;
|
||||
pub mod foot_ik;
|
||||
pub mod vbr;
|
||||
pub mod kickturn;
|
||||
|
||||
pub mod intent_filter;
|
||||
|
||||
pub mod posture;
|
||||
@@ -0,0 +1,128 @@
|
||||
//! Actor field publication825937EC..825939C8 and external payload copy82592810.
|
||||
//! Runtime services remain explicit source interfaces; their implementation is
|
||||
//! not supplied here and these fields do not manufacture a gameplay impulse.
|
||||
use super::{packet_reset::AdditionalResetFields, physics_packet::PhysicsPosePacket};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct ExternalPhysicsInput {
|
||||
/// Ten complete vectors; word representation preserves non-arithmetic bits.
|
||||
pub vectors: [[u32; 4]; 10],
|
||||
pub flags: u32,
|
||||
}
|
||||
impl ExternalPhysicsInput {
|
||||
///82592810 copies160 bytes and only the high seven flag bits. Remaining
|
||||
/// destination flag bits and native padding are not overwritten.
|
||||
pub fn copy_from(&mut self, source: &Self) {
|
||||
self.vectors = source.vectors;
|
||||
self.flags = (self.flags & 0x01ff_ffff) | (source.flags & 0xfe00_0000);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct ExternalReset {
|
||||
pub transform: [[u32; 4]; 4],
|
||||
/// Source payload+64 -> packet10768; retains the complete byte.
|
||||
pub byte64: u8,
|
||||
}
|
||||
|
||||
pub struct ActorPacketFields {
|
||||
pub external_impulse: [u32; 4],
|
||||
pub external_physics: ExternalPhysicsInput,
|
||||
pub external_reset: ExternalReset,
|
||||
/// Actor1904 bit29 -> packet10784; not reset by82590028.
|
||||
pub actor_flag_1904_bit29: bool,
|
||||
}
|
||||
|
||||
pub struct ActorPublicationState {
|
||||
pub flags1904: u32,
|
||||
pub flags1908: u32,
|
||||
pub external_controller_present: bool,
|
||||
pub external_impulse: [u32; 4],
|
||||
pub external_physics: ExternalPhysicsInput,
|
||||
pub external_reset: ExternalReset,
|
||||
/// Actor1768 selects the native168-byte per-player settings record.
|
||||
pub player_index: u32,
|
||||
}
|
||||
|
||||
/// A source-owned binding to the settings object captured before Actor+56's
|
||||
/// virtual call. Each read uses the same captured object and168-byte stride.
|
||||
pub trait PlayerPhysicsSettingsSource {
|
||||
type Error;
|
||||
/// Record+160. This read is skipped by the fixed-settings branch.
|
||||
fn truck_tightness(&mut self, player_index: u32) -> Result<f32, Self::Error>;
|
||||
/// Record+164. This read is skipped by the fixed-settings branch.
|
||||
fn wheel_hardness(&mut self, player_index: u32) -> Result<f32, Self::Error>;
|
||||
/// Record+172. Read after either branch has written both output scalars.
|
||||
fn requested_physics_mode(&mut self, player_index: u32) -> Result<u32, Self::Error>;
|
||||
}
|
||||
|
||||
/// Exact unresolved external service boundaries for this field publisher.
|
||||
/// A game adapter must implement these from their source-owned systems.
|
||||
pub trait ActorPublicationEnvironment {
|
||||
/// Implementations must report unavailable source services as an error.
|
||||
/// There is deliberately no default or placeholder implementation.
|
||||
type Error;
|
||||
type Settings: PlayerPhysicsSettingsSource<Error = Self::Error>;
|
||||
///8259FC28: global830CFD94 ->220 ->328 virtual12. None means no current
|
||||
/// object. Otherwise property824639E8(object20), including its native
|
||||
/// default, supplies the enum. Property identifier is-616069660.
|
||||
fn current_scene_mode(&mut self) -> Result<Option<i32>, Self::Error>;
|
||||
/// Read global byte830B7C30 verbatim, without bool normalization.
|
||||
fn ignore_respawn_reset_button(&mut self) -> Result<u8, Self::Error>;
|
||||
/// Actor subobject+56 virtual16, low return byte. Flags25/28 have already
|
||||
/// been consumed when called. Concrete vtable binding remains unresolved.
|
||||
fn actor_subobject56_slot16(
|
||||
&mut self,
|
||||
actor: &mut ActorPublicationState,
|
||||
) -> Result<u8, Self::Error>;
|
||||
/// Capture global83067068 at82593954 before slot16 at8259395C. The binding
|
||||
/// must stay valid across that callback and the subsequent field reads.
|
||||
/// Do not snapshot record values here: native reads them after the callback.
|
||||
fn capture_physics_settings(&mut self) -> Result<Self::Settings, Self::Error>;
|
||||
}
|
||||
|
||||
/// Full actor-owned suffix on distinct actor/packet payload storage. Scene
|
||||
/// lookup is skipped for an external controller or force-braking bit26.
|
||||
/// This stops before the physical object's borrowed-packet setter82DB3C48.
|
||||
pub fn publish<E: ActorPublicationEnvironment>(
|
||||
actor: &mut ActorPublicationState,
|
||||
pose: &mut PhysicsPosePacket,
|
||||
reset: &mut AdditionalResetFields,
|
||||
fields: &mut ActorPacketFields,
|
||||
environment: &mut E,
|
||||
) -> Result<(), E::Error> {
|
||||
reset.actor_flag_1904_bit23 = actor.flags1904 & (1 << 23) != 0;
|
||||
reset.actor_flag_1908_bit2 = actor.flags1908 & (1 << 2) != 0;
|
||||
reset.external_impulse_active = actor.flags1904 & (1 << 30) != 0;
|
||||
fields.external_impulse = actor.external_impulse;
|
||||
fields.external_physics.copy_from(&actor.external_physics);
|
||||
reset.external_physics_input_active = actor.flags1904 & (1 << 27) != 0;
|
||||
let scene_mode_flag = !actor.external_controller_present
|
||||
&& actor.flags1904 & (1 << 26) == 0
|
||||
&& matches!(environment.current_scene_mode()?, Some(19 | 20));
|
||||
replace_flag(&mut pose.flags, 22, scene_mode_flag);
|
||||
reset.externally_controlled = actor.external_controller_present;
|
||||
fields.external_reset = actor.external_reset;
|
||||
fields.actor_flag_1904_bit29 = actor.flags1904 & (1 << 29) != 0;
|
||||
reset.prevent_manual_respawn = actor.flags1904 & (1 << 28) != 0;
|
||||
reset.force_braking = actor.flags1904 & (1 << 26) != 0;
|
||||
replace_flag(&mut pose.flags, 24, actor.flags1904 & (1 << 25) != 0);
|
||||
actor.flags1904 &= !(1 << 25);
|
||||
reset.ignore_respawn_reset_button = environment.ignore_respawn_reset_button()?;
|
||||
actor.flags1904 &= !(1 << 28);
|
||||
let mut settings = environment.capture_physics_settings()?;
|
||||
let use_fixed_settings = environment.actor_subobject56_slot16(actor)? != 0;
|
||||
if use_fixed_settings {
|
||||
reset.truck_tightness = f32::from_bits(0x3f33_3333);
|
||||
reset.wheel_hardness = f32::from_bits(0x3f33_3333);
|
||||
} else {
|
||||
reset.truck_tightness = settings.truck_tightness(actor.player_index)?;
|
||||
reset.wheel_hardness = settings.wheel_hardness(actor.player_index)?;
|
||||
}
|
||||
reset.requested_physics_mode = settings.requested_physics_mode(actor.player_index)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn replace_flag(flags: &mut u32, bit: u32, value: bool) {
|
||||
*flags = (*flags & !(1 << bit)) | (u32::from(value) << bit);
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
//! Attribute copying and ordered packet merge from TU3 8258EED0/8258F210.
|
||||
//! Binary names stay at this typed boundary. This module does not interpret
|
||||
//! Skeleton attributes or invent a physics effect from their names.
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct AttributeName(pub [u32; 5]);
|
||||
|
||||
/// The six union lanes retain initialization separately from value. Copying a
|
||||
/// scalar must not fabricate values for the other five lanes. Words preserve
|
||||
/// all floating-point payload bits across non-arithmetic publication.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct AttributePayload(pub [Option<u32>; 6]);
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct AnimationAttribute {
|
||||
pub payload: AttributePayload,
|
||||
pub begin_time: f32,
|
||||
pub end_time: f32,
|
||||
pub name: AttributeName,
|
||||
pub status: u8,
|
||||
/// Native+53:0/2 scalar,1 vector,3 six-word payload; other kinds copy none.
|
||||
pub kind: u8,
|
||||
pub sequence_id: i32,
|
||||
}
|
||||
|
||||
impl AnimationAttribute {
|
||||
/// Complete observable field assignment8258EED0. Native padding at54/55
|
||||
/// and60..63 is not copied and has no represented domain field.
|
||||
pub fn copy_from(&mut self, source: &Self) {
|
||||
self.kind = source.kind;
|
||||
self.name = source.name;
|
||||
self.begin_time = source.begin_time;
|
||||
self.end_time = source.end_time;
|
||||
self.status = source.status;
|
||||
self.sequence_id = source.sequence_id;
|
||||
let lanes = match self.kind {
|
||||
0 | 2 => 1,
|
||||
1 => 4,
|
||||
3 => 6,
|
||||
_ => 0,
|
||||
};
|
||||
self.payload.0[..lanes].copy_from_slice(&source.payload.0[..lanes]);
|
||||
}
|
||||
|
||||
/// Copy-construction8258EE68 initializes name storage then performs the
|
||||
/// assignment above. Its inactive union lanes are unspecified, not zero.
|
||||
fn copy_construct(source: &Self) -> Self {
|
||||
let mut result = Self {
|
||||
payload: AttributePayload([None; 6]),
|
||||
..*source
|
||||
};
|
||||
result.copy_from(source);
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct MotionGraphAttribute {
|
||||
/// Native24-byte MG record:20-byte name followed by scalar.
|
||||
pub name: AttributeName,
|
||||
pub value: f32,
|
||||
}
|
||||
impl MotionGraphAttribute {
|
||||
///8258F210 makes an untimed scalar attribute with status6 and sequence-1.
|
||||
pub fn to_animation(self) -> AnimationAttribute {
|
||||
let mut payload = AttributePayload([None; 6]);
|
||||
payload.0[0] = Some(self.value.to_bits());
|
||||
AnimationAttribute {
|
||||
payload,
|
||||
begin_time: -1.0,
|
||||
end_time: -1.0,
|
||||
name: self.name,
|
||||
status: 6,
|
||||
kind: 0,
|
||||
sequence_id: -1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Active entries plus retained slots model native end=begin list reset.
|
||||
/// Capacity allocation policy is host-owned; active ordering and union-lane
|
||||
/// preservation follow native append/copy semantics. No sorting/deduplication.
|
||||
#[derive(Default)]
|
||||
pub struct PacketAttributes {
|
||||
slots: Vec<AnimationAttribute>,
|
||||
active_len: usize,
|
||||
}
|
||||
impl PacketAttributes {
|
||||
pub fn entries(&self) -> &[AnimationAttribute] {
|
||||
&self.slots[..self.active_len]
|
||||
}
|
||||
pub fn clear(&mut self) {
|
||||
self.active_len = 0;
|
||||
}
|
||||
pub fn append(&mut self, source: &AnimationAttribute) {
|
||||
if let Some(slot) = self.slots.get_mut(self.active_len) {
|
||||
slot.copy_from(source);
|
||||
} else {
|
||||
self.slots.push(AnimationAttribute::copy_construct(source));
|
||||
}
|
||||
self.active_len += 1;
|
||||
}
|
||||
|
||||
/// Exact list operation order at82593750..825937E4: discard prior active
|
||||
/// list, append converted MG records, then tree records in stored order.
|
||||
/// Duplicate names remain distinct records for the later Skeleton dispatch.
|
||||
pub fn replace_from(
|
||||
&mut self,
|
||||
motion_graph: &[MotionGraphAttribute],
|
||||
tree: &[AnimationAttribute],
|
||||
) {
|
||||
self.clear();
|
||||
for attribute in motion_graph {
|
||||
self.append(&attribute.to_animation());
|
||||
}
|
||||
for attribute in tree {
|
||||
self.append(attribute);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
use super::{NativeMatrix, PoseBufferError};
|
||||
|
||||
/// TU3 0x828D3B58: local matrices to hierarchy-composed matrices.
|
||||
///
|
||||
/// `detached_parent` is native r5 (hierarchy +8 at the caller). A parent equal to
|
||||
/// it, or -1, causes a verbatim local copy. No world transform is introduced.
|
||||
/// The first matrix is copied even for a nonpositive count. Other output slots
|
||||
/// retain caller state until visited, including when a forward parent is read.
|
||||
pub fn compose_hierarchy(
|
||||
bone_count: i32,
|
||||
parents: &[i32],
|
||||
detached_parent: i32,
|
||||
local: &[NativeMatrix],
|
||||
destination: &mut [NativeMatrix],
|
||||
) -> Result<(), PoseBufferError> {
|
||||
let count = bone_count.max(0) as usize;
|
||||
validate(
|
||||
count,
|
||||
parents,
|
||||
detached_parent,
|
||||
local.len(),
|
||||
destination.len(),
|
||||
)?;
|
||||
destination[0] = local[0];
|
||||
for bone in 0..count {
|
||||
let parent = parents[bone];
|
||||
destination[bone] = if parent == -1 || parent == detached_parent {
|
||||
local[bone]
|
||||
} else {
|
||||
concatenate(local[bone], destination[parent as usize])
|
||||
};
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The same native function with identical source/destination matrix pointers.
|
||||
pub fn compose_hierarchy_in_place(
|
||||
bone_count: i32,
|
||||
parents: &[i32],
|
||||
detached_parent: i32,
|
||||
matrices: &mut [NativeMatrix],
|
||||
) -> Result<(), PoseBufferError> {
|
||||
let count = bone_count.max(0) as usize;
|
||||
validate(
|
||||
count,
|
||||
parents,
|
||||
detached_parent,
|
||||
matrices.len(),
|
||||
matrices.len(),
|
||||
)?;
|
||||
for bone in 0..count {
|
||||
let parent = parents[bone];
|
||||
if parent != -1 && parent != detached_parent {
|
||||
matrices[bone] = concatenate(matrices[bone], matrices[parent as usize]);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate(
|
||||
count: usize,
|
||||
parents: &[i32],
|
||||
detached_parent: i32,
|
||||
input_len: usize,
|
||||
output_len: usize,
|
||||
) -> Result<(), PoseBufferError> {
|
||||
if input_len < count.max(1) {
|
||||
return Err(PoseBufferError::ShortInput);
|
||||
}
|
||||
if output_len < count.max(1) {
|
||||
return Err(PoseBufferError::ShortOutput);
|
||||
}
|
||||
if parents.len() < count {
|
||||
return Err(PoseBufferError::ShortParents);
|
||||
}
|
||||
for (bone, &parent) in parents[..count].iter().enumerate() {
|
||||
if parent != -1
|
||||
&& parent != detached_parent
|
||||
&& (parent < 0 || parent as usize >= output_len)
|
||||
{
|
||||
return Err(PoseBufferError::InvalidParent { bone, parent });
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn concatenate(local: NativeMatrix, parent: NativeMatrix) -> NativeMatrix {
|
||||
let mut result = local;
|
||||
for row in 0..4 {
|
||||
for lane in 0..4 {
|
||||
let first = if row == 3 {
|
||||
parent[0][lane].mul_add(local[row][0], parent[3][lane])
|
||||
} else {
|
||||
parent[0][lane] * local[row][0]
|
||||
};
|
||||
let second = parent[1][lane].mul_add(local[row][1], first);
|
||||
result[row][lane] = parent[2][lane].mul_add(local[row][2], second);
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
//! Fixed intent-map specialization constructed by8258F638, used at full
|
||||
//! MotionGraph+12 (82C0E088) and physics packet+8 (8258FEE8).
|
||||
//! Names are six encoded words; no guessed text hashing or guest addresses.
|
||||
|
||||
use crate::input::graph_intents::IntentMutation;
|
||||
|
||||
pub const BUCKET_COUNT: usize = 61;
|
||||
pub const ENTRY_CAPACITY: usize = 64;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct IntentName(pub [u32; 6]);
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct Intent {
|
||||
pub name: IntentName,
|
||||
pub value: f32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct IntentCapacityExceeded;
|
||||
|
||||
/// Native construction takes greatest prime<=65 (61), installs64 nodes, then
|
||||
/// changes maximum load to10000. Thus every valid state of this specialization
|
||||
/// stays at61 buckets; generic rehash policy is unreachable before exhaustion.
|
||||
/// Owned bucket vectors represent chains in traversal order. Allocator node
|
||||
/// identities/free-list addresses are deliberately outside the domain model.
|
||||
pub struct IntentMap {
|
||||
buckets: [Vec<Intent>; BUCKET_COUNT],
|
||||
len: usize,
|
||||
}
|
||||
|
||||
impl Default for IntentMap {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
buckets: std::array::from_fn(|_| Vec::new()),
|
||||
len: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntentMap {
|
||||
pub fn len(&self) -> usize {
|
||||
self.len
|
||||
}
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.len == 0
|
||||
}
|
||||
|
||||
pub fn get(&self, name: IntentName) -> Option<f32> {
|
||||
let hash = name.0.iter().fold(0u32, |sum, word| sum.wrapping_add(*word));
|
||||
self.buckets[hash as usize % BUCKET_COUNT]
|
||||
.iter()
|
||||
.find(|entry| entry.name == name)
|
||||
.map(|entry| entry.value)
|
||||
}
|
||||
|
||||
///826C20B0/825A0350: ascending buckets, head-to-tail within each chain.
|
||||
pub fn entries(&self) -> impl Iterator<Item = &Intent> {
|
||||
self.buckets.iter().flat_map(|bucket| bucket.iter())
|
||||
}
|
||||
|
||||
///82472CD0/82472E10/824714D0: wrapping sum of all six words, exact
|
||||
/// six-word equality, prepend new entries. Existing values are preserved.
|
||||
/// At exhausted capacity native faults; the host rejects the new entry.
|
||||
pub fn insert(&mut self, intent: Intent) -> Result<bool, IntentCapacityExceeded> {
|
||||
let hash = intent
|
||||
.name
|
||||
.0
|
||||
.iter()
|
||||
.fold(0u32, |sum, word| sum.wrapping_add(*word));
|
||||
let bucket = &mut self.buckets[hash as usize % BUCKET_COUNT];
|
||||
if bucket.iter().any(|entry| entry.name == intent.name) {
|
||||
return Ok(false);
|
||||
}
|
||||
if self.len == ENTRY_CAPACITY {
|
||||
return Err(IntentCapacityExceeded);
|
||||
}
|
||||
bucket.insert(0, intent);
|
||||
self.len += 1;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// Set the value at an existing native key, or insert a new key. The
|
||||
/// native 82471818 lookup returns a value slot before the caller writes
|
||||
/// it, so Set must overwrite an existing entry rather than silently
|
||||
/// preserving it as `insert` does.
|
||||
pub fn set(&mut self, name: IntentName, value: f32) -> Result<bool, IntentCapacityExceeded> {
|
||||
let hash = name
|
||||
.0
|
||||
.iter()
|
||||
.fold(0u32, |sum, word| sum.wrapping_add(*word));
|
||||
let bucket = &mut self.buckets[hash as usize % BUCKET_COUNT];
|
||||
if let Some(entry) = bucket.iter_mut().find(|entry| entry.name == name) {
|
||||
entry.value = value;
|
||||
return Ok(false);
|
||||
}
|
||||
self.insert(Intent { name, value })
|
||||
}
|
||||
|
||||
/// Remove one exact six-word key using the same bucket and chain order as
|
||||
/// native 82BC1068. Removing an entry leaves the relative order of the
|
||||
/// remaining chain untouched.
|
||||
pub fn remove(&mut self, name: IntentName) -> bool {
|
||||
let hash = name
|
||||
.0
|
||||
.iter()
|
||||
.fold(0u32, |sum, word| sum.wrapping_add(*word));
|
||||
let bucket = &mut self.buckets[hash as usize % BUCKET_COUNT];
|
||||
let Some(index) = bucket.iter().position(|entry| entry.name == name) else {
|
||||
return false;
|
||||
};
|
||||
bucket.remove(index);
|
||||
self.len -= 1;
|
||||
true
|
||||
}
|
||||
|
||||
/// Apply the graph host's existing mutation protocol at one decoded key.
|
||||
/// `IntentName` is deliberately supplied by the graph decoder: this
|
||||
/// module does not invent a string-to-six-word encoding.
|
||||
pub fn apply(
|
||||
&mut self,
|
||||
name: IntentName,
|
||||
mutation: IntentMutation,
|
||||
) -> Result<bool, IntentCapacityExceeded> {
|
||||
match mutation {
|
||||
IntentMutation::None => Ok(false),
|
||||
IntentMutation::Remove => Ok(self.remove(name)),
|
||||
IntentMutation::Set(value) => self.set(name, value),
|
||||
}
|
||||
}
|
||||
|
||||
///82BC1B68 clears all chains but preserves bucket configuration.
|
||||
pub fn clear(&mut self) {
|
||||
for bucket in &mut self.buckets {
|
||||
bucket.clear();
|
||||
}
|
||||
self.len = 0;
|
||||
}
|
||||
|
||||
///825936FC..82593740: clear then range-insert, not a structural clone.
|
||||
/// Each colliding source chain therefore reverses in the destination.
|
||||
/// Canonical graph and packet are distinct owned maps. A caller binding a
|
||||
/// map to itself must retain it instead, matching the native identity guard.
|
||||
pub fn replace_from(&mut self, source: &Self) {
|
||||
self.clear();
|
||||
for &entry in source.entries() {
|
||||
// Both maps enforce the same64-entry bound and exact key uniqueness.
|
||||
self.insert(entry).expect("bounded intent map assignment");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests/intents.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,27 @@
|
||||
//! Native TU3 pose-output kernels. These do not evaluate clips or choose graphs.
|
||||
pub mod actor_packet;
|
||||
pub mod attributes;
|
||||
mod hierarchy;
|
||||
pub mod intents;
|
||||
pub mod motion_graph_packet;
|
||||
pub mod packet_reset;
|
||||
pub mod physics_packet;
|
||||
pub mod setup_physics;
|
||||
mod sqt;
|
||||
|
||||
pub use hierarchy::{compose_hierarchy, compose_hierarchy_in_place};
|
||||
pub use sqt::{Sqt, sqt_to_local, sqt_to_matrix};
|
||||
|
||||
/// Four consecutive native vectors, including all packed fourth lanes.
|
||||
/// Row/column conversion for a renderer belongs outside this module.
|
||||
pub type NativeMatrix = [[f32; 4]; 4];
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum PoseBufferError {
|
||||
CountOverflow,
|
||||
ShortInput,
|
||||
ShortOutput,
|
||||
ShortParents,
|
||||
InvalidCacheOffset,
|
||||
InvalidParent { bone: usize, parent: i32 },
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
//! Concrete SkaterMotionGraph::GetPhysUpdateData82595FA8 and copy82596008.
|
||||
//! Actor construction82590DC0 ->8258F488 installs base vtable823006D0;
|
||||
//! slot36 resolves82595FA8. Unlike the reviewed Skate2 counterpart, this
|
||||
//! Skate3 body does not reset trick, gesture or wipeout source state afterward.
|
||||
|
||||
/// Native name storage at the data boundary; no addresses or host pointers.
|
||||
/// Exact text/hash member interpretation is a separate string-format contract.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct TrickIdentifier(pub [u32; 6]);
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct TrickData {
|
||||
pub identifiers: [TrickIdentifier; 2],
|
||||
/// Native trick record+48. Its producer/enum meaning is not recovered here.
|
||||
pub word48: u32,
|
||||
/// Native record+52/+56; copied by lfs/stfs.
|
||||
pub scalars52_56: [f32; 2],
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct GestureData {
|
||||
pub gesture: u32,
|
||||
/// The native body copies the complete second word, including unused bytes.
|
||||
pub flag_bytes: [u8; 4],
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct MotionGraphPacket {
|
||||
/// Source MG+5924 -> physics packet+10392,60 bytes.
|
||||
pub trick: TrickData,
|
||||
/// MG+5984/+5988 -> packet+10452/+10456.
|
||||
pub gesture: GestureData,
|
||||
/// MG+5992/+5996 -> packet+10460/+10464; complete words are copied.
|
||||
pub wipeout_gesture: [f32; 2],
|
||||
}
|
||||
|
||||
/// Complete concrete publication. Timestep is unused by82595FA8; no source
|
||||
/// request is consumed, and repeated publication copies the same state.
|
||||
pub fn publish(source: &MotionGraphPacket, destination: &mut MotionGraphPacket) {
|
||||
destination.trick = source.trick;
|
||||
destination.gesture.gesture = source.gesture.gesture;
|
||||
destination.gesture.flag_bytes = source.gesture.flag_bytes;
|
||||
destination.wipeout_gesture[0] = source.wipeout_gesture[0];
|
||||
destination.wipeout_gesture[1] = source.wipeout_gesture[1];
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
//! AnimOutPhysIn::Reset, TU3 82590028.
|
||||
//! Reset is selective: it preserves storage, count, intents, attributes and all
|
||||
//! fields not listed below. Actor::SetUpPhysics owns the later list replacement.
|
||||
use super::{NativeMatrix, physics_packet::PhysicsPosePacket};
|
||||
use crate::animation::commands::buffers::BufferError;
|
||||
|
||||
/// Additional fields written by Reset, kept separate from the existing pose
|
||||
/// publication view. Unknown flags retain their verified producer/offset names.
|
||||
pub struct AdditionalResetFields {
|
||||
/// 10388; 10912/10916.
|
||||
pub compression: f32,
|
||||
pub foot_ik_influence: [f32; 2],
|
||||
/// 10369. Position itself is preserved by Reset.
|
||||
pub next_step_position_valid: bool,
|
||||
/// 10375/10376; overwritten from these Actor bits during SetUpPhysics.
|
||||
pub actor_flag_1904_bit23: bool,
|
||||
pub actor_flag_1908_bit2: bool,
|
||||
/// 10496,10688,10689: active markers only; payloads are not cleared here.
|
||||
pub external_impulse_active: bool,
|
||||
pub external_physics_input_active: bool,
|
||||
pub externally_controlled: bool,
|
||||
/// 10786/10787/10796.
|
||||
pub prevent_manual_respawn: bool,
|
||||
pub ignore_respawn_reset_button: u8,
|
||||
pub force_braking: bool,
|
||||
/// 10788/10792. Named source fields, same target scalar reset/caller roles.
|
||||
pub truck_tightness: f32,
|
||||
pub wheel_hardness: f32,
|
||||
/// Six complete native vectors at10816,10832,10848,10864,10880,10896.
|
||||
/// Individual target semantics remain unassigned; all fourth lanes reset.
|
||||
pub auxiliary_vectors: [[f32; 4]; 6],
|
||||
/// 10928, later overwritten by the Actor's selected physics settings.
|
||||
pub requested_physics_mode: u32,
|
||||
}
|
||||
|
||||
/// Native packed basis, NOT a conventional homogeneous 4x4 identity.
|
||||
/// Three static vectors at82139A10/20/30 plus an all-zero vector produced
|
||||
/// inside Reset. All four fourth lanes, including translation W, are zero.
|
||||
pub const RESET_POSE: NativeMatrix = [
|
||||
[1.0, 0.0, 0.0, 0.0],
|
||||
[0.0, 1.0, 0.0, 0.0],
|
||||
[0.0, 0.0, 1.0, 0.0],
|
||||
[0.0; 4],
|
||||
];
|
||||
|
||||
/// Complete field coverage for82590028 on valid packet allocations. Host-side
|
||||
/// extent checks precede mutation; native assumes allocations match the count.
|
||||
/// Existing vector lengths/capacities and bones past packet.bone_count remain.
|
||||
pub fn reset(
|
||||
packet: &mut PhysicsPosePacket,
|
||||
fields: &mut AdditionalResetFields,
|
||||
) -> Result<(), BufferError> {
|
||||
// Both native loop comparisons are signed. A high-bit count skips the
|
||||
// matrix loop while scalar/flag resets still run.
|
||||
let count = (packet.bone_count as i32).max(0) as usize;
|
||||
if packet.hierarchy.len() < count || packet.local.len() < count {
|
||||
return Err(BufferError::RangeOutsideAllocation);
|
||||
}
|
||||
packet.timestep = f32::from_bits(0x3c88_8889);
|
||||
fields.external_impulse_active = false;
|
||||
fields.compression = 0.5;
|
||||
fields.foot_ik_influence[0] = 0.0;
|
||||
fields.external_physics_input_active = false;
|
||||
fields.foot_ik_influence[1] = 0.0;
|
||||
fields.externally_controlled = false;
|
||||
fields.truck_tightness = 0.0;
|
||||
packet.mirrored = false;
|
||||
packet.riding_switch = false;
|
||||
packet.riding_fakie = false;
|
||||
packet.weight_forwards = false;
|
||||
packet.regular_stance = false;
|
||||
fields.prevent_manual_respawn = false;
|
||||
fields.ignore_respawn_reset_button = 0;
|
||||
fields.force_braking = false;
|
||||
packet.air_dismount_revert_frames = 0;
|
||||
packet.board_flipped = false;
|
||||
fields.next_step_position_valid = false;
|
||||
fields.actor_flag_1904_bit23 = false;
|
||||
packet.flags &= 0x013f_ffff;
|
||||
fields.actor_flag_1908_bit2 = false;
|
||||
packet.foot_surface_ids[0] = 0;
|
||||
fields.wheel_hardness = 0.0;
|
||||
packet.foot_surface_ids[1] = 0;
|
||||
fields.auxiliary_vectors[0] = [0.0; 4];
|
||||
fields.requested_physics_mode = 1;
|
||||
for vector in &mut fields.auxiliary_vectors[1..] {
|
||||
*vector = [0.0; 4];
|
||||
}
|
||||
for bone in 0..count {
|
||||
// Native completes local first, then hierarchy, for each bone.
|
||||
packet.local[bone] = RESET_POSE;
|
||||
packet.hierarchy[bone] = RESET_POSE;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
//! Concrete SkaterAnim::GetPhysUpdateData, TU3 82B985E8.
|
||||
//! This owns only fields written by that function, not the full AnimOutPhysIn
|
||||
//! reset, motion-graph merge, attribute dispatch, or physical force consumers.
|
||||
use super::NativeMatrix;
|
||||
use crate::animation::commands::{
|
||||
batch::CompletedSetData,
|
||||
buffers::{BoneSlice, BufferError},
|
||||
};
|
||||
|
||||
/// Optional object at SkaterAnim+15104. The original class name is unresolved.
|
||||
pub struct AnimationSignal {
|
||||
pub name_hash: u32,
|
||||
pub active: u8,
|
||||
}
|
||||
|
||||
/// Fields read or consumed from the concrete native SkaterAnim. Other object
|
||||
/// fields remain with their owners. The two unknown requests are NOT push flags.
|
||||
pub struct SkaterPublicationState {
|
||||
/// Native packed flags+15180 bits31,30,29,28 respectively.
|
||||
pub orientation_bit31: bool,
|
||||
pub mirrored: bool,
|
||||
pub riding_fakie: bool,
|
||||
pub weight_on_nose: bool,
|
||||
/// +15196 and +15188 are distinct enums. Only equality is tested here.
|
||||
pub relative_stance: i32,
|
||||
pub natural_stance: i32,
|
||||
/// Native flags bits16 and15, consumed at publication.
|
||||
pub request_bit16: bool,
|
||||
pub request_bit15: bool,
|
||||
/// Native flags bit20 and persistent count+15184.
|
||||
pub air_dismount_revert_requested: bool,
|
||||
pub air_dismount_revert_frames: i32,
|
||||
pub signal: Option<AnimationSignal>,
|
||||
}
|
||||
|
||||
pub struct ActorPoseBuffers {
|
||||
/// Actor-owned hierarchy-global matrices, full SkaterAnim+13460.
|
||||
pub hierarchy: BoneSlice<NativeMatrix>,
|
||||
/// Actor-owned local matrices, full SkaterAnim+13468.
|
||||
pub local: BoneSlice<NativeMatrix>,
|
||||
}
|
||||
|
||||
/// Physics-packet-owned arrays are separate from the animation allocations.
|
||||
/// Count is packet+10380, which native publication reads without rewriting.
|
||||
/// Unlisted packet fields must be preserved by the surrounding packet owner.
|
||||
pub struct PhysicsPosePacket {
|
||||
pub bone_count: u32,
|
||||
pub hierarchy: Vec<NativeMatrix>,
|
||||
pub local: Vec<NativeMatrix>,
|
||||
pub timestep: f32,
|
||||
pub foot_surface_ids: [u32; 2],
|
||||
pub flags: u32,
|
||||
pub board_flipped: bool,
|
||||
pub mirrored: bool,
|
||||
pub riding_switch: bool,
|
||||
pub riding_fakie: bool,
|
||||
pub weight_forwards: bool,
|
||||
pub regular_stance: bool,
|
||||
pub air_dismount_revert_frames: i32,
|
||||
}
|
||||
|
||||
/// Publish after the source worker completes. Before modifying state, this safe
|
||||
/// adapter rejects invalid/uninitialized ranges; native assumes valid storage.
|
||||
/// `signal_name` is the current string selected by native global830301E4 (the
|
||||
/// inspected TU3 image selects "signup"). Supply it from verified startup data.
|
||||
/// Concrete getters 82B970D8/82B97100/82B97128 are pure field reads here;
|
||||
/// polymorphic replacement getters are outside this concrete SkaterAnim port.
|
||||
pub fn publish(
|
||||
state: &mut SkaterPublicationState,
|
||||
completed: &CompletedSetData,
|
||||
poses: &ActorPoseBuffers,
|
||||
packet: &mut PhysicsPosePacket,
|
||||
timestep: f64,
|
||||
signal_name: &[u8],
|
||||
) -> Result<i32, BufferError> {
|
||||
let count = packet.bone_count as usize;
|
||||
if packet.hierarchy.len() < count || packet.local.len() < count {
|
||||
return Err(BufferError::RangeOutsideAllocation);
|
||||
}
|
||||
let hierarchy = completed.buffers().matrices.read(poses.hierarchy, count)?;
|
||||
let local = completed.buffers().matrices.read(poses.local, count)?;
|
||||
|
||||
publish_evaluated(state, &hierarchy, &local, packet, timestep, signal_name)
|
||||
}
|
||||
|
||||
/// Same publication with host-owned evaluated matrices. Completing animation
|
||||
/// evaluation before this call preserves the source dependency without
|
||||
/// requiring the original job/cache allocation system in the game.
|
||||
pub fn publish_evaluated(
|
||||
state: &mut SkaterPublicationState,
|
||||
hierarchy: &[NativeMatrix],
|
||||
local: &[NativeMatrix],
|
||||
packet: &mut PhysicsPosePacket,
|
||||
timestep: f64,
|
||||
signal_name: &[u8],
|
||||
) -> Result<i32, BufferError> {
|
||||
let count=packet.bone_count as usize;
|
||||
if hierarchy.len()<count || local.len()<count || packet.hierarchy.len()<count || packet.local.len()<count {
|
||||
return Err(BufferError::RangeOutsideAllocation);
|
||||
}
|
||||
|
||||
packet.timestep = timestep as f32;
|
||||
packet.foot_surface_ids = [0; 2];
|
||||
replace_flag(&mut packet.flags, 27, state.request_bit16);
|
||||
state.request_bit16 = false;
|
||||
replace_flag(&mut packet.flags, 23, state.request_bit15);
|
||||
state.request_bit15 = false;
|
||||
packet.flags &= !((1 << 25) | (1 << 26));
|
||||
if let Some(signal) = &mut state.signal {
|
||||
signal.name_hash = signal_hash(signal_name);
|
||||
signal.active = 0;
|
||||
}
|
||||
packet.hierarchy[..count].copy_from_slice(&hierarchy[..count]);
|
||||
packet.local[..count].copy_from_slice(&local[..count]);
|
||||
packet.board_flipped = state.orientation_bit31 ^ state.mirrored;
|
||||
packet.mirrored = state.mirrored;
|
||||
packet.riding_switch = state.relative_stance == 1;
|
||||
packet.riding_fakie = state.riding_fakie;
|
||||
packet.weight_forwards = if state.riding_fakie {
|
||||
!state.weight_on_nose
|
||||
} else {
|
||||
state.weight_on_nose
|
||||
};
|
||||
packet.regular_stance = state.natural_stance == 0;
|
||||
replace_flag(&mut packet.flags, 28, state.air_dismount_revert_requested);
|
||||
state.air_dismount_revert_requested = false;
|
||||
packet.air_dismount_revert_frames = state.air_dismount_revert_frames;
|
||||
Ok(state.air_dismount_revert_frames)
|
||||
}
|
||||
|
||||
fn replace_flag(flags: &mut u32, bit: u32, value: bool) {
|
||||
*flags = (*flags & !(1 << bit)) | (u32::from(value) << bit);
|
||||
}
|
||||
|
||||
/// 8296EA10: NUL-terminated, signed-byte name hashing. This deliberately does
|
||||
/// not substitute a library hash or unsigned-byte variant.
|
||||
pub fn signal_hash(name: &[u8]) -> u32 {
|
||||
let mut hash = 0_u32;
|
||||
for &byte in name.iter().take_while(|&&byte| byte != 0) {
|
||||
hash = hash
|
||||
.wrapping_shl(4)
|
||||
.wrapping_add((byte as i8 as i32) as u32);
|
||||
let high = hash & 0xf000_0000;
|
||||
if high != 0 {
|
||||
hash ^= (high >> 23) ^ high;
|
||||
}
|
||||
}
|
||||
hash
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
//! Source-bound packet composition for concrete Actor::SetUpPhysics82593640.
|
||||
//! The caller supplies the runtime services and completed materialized poses.
|
||||
//! This is not the complete Actor update or Skeleton attribute interpreter.
|
||||
use super::{
|
||||
actor_packet::{self, ActorPacketFields, ActorPublicationEnvironment, ActorPublicationState},
|
||||
attributes::{AnimationAttribute, MotionGraphAttribute, PacketAttributes},
|
||||
intents::IntentMap,
|
||||
motion_graph_packet::{self, MotionGraphPacket},
|
||||
packet_reset::{self, AdditionalResetFields},
|
||||
physics_packet::{self, ActorPoseBuffers, PhysicsPosePacket, SkaterPublicationState},
|
||||
};
|
||||
use crate::animation::commands::{batch::CompletedSetData, buffers::BufferError};
|
||||
|
||||
/// Views cover fields touched by this concrete publication chain. Packet fields
|
||||
/// only used by later Skeleton consumers are not fabricated here.
|
||||
pub struct PhysicsInputPacket {
|
||||
pub pose: PhysicsPosePacket,
|
||||
pub reset: AdditionalResetFields,
|
||||
pub motion_graph: MotionGraphPacket,
|
||||
pub intents: IntentMap,
|
||||
pub attributes: PacketAttributes,
|
||||
pub actor: ActorPacketFields,
|
||||
}
|
||||
|
||||
pub struct AnimationPublication<'a> {
|
||||
pub state: &'a mut SkaterPublicationState,
|
||||
pub completed: &'a CompletedSetData,
|
||||
pub poses: &'a ActorPoseBuffers,
|
||||
pub signal_name: &'a [u8],
|
||||
pub tree_attributes: &'a [AnimationAttribute],
|
||||
}
|
||||
|
||||
pub enum IntentPublication<'a> {
|
||||
MotionGraph(&'a IntentMap),
|
||||
/// Exact same-container branch825936FC; no clear/reinsert occurs.
|
||||
RetainPacket,
|
||||
}
|
||||
|
||||
pub struct MotionGraphPublication<'a> {
|
||||
pub state: &'a MotionGraphPacket,
|
||||
pub intents: IntentPublication<'a>,
|
||||
pub attributes: &'a [MotionGraphAttribute],
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum PublicationError<E> {
|
||||
Pose(BufferError),
|
||||
RuntimeService(E),
|
||||
}
|
||||
|
||||
///82DB3C48 only stores a borrowed packet pointer at physical+1808. This safe
|
||||
/// binding keeps the entire owned packet alive and prevents its next mutation
|
||||
/// while a consumer reads it. It does not execute the downstream physics step.
|
||||
pub struct PhysicsPacketBinding<'a> {
|
||||
packet: &'a PhysicsInputPacket,
|
||||
}
|
||||
impl<'a> PhysicsPacketBinding<'a> {
|
||||
pub fn packet(&self) -> &'a PhysicsInputPacket {
|
||||
self.packet
|
||||
}
|
||||
}
|
||||
|
||||
/// Concrete canonical virtual144 is a no-op. Remaining phases execute in the
|
||||
/// native order: reset, SkaterAnim, MG, intents, MG/tree attrs, actor suffix,
|
||||
/// then packet binding. Failure preserves writes already made and returns no
|
||||
/// binding; the caller must not consume the partially published packet.
|
||||
pub fn compose<'a, E: ActorPublicationEnvironment>(
|
||||
packet: &'a mut PhysicsInputPacket,
|
||||
animation: AnimationPublication<'_>,
|
||||
graph: MotionGraphPublication<'_>,
|
||||
actor: &mut ActorPublicationState,
|
||||
environment: &mut E,
|
||||
) -> Result<PhysicsPacketBinding<'a>, PublicationError<E::Error>> {
|
||||
packet_reset::reset(&mut packet.pose, &mut packet.reset).map_err(PublicationError::Pose)?;
|
||||
physics_packet::publish(
|
||||
animation.state,
|
||||
animation.completed,
|
||||
animation.poses,
|
||||
&mut packet.pose,
|
||||
f64::from(f32::from_bits(0x3c88_8889)),
|
||||
animation.signal_name,
|
||||
)
|
||||
.map_err(PublicationError::Pose)?;
|
||||
motion_graph_packet::publish(graph.state, &mut packet.motion_graph);
|
||||
if let IntentPublication::MotionGraph(source) = graph.intents {
|
||||
packet.intents.replace_from(source);
|
||||
}
|
||||
packet
|
||||
.attributes
|
||||
.replace_from(graph.attributes, animation.tree_attributes);
|
||||
actor_packet::publish(
|
||||
actor,
|
||||
&mut packet.pose,
|
||||
&mut packet.reset,
|
||||
&mut packet.actor,
|
||||
environment,
|
||||
)
|
||||
.map_err(PublicationError::RuntimeService)?;
|
||||
Ok(PhysicsPacketBinding { packet })
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
use super::{NativeMatrix, PoseBufferError};
|
||||
|
||||
/// TU3 48-byte SQT record: scale, quaternion (x,y,z,w), translation.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct Sqt {
|
||||
pub scale: [f32; 4],
|
||||
pub rotation: [f32; 4],
|
||||
pub translation: [f32; 4],
|
||||
}
|
||||
|
||||
/// Complete arithmetic/store behavior of TU3 0x828D3800.
|
||||
///
|
||||
/// The native SIMD loop writes four records at a time, including padding beyond
|
||||
/// `bone_count`. The native `dcbzl` additionally zeros the following cache line;
|
||||
/// `cache_offset` specifies the destination address modulo 128 (a multiple of 16).
|
||||
/// Destination must include that trailing storage. No normalization or
|
||||
/// bind-pose composition occurs. Unlike native speculative loads, this safe
|
||||
/// adapter does not read a further unused group beyond the padded inputs.
|
||||
pub fn sqt_to_local(
|
||||
bone_count: usize,
|
||||
cache_offset: u8,
|
||||
source: &[Sqt],
|
||||
destination: &mut [NativeMatrix],
|
||||
) -> Result<usize, PoseBufferError> {
|
||||
if cache_offset >= 128 || cache_offset % 16 != 0 {
|
||||
return Err(PoseBufferError::InvalidCacheOffset);
|
||||
}
|
||||
let padded = bone_count
|
||||
.checked_add(3)
|
||||
.ok_or(PoseBufferError::CountOverflow)?
|
||||
& !3;
|
||||
if source.len() < padded {
|
||||
return Err(PoseBufferError::ShortInput);
|
||||
}
|
||||
let trailing_words = if padded == 0 {
|
||||
0
|
||||
} else {
|
||||
(128 - usize::from(cache_offset)) / 4
|
||||
};
|
||||
let required = padded
|
||||
.checked_add(trailing_words.div_ceil(16))
|
||||
.ok_or(PoseBufferError::CountOverflow)?;
|
||||
if destination.len() < required {
|
||||
return Err(PoseBufferError::ShortOutput);
|
||||
}
|
||||
// Earlier zeroed lines are overwritten by later groups; only the final tail
|
||||
// remains visible. No typed input/output aliasing is allowed by this adapter.
|
||||
for word in destination[padded..required]
|
||||
.iter_mut()
|
||||
.flatten()
|
||||
.flatten()
|
||||
.take(trailing_words)
|
||||
{
|
||||
*word = 0.0;
|
||||
}
|
||||
for (input, output) in source[..padded].iter().zip(&mut destination[..padded]) {
|
||||
*output = sqt_to_matrix(*input);
|
||||
}
|
||||
Ok(padded)
|
||||
}
|
||||
|
||||
/// Per-bone numerical result828D3800. The game owns its vector allocations;
|
||||
/// cache-line padding and speculative DMA loads are not gameplay behavior.
|
||||
pub fn sqt_to_matrix(input: Sqt) -> NativeMatrix {
|
||||
let [x, y, z, w] = input.rotation;
|
||||
let [sx, sy, sz, _] = input.scale;
|
||||
// Separate products and sums reflect the native VMX instruction order.
|
||||
let xx = x * x;
|
||||
let yy = y * y;
|
||||
let zz = z * z;
|
||||
let xy = x * y;
|
||||
let wz = w * z;
|
||||
let xz = x * z;
|
||||
let wy = w * y;
|
||||
let yz = y * z;
|
||||
let wx = w * x;
|
||||
[
|
||||
[
|
||||
(-(yy + zz)).mul_add(2.0, 1.0) * sx,
|
||||
((xy + wz) * 2.0) * sx,
|
||||
((xz - wy) * 2.0) * sx,
|
||||
0.0,
|
||||
],
|
||||
[
|
||||
((xy - wz) * 2.0) * sy,
|
||||
(-(xx + zz)).mul_add(2.0, 1.0) * sy,
|
||||
((yz + wx) * 2.0) * sy,
|
||||
0.0,
|
||||
],
|
||||
[
|
||||
((xz + wy) * 2.0) * sz,
|
||||
((yz - wx) * 2.0) * sz,
|
||||
(-(xx + yy)).mul_add(2.0, 1.0) * sz,
|
||||
0.0,
|
||||
],
|
||||
[
|
||||
input.translation[0],
|
||||
input.translation[1],
|
||||
input.translation[2],
|
||||
1.0,
|
||||
],
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
use super::{BUCKET_COUNT, ENTRY_CAPACITY, Intent, IntentMap, IntentName};
|
||||
use crate::input::graph_intents::IntentMutation;
|
||||
|
||||
fn key(seed: u32) -> IntentName {
|
||||
IntentName([seed, 0, 0, 0, 0, 0])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_overwrites_and_remove_preserves_other_chain_entries() {
|
||||
let mut map = IntentMap::default();
|
||||
let first = key(1);
|
||||
let second = key(1 + BUCKET_COUNT as u32);
|
||||
assert!(
|
||||
map.insert(Intent {
|
||||
name: first,
|
||||
value: 1.0
|
||||
})
|
||||
.unwrap()
|
||||
);
|
||||
assert!(
|
||||
map.insert(Intent {
|
||||
name: second,
|
||||
value: 2.0
|
||||
})
|
||||
.unwrap()
|
||||
);
|
||||
assert!(!map.set(first, 7.0).unwrap());
|
||||
assert_eq!(
|
||||
map.entries()
|
||||
.find(|entry| entry.name == first)
|
||||
.unwrap()
|
||||
.value,
|
||||
7.0
|
||||
);
|
||||
assert!(map.remove(first));
|
||||
assert!(!map.remove(first));
|
||||
assert_eq!(map.len(), 1);
|
||||
assert_eq!(map.entries().next().unwrap().name, second);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_uses_existing_mutation_protocol_and_capacity() {
|
||||
let mut map = IntentMap::default();
|
||||
let name = key(42);
|
||||
assert!(map.apply(name, IntentMutation::Set(3.0)).unwrap());
|
||||
assert!(!map.apply(name, IntentMutation::None).unwrap());
|
||||
assert!(!map.apply(name, IntentMutation::Set(4.0)).unwrap());
|
||||
assert_eq!(map.entries().next().unwrap().value, 4.0);
|
||||
assert!(map.apply(name, IntentMutation::Remove).unwrap());
|
||||
assert_eq!(map.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_rejects_only_after_native_fixed_capacity() {
|
||||
let mut map = IntentMap::default();
|
||||
for seed in 0..ENTRY_CAPACITY as u32 {
|
||||
assert!(
|
||||
map.insert(Intent {
|
||||
name: key(seed),
|
||||
value: seed as f32
|
||||
})
|
||||
.is_ok()
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
map.insert(Intent {
|
||||
name: key(ENTRY_CAPACITY as u32),
|
||||
value: 0.0
|
||||
})
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
//! Andale PhaseBlend, TU3 82D24C40..82D25808; construction 82D1B67C.
|
||||
use super::{clip_clock::AdvanceResult, output::attributes::{AnimationAttribute, AttributeName, MotionGraphAttribute}, playback_attributes, playback_parameters::SettableAttribute, playback_tree::{Evaluation, PlaybackTree, PoseCommand}};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PhaseBlend {
|
||||
pub parameter: AttributeName,
|
||||
pub children: Vec<PlaybackTree>,
|
||||
order: Vec<usize>,
|
||||
values: Vec<f32>,
|
||||
left: usize,
|
||||
right: usize,
|
||||
weight: f32,
|
||||
parameter_value: f32,
|
||||
normalized: bool,
|
||||
cull_threshold: f32,
|
||||
pub length: f32,
|
||||
pub time: f32,
|
||||
}
|
||||
impl PhaseBlend {
|
||||
pub fn new(parameter: AttributeName, children: Vec<PlaybackTree>) -> Result<Self,String> {
|
||||
if children.len() < 2 { return Err("PhaseBlend requires at least two authored children".into()); }
|
||||
let count = children.len();
|
||||
Ok(Self { parameter, length: children[0].length(), children, order: (0..count).collect(), values: vec![0.0;count], left: 0, right: 1, weight: 0.0, parameter_value: f32::MAX, normalized: true, cull_threshold: 0.0, time: 0.0 })
|
||||
}
|
||||
pub fn set_time(&mut self, time: f32) {
|
||||
let phase = time/self.length;
|
||||
let left_time = self.children[self.left].length()*phase;
|
||||
let right_time = self.children[self.right].length()*phase;
|
||||
self.children[self.left].set_time(left_time);
|
||||
self.children[self.right].set_time(right_time);
|
||||
self.time = time;
|
||||
}
|
||||
fn phase(&self) -> f32 { self.time/if -self.length >= 0.0 {1.0} else {self.length} }
|
||||
fn update_length(&mut self, phase: f32) {
|
||||
self.length = self.children[self.right].length().mul_add(self.weight,self.children[self.left].length()*(1.0-self.weight));
|
||||
self.set_time(self.length*phase);
|
||||
}
|
||||
pub fn set_speed(&mut self, speed: f32) {
|
||||
let phase = self.phase();
|
||||
for &index in &self.order { self.children[index].set_speed(speed); }
|
||||
self.update_length(phase);
|
||||
}
|
||||
pub fn advance(&mut self, dt: f32, phase: f32, property: &mut AdvanceResult) {
|
||||
let left_length = self.children[self.left].length();
|
||||
let right_length = self.children[self.right].length();
|
||||
let fraction = dt/self.length;
|
||||
self.children[self.left].advance(fraction*left_length,phase,property);
|
||||
let mut discarded = AdvanceResult { crossed_end:false,overshoot:-1.0,remaining_before_wrap:-1.0 };
|
||||
self.children[self.right].advance(fraction*right_length,phase,&mut discarded);
|
||||
let inverse_left = 1.0/left_length;
|
||||
self.time = (self.children[self.left].time()*inverse_left)*self.length;
|
||||
property.remaining_before_wrap = (property.remaining_before_wrap*inverse_left)*self.length;
|
||||
property.overshoot = (inverse_left*self.length)*property.overshoot;
|
||||
}
|
||||
pub fn set_attributes(&mut self, attributes: &[SettableAttribute]) -> Result<bool,String> {
|
||||
let mut changed = false;
|
||||
for &index in &self.order { changed |= self.children[index].set_attributes(attributes)?; }
|
||||
if let Some(attribute) = attributes.iter().find(|a| a.name == self.parameter) {
|
||||
if attribute.normalized != self.normalized || !((attribute.value-self.parameter_value).abs() < f32::from_bits(0x38d1b717)) {
|
||||
self.parameter_value = attribute.value;
|
||||
self.normalized = attribute.normalized;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if !changed { return Ok(false); }
|
||||
for &index in &self.order {
|
||||
let mut attribute = MotionGraphAttribute {name:self.parameter,value:0.0}.to_animation();
|
||||
// Native deliberately ignores success; partial output from nested
|
||||
// GetAttribute remains visible, and a complete miss retains zero.
|
||||
self.children[index].query_attribute(self.parameter,15,&mut attribute)?;
|
||||
self.values[index] = f32::from_bits(attribute.payload.0[0].ok_or("Uninitialized PhaseBlend parameter")?);
|
||||
}
|
||||
for end in (1..self.order.len()).rev() {
|
||||
for i in 0..end {
|
||||
if self.values[self.order[i+1]] < self.values[self.order[i]] { self.order.swap(i,i+1); }
|
||||
}
|
||||
}
|
||||
let minimum = self.values[self.order[0]];
|
||||
let maximum = self.values[*self.order.last().unwrap()];
|
||||
let target = if self.normalized {(maximum-minimum).mul_add(self.parameter_value,minimum)} else {self.parameter_value};
|
||||
let mut low = 0;
|
||||
let mut count = self.order.len();
|
||||
while count != 0 {
|
||||
let step = count/2;
|
||||
let middle = low+step;
|
||||
if self.values[self.order[middle]] < target { low=middle+1;count-=step+1; } else {count=step;}
|
||||
}
|
||||
let upper = low.clamp(1,self.order.len()-1);
|
||||
let left = self.order[upper-1];
|
||||
let right = self.order[upper];
|
||||
let lower_value = self.values[left];
|
||||
let upper_value = self.values[right];
|
||||
let delta = upper_value-lower_value;
|
||||
let lower = if lower_value-target >= 0.0 {lower_value} else {target};
|
||||
let clamped = if upper_value-lower >= 0.0 {lower} else {upper_value};
|
||||
let weight = (clamped-lower_value)/if delta == 0.0 {1.0} else {delta};
|
||||
let lower = if -weight >= 0.0 {0.0} else {weight};
|
||||
self.weight = if 1.0-lower >= 0.0 {lower} else {1.0};
|
||||
let phase = self.phase();
|
||||
self.left=left;self.right=right;
|
||||
self.update_length(phase);
|
||||
Ok(true)
|
||||
}
|
||||
pub fn attributes(&self, mask:u32) -> Result<Vec<AnimationAttribute>,String> {
|
||||
if self.weight < self.cull_threshold {return self.children[self.left].attributes(mask);}
|
||||
if self.weight > 1.0-self.cull_threshold {return self.children[self.right].attributes(mask);}
|
||||
playback_attributes::intersection(self.children[self.left].attributes(mask)?,&self.children[self.right].attributes(31)?,self.weight)
|
||||
}
|
||||
pub fn query_attribute(&self, name:AttributeName,mask:u32,output:&mut AnimationAttribute) -> Result<bool,String> {
|
||||
if self.weight < self.cull_threshold {return self.children[self.left].query_attribute(name,mask,output);}
|
||||
if self.weight > 1.0-self.cull_threshold {return self.children[self.right].query_attribute(name,mask,output);}
|
||||
let left = self.children[self.left].query_attribute(name,mask,output)?;
|
||||
let mut right = MotionGraphAttribute {name,value:0.0}.to_animation();
|
||||
let found_right = self.children[self.right].query_attribute(name,31,&mut right)?;
|
||||
if left && found_right {playback_attributes::blend(output,&right,self.weight)?;Ok(true)} else {Ok(false)}
|
||||
}
|
||||
pub fn evaluate(&mut self, parameters:Evaluation,enabled:bool,output:&mut Vec<PoseCommand>) -> Result<bool,String> {
|
||||
self.cull_threshold = parameters.cull_threshold;
|
||||
let c = parameters.cull_threshold;
|
||||
let lower = if c-self.weight >= 0.0 {c} else {self.weight};
|
||||
let upper = 1.0-c;
|
||||
let clamped = if upper-lower >= 0.0 {lower} else {upper};
|
||||
let t = (clamped-c)/(-c).mul_add(2.0,1.0);
|
||||
let square = t*t;
|
||||
let weight = if !(t>0.5) && self.left == self.order[0] {(-t).mul_add(4.0,4.0)*square}
|
||||
else if t>0.5 && self.right == *self.order.last().unwrap() {let four=t*4.0;(8.0-four).mul_add(square,-four)+1.0}
|
||||
else {self.weight};
|
||||
let left=self.children[self.left].evaluate(parameters,enabled && weight<1.0,output)?;
|
||||
let right=self.children[self.right].evaluate(parameters,enabled && weight>0.0,output)?;
|
||||
if left && right {output.push(PoseCommand::Blend {weight});}
|
||||
Ok(left || right)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
//! Physical publications consumed by riding animation, TU3 82DB6EC0.
|
||||
//!
|
||||
//! This receives the actual board, Pumping, SpeedWobble and Reckoning results.
|
||||
//! It does not reconstruct the skater frame from the deck or rendered pose.
|
||||
use crate::{
|
||||
animation::{crouching, ground_acceleration},
|
||||
input::{set_turning, turn_conditioner},
|
||||
math::Vector3,
|
||||
physics::{board_motion_output::BoardMotionOutput, native_arithmetic},
|
||||
riding::{pumping::state::PumpingState, speed_wobble::SpeedWobbleState},
|
||||
};
|
||||
|
||||
/// The completed Reckoning owner's output required by animation publication.
|
||||
/// There is intentionally no default: these are distinct physical frames.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct ReckoningFeedback {
|
||||
/// PhysOutSystemReckoning+64.
|
||||
pub system_position: Vector3,
|
||||
/// Reckoning+1152 -> PhysOutSystemReckoning+96 at82DB7068.
|
||||
pub system_up: Vector3,
|
||||
/// PhysOutDeck+48.
|
||||
pub board_position: Vector3,
|
||||
/// Reckoning+1576, before the Processed2468 bit20 sign correction.
|
||||
pub target_lean_angle: f32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct ControlFeedback {
|
||||
/// The complete ProcessedPhysIn+2468 flag word.
|
||||
pub processed_flags: u32,
|
||||
/// ProcessedPhysIn+2676 -> PhysOutGround+264.
|
||||
pub turn: f32,
|
||||
/// AnimOutPhysIn+10370 -> PhysOutAnimation+156.
|
||||
pub animation_mirrored: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct PhysicalFeedback {
|
||||
pub turning: set_turning::Physical,
|
||||
pub crouching: crouching::Physical,
|
||||
pub pumping_acceleration: f32,
|
||||
/// PhysOutAnimation112, conditioned actual deck acceleration82DF0508.
|
||||
pub ground_acceleration: [f32; 4],
|
||||
/// Original IsBumped82BA7310 evaluated with anim_motion/bumps.
|
||||
pub bumped: bool,
|
||||
/// Complete PhysOutAnimation32..60, retained for other native consumers.
|
||||
pub conditioned_turn: turn_conditioner::Output,
|
||||
}
|
||||
|
||||
/// Postphysics publication followed by the animation turn conditioner82DEFF38.
|
||||
/// Stateful filtering happens once per conditioner update, without a dt factor.
|
||||
pub fn publish(
|
||||
state: &mut turn_conditioner::State,
|
||||
settings: &turn_conditioner::Settings,
|
||||
motion: &BoardMotionOutput,
|
||||
pumping: &PumpingState,
|
||||
wobble: &SpeedWobbleState,
|
||||
reckoning: ReckoningFeedback,
|
||||
controls: ControlFeedback,
|
||||
acceleration: ground_acceleration::Output,
|
||||
) -> PhysicalFeedback {
|
||||
let lean = if controls.processed_flags & 0x0010_0000 != 0 {
|
||||
reckoning.target_lean_angle
|
||||
} else {
|
||||
-reckoning.target_lean_angle
|
||||
};
|
||||
let turn = turn_conditioner::update(
|
||||
state,
|
||||
turn_conditioner::Input {
|
||||
body_160: motion.speed,
|
||||
// Skateboard::FillPhysOut82C03230..3238 reads Wobble+36.
|
||||
body_176: f32::from_bits(wobble.0[5]),
|
||||
bundle_36_field_160: lean,
|
||||
bundle_32_field_264: controls.turn,
|
||||
animation_152: u8::from(controls.processed_flags & 0x8000_0000 != 0),
|
||||
animation_156: u8::from(controls.animation_mirrored),
|
||||
},
|
||||
settings,
|
||||
);
|
||||
let system = vector(reckoning.system_position);
|
||||
let board = vector(reckoning.board_position);
|
||||
//82DB7A38..7A54: vsubfp followed by vmsum3fp. The isolated native
|
||||
// arithmetic retains its documented host approximation boundary.
|
||||
let difference = core::array::from_fn(|lane| {
|
||||
flush_subnormal(flush_subnormal(system[lane]) - flush_subnormal(board[lane]))
|
||||
});
|
||||
let height = native_arithmetic::dot3(difference, vector(reckoning.system_up));
|
||||
PhysicalFeedback {
|
||||
turning: set_turning::Physical {
|
||||
field_32: turn[0],
|
||||
field_36: turn[1],
|
||||
field_52: turn[5],
|
||||
field_56: turn[6],
|
||||
field_60: turn[7],
|
||||
body_168: motion.forward_speed,
|
||||
},
|
||||
crouching: crouching::Physical {
|
||||
body_84: motion.linear_velocity.y,
|
||||
body_164: motion.ground_speed,
|
||||
//82C02CF0 copies Pumping+44, named mPumpPotential in S2 FillPhysOut.
|
||||
body_188: pumping.pumping,
|
||||
force_516: pumping.absorption,
|
||||
ground_force_520: pumping.ground_normal_absorption,
|
||||
minimum_crouch_528: pumping.minimum_crouch,
|
||||
deck_angle_532: pumping.deck_angle_absorption,
|
||||
animation_height_72: height,
|
||||
},
|
||||
pumping_acceleration: pumping.pump_acceleration,
|
||||
ground_acceleration: acceleration.acceleration,
|
||||
bumped: acceleration.bumped,
|
||||
conditioned_turn: turn,
|
||||
}
|
||||
}
|
||||
|
||||
fn vector(value: Vector3) -> [f32; 4] {
|
||||
[value.x, value.y, value.z, 0.0]
|
||||
}
|
||||
|
||||
fn flush_subnormal(value: f32) -> f32 {
|
||||
if value.to_bits() & 0x7f80_0000 == 0 {
|
||||
f32::from_bits(value.to_bits() & 0x8000_0000)
|
||||
} else {
|
||||
value
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
//! MotionGraph PlayAnimation ctor82BB4B38, Begin82BB5188, Update82BB5670.
|
||||
//! End is the verified no-op82B61BB8; leaving a graph state does not stop a tree.
|
||||
use super::{
|
||||
output::attributes::AttributeName,
|
||||
playback_parameters::{AttributeSink, ParameterInputs, PlaybackParameter},
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct TransitionSettings {
|
||||
///1 play,2 blend,3 channelblend,4 sequence. Zero means no graph override.
|
||||
pub kind: u32,
|
||||
pub seconds: f32,
|
||||
pub under: u32,
|
||||
///0 default,1 current frame,2 match phase,3 match frame.
|
||||
pub matching: u32,
|
||||
pub use_channels_from_weights: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct PlayAnimation {
|
||||
pub animation: String,
|
||||
pub switch_animation: Option<String>,
|
||||
pub mirror_animation: Option<String>,
|
||||
pub no_board_animation: Option<String>,
|
||||
pub playback_speed: f32,
|
||||
pub apply_posture: bool,
|
||||
pub transition: TransitionSettings,
|
||||
pub parameters: Vec<PlaybackParameter>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct PlaybackRequest {
|
||||
pub animation: String,
|
||||
pub speed: f32,
|
||||
pub start_time: f32,
|
||||
pub transition: TransitionSettings,
|
||||
}
|
||||
|
||||
/// Values published by the actual skater-animation/physical interfaces. The
|
||||
/// no-board bit is PhysOut+72's byte311, not a contact-derived guess.
|
||||
pub struct PlaybackContext {
|
||||
pub is_switch: Option<bool>,
|
||||
pub is_mirrored: Option<bool>,
|
||||
pub board_available: Option<bool>,
|
||||
pub pro_skater: AttributeName,
|
||||
pub transition_override: Option<TransitionSettings>,
|
||||
}
|
||||
|
||||
pub trait PlaybackService: ParameterInputs + AttributeSink {
|
||||
fn set_construction_value(&mut self, name: AttributeName, value: AttributeName);
|
||||
fn set_posture_enabled(&mut self, enabled: bool);
|
||||
/// Return false only when the native controller's Play/Blend/Sequence
|
||||
/// request fails. Unimplemented evaluation is an error, never false.
|
||||
fn play(&mut self, request: PlaybackRequest) -> Result<bool, String>;
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct PlayAnimationInstance {
|
||||
first_update: bool,
|
||||
}
|
||||
impl PlayAnimationInstance {
|
||||
pub fn begin(
|
||||
&mut self,
|
||||
operation: &PlayAnimation,
|
||||
context: &mut PlaybackContext,
|
||||
service: &mut impl PlaybackService,
|
||||
) -> Result<(), String> {
|
||||
self.first_update = true;
|
||||
refresh_parameters(&operation.parameters, true, service)?;
|
||||
service.set_construction_value(
|
||||
super::skeleton_input::name::encode(b"ProSkater"), context.pro_skater,
|
||||
);
|
||||
// The graph override is consumed/reset before variant choice or play.
|
||||
let transition = context.transition_override.take()
|
||||
.filter(|settings| settings.kind != 0)
|
||||
.unwrap_or(operation.transition);
|
||||
let mut animation = &operation.animation;
|
||||
if let Some(switch) = &operation.switch_animation {
|
||||
if context.is_switch.ok_or("PlayAnimation needs skater switch state")? {
|
||||
animation = switch;
|
||||
}
|
||||
}
|
||||
if let Some(mirror) = &operation.mirror_animation {
|
||||
if context.is_mirrored.ok_or("PlayAnimation needs skater mirror state")? {
|
||||
animation = mirror;
|
||||
}
|
||||
}
|
||||
if let Some(no_board) = &operation.no_board_animation {
|
||||
if !context.board_available.ok_or("PlayAnimation needs PhysOut board availability")? {
|
||||
animation = no_board;
|
||||
}
|
||||
}
|
||||
service.set_posture_enabled(operation.apply_posture);
|
||||
let played = if (1..=4).contains(&transition.kind) {
|
||||
service.play(PlaybackRequest {
|
||||
animation: animation.clone(), speed: operation.playback_speed,
|
||||
start_time: 0.0, transition,
|
||||
})?
|
||||
} else { false };
|
||||
if !played {
|
||||
// This fallback is present in native Begin82BB5630..5660. The
|
||||
// service must not use it to conceal missing tree implementations.
|
||||
service.play(PlaybackRequest {
|
||||
animation: "KeepDefaultAnim".into(), speed: 1.0, start_time: 0.0,
|
||||
transition: TransitionSettings { kind: 1, ..operation.transition },
|
||||
})?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn update(
|
||||
&mut self, operation: &PlayAnimation, service: &mut impl PlaybackService,
|
||||
) -> Result<(), String> {
|
||||
if !self.first_update {
|
||||
refresh_parameters(&operation.parameters, false, service)?;
|
||||
}
|
||||
self.first_update = false;
|
||||
Ok(())
|
||||
}
|
||||
pub fn end(&mut self) {}
|
||||
}
|
||||
|
||||
fn refresh_parameters(
|
||||
parameters: &[PlaybackParameter], beginning: bool, service: &mut impl PlaybackService,
|
||||
) -> Result<(), String> {
|
||||
// Keep read/write effects interleaved across parameters while avoiding two
|
||||
// mutable borrows of the same concrete animation service.
|
||||
struct One(Option<super::playback_parameters::SettableAttribute>);
|
||||
impl AttributeSink for One {
|
||||
fn set_attribute(&mut self, value: super::playback_parameters::SettableAttribute) {
|
||||
self.0 = Some(value);
|
||||
}
|
||||
}
|
||||
for parameter in parameters {
|
||||
let mut output = One(None);
|
||||
parameter.update(beginning, service, &mut output)?;
|
||||
if let Some(attribute) = output.0 { service.set_attribute(attribute); }
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
//! PhaseBlend attribute intersection82D161B0 and Scale/AddWeighted82D16370/428.
|
||||
use super::output::attributes::{AnimationAttribute, AttributeName};
|
||||
|
||||
/// Bone-name mapping supplied by the stock hierarchy. A native excluded bone
|
||||
/// (mirror index -1) maps to itself; missing names remain a data error.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AttributeMirror(pub Vec<(AttributeName, AttributeName)>);
|
||||
impl AttributeMirror {
|
||||
/// Attribute::Mirror82530F48 changes payload identity, never the event key.
|
||||
pub fn apply(&self, attribute: &mut AnimationAttribute) -> Result<(), String> {
|
||||
match attribute.kind {
|
||||
1 => {
|
||||
let x = attribute.payload.0[0].ok_or("Uninitialized mirrored vector attribute")?;
|
||||
attribute.payload.0[0] = Some(x ^ 0x8000_0000);
|
||||
}
|
||||
3 => {
|
||||
let mut name = [0; 5];
|
||||
for (i, word) in name.iter_mut().enumerate() {
|
||||
*word = attribute.payload.0[i].ok_or("Uninitialized mirrored bone reference")?;
|
||||
}
|
||||
let mirrored = self.0.iter().find(|(source, _)| source.0 == name)
|
||||
.map(|(_, target)| target).ok_or("Animation event bone is absent from its hierarchy")?;
|
||||
for (target, source) in attribute.payload.0.iter_mut().zip(mirrored.0) { *target = Some(source); }
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn blend(left: &mut AnimationAttribute, right: &AnimationAttribute, weight: f32) -> Result<(), String> {
|
||||
let left_weight = 1.0-weight;
|
||||
if left.status & 2 != 0 {
|
||||
left.begin_time = -1.0;
|
||||
left.end_time = -1.0;
|
||||
} else {
|
||||
// Time fields use separate multiply/add instructions, not fused math.
|
||||
left.begin_time = right.begin_time * weight + left.begin_time * left_weight;
|
||||
left.end_time = right.end_time * weight + left.end_time * left_weight;
|
||||
}
|
||||
let lanes: &[usize] = match left.kind { 0 | 2 => &[0], 1 => &[0,1,2,3], 3 => &[5], _ => &[] };
|
||||
for &i in lanes {
|
||||
let l = f32::from_bits(left.payload.0[i].ok_or("Uninitialized blended attribute payload")?);
|
||||
let r = f32::from_bits(right.payload.0[i].ok_or("Uninitialized blended attribute payload")?);
|
||||
left.payload.0[i] = Some(r.mul_add(weight,l*left_weight).to_bits());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Native uses a forward-only intersection, retaining left order/metadata.
|
||||
/// It does not append right-only attributes or replace the left reference name.
|
||||
pub fn intersection(left: Vec<AnimationAttribute>, right: &[AnimationAttribute], weight: f32) -> Result<Vec<AnimationAttribute>, String> {
|
||||
let mut result = Vec::new();
|
||||
let mut next = 0;
|
||||
for mut attribute in left {
|
||||
while next < right.len() && right[next].name.0 < attribute.name.0 { next += 1; }
|
||||
let Some(other) = right.get(next) else { break; };
|
||||
if other.name == attribute.name {
|
||||
blend(&mut attribute, other, weight)?;
|
||||
result.push(attribute);
|
||||
}
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
//! Stateful direct Andale Clip clock and attribute evaluation.
|
||||
//! Init827B8AB0, AddTime82D25A00, GetAttributes82D25E30,
|
||||
//! Attribute::Init82D164F8 and scalar curve lookup82D193E0.
|
||||
use super::{clip_clock::{AdvanceResult, ClipClock}, output::attributes::{AnimationAttribute, AttributeName, AttributePayload}};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ClipAttribute {
|
||||
pub name: AttributeName,
|
||||
pub kind: u8,
|
||||
pub begin: f32,
|
||||
pub end: f32,
|
||||
pub payload: Vec<u32>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PlaybackClip {
|
||||
pub clock: ClipClock,
|
||||
pub attributes: Vec<ClipAttribute>,
|
||||
}
|
||||
impl PlaybackClip {
|
||||
pub fn new(frames: f32, fps: f32, base_speed: f32, flags: u32, attributes: Vec<ClipAttribute>) -> Self {
|
||||
Self { clock: ClipClock {
|
||||
frames, fps, base_speed, speed: 1.0,
|
||||
length: (frames - 1.0) / ((1.0 * base_speed) * fps),
|
||||
time: 0.0, previous_time: 0.0, loops_since_evaluation: 0,
|
||||
looping: flags & 0x1000_0000 != 0,
|
||||
phase_controlled: flags & 0x4000_0000 != 0,
|
||||
}, attributes }
|
||||
}
|
||||
/// Animatable825310F0 resets the property before every AddTime call.
|
||||
pub fn advance(&mut self, dt: f32, phase: f32) -> AdvanceResult {
|
||||
let mut result = AdvanceResult {
|
||||
crossed_end: false, overshoot: -1.0, remaining_before_wrap: -1.0,
|
||||
};
|
||||
self.clock.advance(dt, phase, &mut result);
|
||||
result
|
||||
}
|
||||
/// Ordered GetAttributes. The untimed bulk path passes status4, unlike
|
||||
/// GetAttributeStatus's untimed status6; its times are multiplied by length.
|
||||
pub fn attributes(&self, mask: u32) -> Result<Vec<AnimationAttribute>, String> {
|
||||
let mut output = Vec::new();
|
||||
for attribute in &self.attributes {
|
||||
let status = if attribute.begin == -1.0 { 4 }
|
||||
else { self.clock.attribute_status(attribute.begin, attribute.end) };
|
||||
if attribute.begin != -1.0 && (mask & 0x1c & u32::from(status) == 0 || mask & 3 & u32::from(status) == 0) { continue; }
|
||||
output.push(self.materialize(attribute, status)?);
|
||||
}
|
||||
Ok(output)
|
||||
}
|
||||
///82D25CA0 uses status6 for untimed records and returns first valid match.
|
||||
pub fn attribute(&self, name: AttributeName, mask: u32) -> Result<Option<AnimationAttribute>, String> {
|
||||
for attribute in &self.attributes {
|
||||
if attribute.name != name { continue; }
|
||||
let status = self.clock.attribute_status(attribute.begin, attribute.end);
|
||||
if mask & 0x1c & u32::from(status) == 0 || mask & 3 & u32::from(status) == 0 { continue; }
|
||||
return self.materialize(attribute, status).map(Some);
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
fn materialize(&self, attribute: &ClipAttribute, status: u8) -> Result<AnimationAttribute, String> {
|
||||
let mut payload = [None; 6];
|
||||
let lanes = match attribute.kind { 0 => 1, 1 => 4, 3 => 6, _ => 0 };
|
||||
if attribute.payload.len() < lanes { return Err("Truncated clip attribute payload".into()); }
|
||||
for i in 0..lanes { payload[i] = Some(attribute.payload[i]); }
|
||||
if attribute.kind == 2 {
|
||||
// The source deliberately multiplies time*fps*playback_speed;
|
||||
// base_speed is not an additional factor in Attribute::Init.
|
||||
let frame = (self.clock.time * self.clock.fps) * self.clock.speed;
|
||||
payload[0] = Some(sample_curve(&attribute.payload, frame)?.to_bits());
|
||||
}
|
||||
Ok(AnimationAttribute {
|
||||
name: attribute.name, kind: attribute.kind, payload: AttributePayload(payload),
|
||||
status, sequence_id: -1,
|
||||
begin_time: if status & 2 != 0 { -1.0 } else { attribute.begin * self.clock.length },
|
||||
end_time: if status & 2 != 0 { -1.0 } else { attribute.end * self.clock.length },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// The curve record is an opaque word, point count, then(x,y) pairs. Native
|
||||
/// binary search interpolates the final crossed bounds with a fused multiply.
|
||||
pub fn sample_curve(words: &[u32], time: f32) -> Result<f32, String> {
|
||||
let count = *words.get(1).ok_or("Missing animation curve count")? as usize;
|
||||
if count == 0 || count > words.len().saturating_sub(2) / 2 { return Err("Truncated/empty animation curve".into()); }
|
||||
let point = |index: usize| (f32::from_bits(words[2+2*index]), f32::from_bits(words[3+2*index]));
|
||||
let mut low = 0;
|
||||
let mut high = count-1;
|
||||
let (mut low_x, mut low_y) = point(low);
|
||||
let (mut high_x, mut high_y) = point(high);
|
||||
if !(time > low_x) { return Ok(low_y); }
|
||||
if !(time < high_x) { return Ok(high_y); }
|
||||
while !(low_x > high_x) {
|
||||
let middle = (low+high)/2;
|
||||
let (x,y) = point(middle);
|
||||
if time > x {
|
||||
low = middle+1;
|
||||
if low >= count { return Err("Malformed animation curve search bounds".into()); }
|
||||
(low_x,low_y) = point(low);
|
||||
} else if time < x {
|
||||
high = middle.checked_sub(1).ok_or("Malformed animation curve search bounds")?;
|
||||
(high_x,high_y) = point(high);
|
||||
} else { return Ok(y); }
|
||||
}
|
||||
let slope = (low_y-high_y)/(low_x-high_x);
|
||||
Ok(slope.mul_add(time-high_x,high_y))
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
//! PlayAnimation's parameter refresh82BB4760 and settable queue82D19100.
|
||||
use super::output::attributes::{AnimationAttribute, AttributeName};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum ParameterSource {
|
||||
MotionIntent(String),
|
||||
FilteredIntent(String),
|
||||
LastAnimation(AttributeName),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct PlaybackParameter {
|
||||
pub source: ParameterSource,
|
||||
pub rename: Option<AttributeName>,
|
||||
pub default_value: Option<f32>,
|
||||
pub normalized: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct SettableAttribute {
|
||||
pub name: AttributeName,
|
||||
pub value: f32,
|
||||
pub normalized: bool,
|
||||
pub sequence_id: i32,
|
||||
}
|
||||
|
||||
/// Gameplay providers are queried when a node executes, preserving visibility
|
||||
/// of earlier same-tick graph operations and the last animation tree output.
|
||||
pub trait ParameterInputs {
|
||||
fn motion_intent(&self, name: &str) -> Option<f32>;
|
||||
fn filtered_intent(&self, name: &str) -> Option<f32>;
|
||||
fn last_attribute(&mut self, name: AttributeName) -> Result<Option<AnimationAttribute>, String>;
|
||||
}
|
||||
|
||||
pub trait AttributeSink {
|
||||
fn set_attribute(&mut self, attribute: SettableAttribute);
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct SettableAttributes {
|
||||
entries: Vec<SettableAttribute>,
|
||||
}
|
||||
impl SettableAttributes {
|
||||
pub fn entries(&self) -> &[SettableAttribute] {
|
||||
&self.entries
|
||||
}
|
||||
pub fn clear(&mut self) {
|
||||
self.entries.clear();
|
||||
}
|
||||
}
|
||||
impl AttributeSink for SettableAttributes {
|
||||
fn set_attribute(&mut self, attribute: SettableAttribute) {
|
||||
if let Some(entry) = self.entries.iter_mut().find(|entry| {
|
||||
entry.name == attribute.name && entry.sequence_id == attribute.sequence_id
|
||||
}) {
|
||||
*entry = attribute;
|
||||
} else {
|
||||
self.entries.push(attribute);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PlaybackParameter {
|
||||
/// Last-animation parameters execute only on Begin. Existing non-scalar
|
||||
/// attributes suppress the default too: native does not reinterpret them.
|
||||
pub fn update(
|
||||
&self,
|
||||
beginning: bool,
|
||||
inputs: &mut impl ParameterInputs,
|
||||
output: &mut impl AttributeSink,
|
||||
) -> Result<(), String> {
|
||||
use super::skeleton_input::name::encode;
|
||||
let (name, value, normalized) = match &self.source {
|
||||
ParameterSource::MotionIntent(source) => (
|
||||
self.rename.unwrap_or_else(|| encode(source.as_bytes())),
|
||||
inputs.motion_intent(source).or(self.default_value),
|
||||
self.normalized,
|
||||
),
|
||||
ParameterSource::FilteredIntent(source) => {
|
||||
let found = inputs.filtered_intent(source);
|
||||
(
|
||||
self.rename.unwrap_or_else(|| encode(source.as_bytes())),
|
||||
found.or(self.default_value),
|
||||
//82BB4908..491C goes through4AF8 and explicitly passes0.
|
||||
found.is_some() && self.normalized,
|
||||
)
|
||||
}
|
||||
ParameterSource::LastAnimation(source) => {
|
||||
if !beginning {
|
||||
return Ok(());
|
||||
}
|
||||
let value = match inputs.last_attribute(*source)? {
|
||||
Some(attribute) if matches!(attribute.kind, 0 | 2) => Some(
|
||||
f32::from_bits(attribute.payload.0[0]
|
||||
.ok_or("Last animation scalar payload is uninitialized")?),
|
||||
),
|
||||
Some(_) => return Ok(()),
|
||||
None => self.default_value,
|
||||
};
|
||||
(self.rename.unwrap_or(*source), value, false)
|
||||
}
|
||||
};
|
||||
if let Some(value) = value {
|
||||
output.set_attribute(SettableAttribute {
|
||||
name, value, normalized, sequence_id: -1,
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Six-word intent/animation identifiers use the same native six-character
|
||||
/// chunks as the five-word attribute encoder, but retain up to36 input bytes.
|
||||
pub fn intent_key(text: &str) -> [u32; 6] {
|
||||
let bytes = text.as_bytes();
|
||||
let first = super::skeleton_input::name::encode(bytes).0;
|
||||
let tail = if bytes.len() > 30 && !bytes[..30].contains(&0) {
|
||||
super::skeleton_input::name::encode(&bytes[30..]).0[0]
|
||||
} else { 0 };
|
||||
[first[0], first[1], first[2], first[3], first[4], tail]
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
//! TU3 Sequence82B95C18 and Blend82B96058/82B961F8/82B963D8.
|
||||
use super::{clip_clock::AdvanceResult, output::attributes::{AnimationAttribute,AttributeName}, playback::TransitionSettings, playback_parameters::SettableAttribute, playback_tree::{Evaluation,PlaybackTree,PoseCommand}};
|
||||
|
||||
#[derive(Clone,Debug)]
|
||||
pub struct PlaybackTransition {
|
||||
pub from:Box<PlaybackTree>,
|
||||
pub to:Box<PlaybackTree>,
|
||||
pub settings:TransitionSettings,
|
||||
elapsed:f32,
|
||||
sequence_complete:bool,
|
||||
}
|
||||
impl PlaybackTransition {
|
||||
pub fn new(from:PlaybackTree,to:PlaybackTree,settings:TransitionSettings)->Self {
|
||||
Self {from:Box::new(from),to:Box::new(to),settings,elapsed:0.0,sequence_complete:false}
|
||||
}
|
||||
pub fn complete(&self)->bool {
|
||||
if self.settings.kind==4 {self.sequence_complete} else {self.elapsed>self.settings.seconds}
|
||||
}
|
||||
pub fn advance(&mut self,dt:f32,phase:f32,property:&mut AdvanceResult) {
|
||||
let mut discarded=AdvanceResult {crossed_end:false,overshoot:-1.0,remaining_before_wrap:-1.0};
|
||||
if self.settings.kind==4 {
|
||||
if self.sequence_complete {self.to.advance(dt,phase,property);}
|
||||
else {
|
||||
self.from.advance(dt,phase,&mut discarded);
|
||||
if discarded.crossed_end {self.sequence_complete=true;self.to.advance(discarded.overshoot,phase,property);}
|
||||
else {property.crossed_end=false;property.remaining_before_wrap=self.to.length()+discarded.remaining_before_wrap;}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if self.settings.matching!=1 {self.from.advance(dt,phase,&mut discarded);}
|
||||
if self.settings.matching==3 {
|
||||
let length=self.to.length();let time=self.from.time();
|
||||
self.to.set_time(if time-length>=0.0 {length} else {time});
|
||||
} else if self.settings.matching==2 && self.from.length()>0.0 {
|
||||
self.to.set_time(self.to.length()*(self.from.time()/self.from.length()));
|
||||
} else {self.to.advance(dt,phase,property);}
|
||||
self.elapsed+=dt;
|
||||
}
|
||||
pub fn set_attributes(&mut self,attributes:&[SettableAttribute])->Result<bool,String> {
|
||||
let result=self.to.set_attributes(attributes)?;
|
||||
if self.settings.kind==4 && !self.sequence_complete {self.from.set_attributes(attributes)} else {Ok(result)}
|
||||
}
|
||||
fn attributes_child(&self)->&PlaybackTree {
|
||||
if self.settings.kind==4 && !self.sequence_complete {&self.from} else {&self.to}
|
||||
}
|
||||
pub fn attributes(&self,mask:u32)->Result<Vec<AnimationAttribute>,String> {self.attributes_child().attributes(mask)}
|
||||
pub fn query_attribute(&self,name:AttributeName,mask:u32,output:&mut AnimationAttribute)->Result<bool,String> {self.attributes_child().query_attribute(name,mask,output)}
|
||||
pub fn weight(&self)->f32 {
|
||||
let value=self.elapsed/self.settings.seconds;
|
||||
let lower=if -value>=0.0 {0.0} else {value};
|
||||
let weight=if 1.0-lower>=0.0 {lower} else {1.0};
|
||||
if self.settings.seconds<f32::from_bits(0x3d4ccccd) || weight>1.0 || weight<0.0 {return weight;}
|
||||
let square=weight*weight;let cube=square*weight;
|
||||
square.mul_add(3.0,-(cube*2.0))
|
||||
}
|
||||
pub fn evaluate(&mut self,parameters:Evaluation,enabled:bool,output:&mut Vec<PoseCommand>)->Result<bool,String> {
|
||||
if self.settings.kind==4 {return if self.sequence_complete {self.to.evaluate(parameters,enabled,output)} else {self.from.evaluate(parameters,enabled,output)};}
|
||||
let weight=self.weight();
|
||||
let from=self.from.evaluate(parameters,enabled,output)?;
|
||||
let to=self.to.evaluate(parameters,enabled,output)?;
|
||||
if enabled && from && to {
|
||||
output.push(if self.settings.kind==3 {PoseCommand::ChannelBlend {weight,use_channels_from_weights:self.settings.use_channels_from_weights}} else {PoseCommand::Blend {weight}});
|
||||
}
|
||||
Ok(from||to)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
//! Owned animation tree evaluation; stock data constructs this topology.
|
||||
pub mod selection_space;
|
||||
use super::{
|
||||
clip_clock::AdvanceResult,
|
||||
output::attributes::{AnimationAttribute, AttributeName},
|
||||
phase_blend::PhaseBlend,
|
||||
playback_clip::PlaybackClip,
|
||||
playback_parameters::SettableAttribute,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum PlaybackTree {
|
||||
Clip {
|
||||
name: String,
|
||||
clip: PlaybackClip,
|
||||
},
|
||||
PhaseBlend(PhaseBlend),
|
||||
SelectionSpace(selection_space::SelectionSpace),
|
||||
Transition(super::playback_transition::PlaybackTransition),
|
||||
///SkaterAnim::AddBindPose82B98118, latched when a named tree is created.
|
||||
BindPose {
|
||||
motion: Box<PlaybackTree>,
|
||||
posture: Option<super::posture::PosturePose>,
|
||||
board_backwards: bool,
|
||||
mirror_modes: Vec<u32>,
|
||||
attribute_mirror: std::sync::Arc<super::playback_attributes::AttributeMirror>,
|
||||
},
|
||||
}
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum PoseCommand {
|
||||
///82D25B00 supplies both bounded source times and the unconsumed wrap count.
|
||||
Clip {
|
||||
name: String,
|
||||
previous_time: f32,
|
||||
time: f32,
|
||||
loops: u32,
|
||||
},
|
||||
///Blend the two most recent subtree poses, preserving authored nesting.
|
||||
Blend {
|
||||
weight: f32,
|
||||
},
|
||||
ChannelBlend {
|
||||
weight: f32,
|
||||
use_channels_from_weights: bool,
|
||||
},
|
||||
Pose {
|
||||
name: String,
|
||||
},
|
||||
Add {
|
||||
motion_is_a: bool,
|
||||
},
|
||||
Mirror {
|
||||
trajectory_mode: u32,
|
||||
},
|
||||
}
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Evaluation {
|
||||
pub cull_threshold: f32,
|
||||
pub update_history: bool,
|
||||
}
|
||||
impl PlaybackTree {
|
||||
pub fn length(&self) -> f32 {
|
||||
match self {
|
||||
Self::Clip { clip, .. } => clip.clock.length,
|
||||
Self::PhaseBlend(tree) => tree.length,
|
||||
Self::SelectionSpace(tree) => tree.current().map_or(0.0, Self::length),
|
||||
Self::Transition(tree) => tree.to.length(),
|
||||
Self::BindPose { motion, .. } => motion.length(),
|
||||
}
|
||||
}
|
||||
pub fn time(&self) -> f32 {
|
||||
match self {
|
||||
Self::Clip { clip, .. } => clip.clock.sample_time(),
|
||||
Self::PhaseBlend(tree) => tree.time,
|
||||
Self::SelectionSpace(tree) => tree.current().map_or(0.0, Self::time),
|
||||
Self::Transition(tree) => tree.to.time(),
|
||||
Self::BindPose { motion, .. } => motion.time(),
|
||||
}
|
||||
}
|
||||
pub fn set_time(&mut self, time: f32) {
|
||||
match self {
|
||||
Self::Clip { clip, .. } => clip.clock.set_time(time),
|
||||
Self::PhaseBlend(tree) => tree.set_time(time),
|
||||
Self::SelectionSpace(tree) => tree.set_time(time),
|
||||
Self::Transition(tree) => tree.to.set_time(time),
|
||||
Self::BindPose { motion, .. } => motion.set_time(time),
|
||||
}
|
||||
}
|
||||
pub fn set_speed(&mut self, speed: f32) {
|
||||
match self {
|
||||
Self::Clip { clip, .. } => clip.clock.set_speed(speed),
|
||||
Self::PhaseBlend(tree) => tree.set_speed(speed),
|
||||
Self::SelectionSpace(tree) => tree.set_speed(speed),
|
||||
Self::Transition(tree) => tree.to.set_speed(speed),
|
||||
Self::BindPose { motion, .. } => motion.set_speed(speed),
|
||||
}
|
||||
}
|
||||
pub fn advance(&mut self, dt: f32, phase: f32, property: &mut AdvanceResult) {
|
||||
match self {
|
||||
Self::Clip { clip, .. } => clip.clock.advance(dt, phase, property),
|
||||
Self::PhaseBlend(tree) => tree.advance(dt, phase, property),
|
||||
Self::SelectionSpace(tree) => {
|
||||
if let Some(child) = tree.current_mut() {
|
||||
child.advance(dt, phase, property);
|
||||
}
|
||||
}
|
||||
Self::Transition(tree) => tree.advance(dt, phase, property),
|
||||
Self::BindPose { motion, .. } => motion.advance(dt, phase, property),
|
||||
}
|
||||
}
|
||||
pub fn set_attributes(&mut self, attributes: &[SettableAttribute]) -> Result<bool, String> {
|
||||
match self {
|
||||
Self::Clip { .. } => Ok(false),
|
||||
Self::PhaseBlend(tree) => tree.set_attributes(attributes),
|
||||
Self::SelectionSpace(tree) => tree.set_attributes(attributes),
|
||||
Self::Transition(tree) => tree.set_attributes(attributes),
|
||||
Self::BindPose { motion, .. } => motion.set_attributes(attributes),
|
||||
}
|
||||
}
|
||||
pub fn attributes(&self, mask: u32) -> Result<Vec<AnimationAttribute>, String> {
|
||||
match self {
|
||||
Self::Clip { clip, .. } => clip.attributes(mask),
|
||||
Self::PhaseBlend(tree) => tree.attributes(mask),
|
||||
Self::SelectionSpace(tree) => tree
|
||||
.current()
|
||||
.ok_or("SelectionSpace attributes requested before native selection")?
|
||||
.attributes(mask),
|
||||
Self::Transition(tree) => tree.attributes(mask),
|
||||
Self::BindPose {
|
||||
motion,
|
||||
mirror_modes,
|
||||
attribute_mirror,
|
||||
..
|
||||
} => {
|
||||
let mut attributes = motion.attributes(mask)?;
|
||||
for _ in mirror_modes {
|
||||
for attribute in &mut attributes {
|
||||
attribute_mirror.apply(attribute)?;
|
||||
}
|
||||
}
|
||||
Ok(attributes)
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn attribute(
|
||||
&self,
|
||||
name: AttributeName,
|
||||
mask: u32,
|
||||
) -> Result<Option<AnimationAttribute>, String> {
|
||||
let mut output =
|
||||
super::output::attributes::MotionGraphAttribute { name, value: 0.0 }.to_animation();
|
||||
Ok(self
|
||||
.query_attribute(name, mask, &mut output)?
|
||||
.then_some(output))
|
||||
}
|
||||
pub fn query_attribute(
|
||||
&self,
|
||||
name: AttributeName,
|
||||
mask: u32,
|
||||
output: &mut AnimationAttribute,
|
||||
) -> Result<bool, String> {
|
||||
match self {
|
||||
Self::PhaseBlend(tree) => tree.query_attribute(name, mask, output),
|
||||
Self::SelectionSpace(tree) => tree
|
||||
.current()
|
||||
.ok_or("SelectionSpace attribute requested before native selection")?
|
||||
.query_attribute(name, mask, output),
|
||||
Self::Transition(tree) => tree.query_attribute(name, mask, output),
|
||||
Self::BindPose {
|
||||
motion,
|
||||
mirror_modes,
|
||||
attribute_mirror,
|
||||
..
|
||||
} => {
|
||||
//825476D0 leaves a child's partial output untouched on false.
|
||||
let found = motion.query_attribute(name, mask, output)?;
|
||||
if found {
|
||||
for _ in mirror_modes {
|
||||
attribute_mirror.apply(output)?;
|
||||
}
|
||||
}
|
||||
Ok(found)
|
||||
}
|
||||
Self::Clip { clip, .. } => match clip.attribute(name, mask)? {
|
||||
Some(attribute) => {
|
||||
output.copy_from(&attribute);
|
||||
Ok(true)
|
||||
}
|
||||
None => Ok(false),
|
||||
},
|
||||
}
|
||||
}
|
||||
pub fn evaluate(
|
||||
&mut self,
|
||||
parameters: Evaluation,
|
||||
enabled: bool,
|
||||
output: &mut Vec<PoseCommand>,
|
||||
) -> Result<bool, String> {
|
||||
match self {
|
||||
Self::PhaseBlend(tree) => tree.evaluate(parameters, enabled, output),
|
||||
Self::SelectionSpace(tree) => {
|
||||
if !enabled {
|
||||
Ok(false)
|
||||
} else {
|
||||
tree.current_mut()
|
||||
.ok_or("SelectionSpace evaluated before native selection")?
|
||||
.evaluate(parameters, true, output)
|
||||
}
|
||||
}
|
||||
Self::Transition(tree) => tree.evaluate(parameters, enabled, output),
|
||||
Self::BindPose {
|
||||
motion,
|
||||
posture,
|
||||
board_backwards,
|
||||
mirror_modes,
|
||||
..
|
||||
} => {
|
||||
let produced = motion.evaluate(parameters, enabled, output)?;
|
||||
if produced {
|
||||
if let Some(pose) = posture {
|
||||
output.push(PoseCommand::Pose {
|
||||
name: pose.name().into(),
|
||||
});
|
||||
output.push(PoseCommand::Add { motion_is_a: true });
|
||||
}
|
||||
output.push(PoseCommand::Pose {
|
||||
name: "RIG_TPOSE".into(),
|
||||
});
|
||||
output.push(PoseCommand::Add { motion_is_a: true });
|
||||
if *board_backwards {
|
||||
output.push(PoseCommand::Pose {
|
||||
name: "BOARD_BACKWARDS".into(),
|
||||
});
|
||||
output.push(PoseCommand::Add { motion_is_a: false });
|
||||
output.push(PoseCommand::Pose {
|
||||
name: "BOARD_BACKWARDS_IK".into(),
|
||||
});
|
||||
output.push(PoseCommand::Add { motion_is_a: true });
|
||||
}
|
||||
for &trajectory_mode in mirror_modes.iter() {
|
||||
output.push(PoseCommand::Mirror { trajectory_mode });
|
||||
}
|
||||
}
|
||||
Ok(produced)
|
||||
}
|
||||
Self::Clip { name, clip } => {
|
||||
let clock = &mut clip.clock;
|
||||
let rate = clock.base_speed * clock.speed;
|
||||
let limit = (clock.frames - 1.0) / clock.fps;
|
||||
let bound = |time: f32| {
|
||||
let lower = if -time >= 0.0 { 0.0 } else { time };
|
||||
if limit - lower >= 0.0 { lower } else { limit }
|
||||
};
|
||||
let mut previous_time = bound(clock.previous_time * rate);
|
||||
let time = bound(clock.sample_time() * rate);
|
||||
if clock.loops_since_evaluation == 0 && previous_time > time {
|
||||
previous_time = time;
|
||||
}
|
||||
if enabled {
|
||||
output.push(PoseCommand::Clip {
|
||||
name: name.clone(),
|
||||
previous_time,
|
||||
time,
|
||||
loops: clock.loops_since_evaluation,
|
||||
});
|
||||
}
|
||||
if parameters.update_history {
|
||||
clock.commit_evaluation();
|
||||
}
|
||||
Ok(enabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "posture/tree_tests.rs"]
|
||||
mod posture_tests;
|
||||
Loaded 100 of 816 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user