mirror of
https://github.com/SK8-ENGINE/skate-3-rust-engine.git
synced 2026-10-26 19:47:44 +00:00
Wire stock gesture recognition and 360 flip graph path
This commit is contained in:
1 parent
b4f5b67d5d
commit
b033d59b1d
24 files changed
+1698
-11
No files matched your search
@@ -3,6 +3,10 @@
|
||||
Double-click **PLAY.bat** to launch. Use an XInput-compatible controller;
|
||||
**Esc** exits. Startup selects the supplied **Easy** profile.
|
||||
|
||||
The standing/rolling **360 flip** now uses the supplied gesture files and
|
||||
ActionGraph/MotionGraph route, including right-stick crouch preparation.
|
||||
See [360-flip integration and visual checks](docs/360-FLIP.md).
|
||||
|
||||
Double-click **PLAY-MAP.bat** to choose a `.skate` map, or drag a map onto
|
||||
either launcher. `maps/private/` contains local copies of the transition park
|
||||
and parity grid. `maps/format-demo.skate` is a small original textured floor.
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
//! Right-stick anticipation publication, Fill825999F0:
|
||||
//! angle/magnitude 82599AD0..9C4C, emissions 8259AD0C..AD40.
|
||||
use super::{
|
||||
angle::left_stick_angle,
|
||||
controller::{DerivedControllerInput, magnitude},
|
||||
riding_intentions::RidingIntent,
|
||||
};
|
||||
|
||||
pub fn produce(controller: &DerivedControllerInput) -> Vec<RidingIntent> {
|
||||
let words = controller.words();
|
||||
let x = f32::from_bits(words[9]);
|
||||
let y = f32::from_bits(words[10]);
|
||||
if x == 0.0 && y == 0.0 {
|
||||
return Vec::new();
|
||||
}
|
||||
// Unlike steering, these two records have no actor1908 bit gate.
|
||||
vec![
|
||||
RidingIntent {
|
||||
name: "AnticMag",
|
||||
value: magnitude(x.mul_add(x, y * y)),
|
||||
},
|
||||
RidingIntent {
|
||||
name: "AnticAngle",
|
||||
value: left_stick_angle(x, y),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn neutral_removes_anticipation_and_tail_hold_selects_ollie_range() {
|
||||
assert!(produce(&DerivedControllerInput::from_words([0; 26])).is_empty());
|
||||
let mut words = [0; 26];
|
||||
words[10] = (-1.0f32).to_bits();
|
||||
let intents = produce(&DerivedControllerInput::from_words(words));
|
||||
assert!((intents[0].value - 1.0).abs() < 0.000001);
|
||||
assert!(intents[1].value.abs() <= 0.52);
|
||||
words[10] = 1.0f32.to_bits();
|
||||
assert!(
|
||||
produce(&DerivedControllerInput::from_words(words))[1]
|
||||
.value
|
||||
.abs()
|
||||
>= 2.62
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
//! TU3 pattern recognition, 82697168/826972B8/826974A8.
|
||||
//! Points are supplied in PAT authoring order. Native 82696B18 pushes them
|
||||
//! to the front of a ring buffer, then the matcher walks that buffer backwards.
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Pattern {
|
||||
pub name: String,
|
||||
pub points: Vec<[f32; 2]>,
|
||||
pub tolerance_squared: f32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Settings {
|
||||
/// Recognizer config byte 33. Supplied by the owner, never inferred here.
|
||||
pub maximum_misses: u8,
|
||||
pub difficulty: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
struct Node {
|
||||
active: bool,
|
||||
complete: bool,
|
||||
next: usize,
|
||||
elapsed: u16,
|
||||
misses: u8,
|
||||
distance: f32,
|
||||
}
|
||||
|
||||
impl Node {
|
||||
fn tick(&mut self, pattern: &Pattern, sample: [f32; 2], maximum_misses: u8) {
|
||||
if !self.active {
|
||||
let distance = distance_squared(pattern.points[0], sample);
|
||||
if distance <= pattern.tolerance_squared {
|
||||
*self = Self {
|
||||
active: true,
|
||||
next: 1,
|
||||
elapsed: 1,
|
||||
distance,
|
||||
..Self::default()
|
||||
};
|
||||
}
|
||||
} else if !self.complete {
|
||||
let distance = distance_squared(pattern.points[self.next], sample);
|
||||
if distance <= pattern.tolerance_squared {
|
||||
self.distance += distance;
|
||||
self.elapsed = (self.elapsed + 1) & 0x3ff;
|
||||
if self.next + 1 == pattern.points.len() {
|
||||
self.complete = true;
|
||||
} else {
|
||||
self.next += 1;
|
||||
self.misses = 0;
|
||||
}
|
||||
} else {
|
||||
// Holding the first point does not age a pending gesture.
|
||||
if self.next != 1
|
||||
|| distance_squared(pattern.points[0], sample) > pattern.tolerance_squared
|
||||
{
|
||||
self.elapsed = (self.elapsed + 1) & 0x3ff;
|
||||
self.misses = (self.misses + 1) & 0x3f;
|
||||
}
|
||||
if self.misses > maximum_misses {
|
||||
*self = Self::default();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn score(self, count: usize) -> f32 {
|
||||
let count = count as f32;
|
||||
// The unusual two-sided 0.15 operation is present in 826972B8.
|
||||
let mean = (self.distance.max(0.15) / count).min(0.15);
|
||||
(count * count * count * count) / (mean * self.elapsed as f32)
|
||||
}
|
||||
}
|
||||
|
||||
fn distance_squared(a: [f32; 2], b: [f32; 2]) -> f32 {
|
||||
let x = a[0] - b[0];
|
||||
let y = a[1] - b[1];
|
||||
// VMX multiplies then adds; do not contract to FMA.
|
||||
x * x + y * y
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Recognition {
|
||||
pub pattern: usize,
|
||||
pub strength: f32,
|
||||
pub distance: f32,
|
||||
pub elapsed: f32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Recognizer {
|
||||
patterns: Vec<Pattern>,
|
||||
nodes: Vec<Node>,
|
||||
has_previous_sample: bool,
|
||||
refractory: bool,
|
||||
held: Option<usize>,
|
||||
}
|
||||
|
||||
impl Recognizer {
|
||||
pub fn new(patterns: Vec<Pattern>) -> Result<Self, String> {
|
||||
for pattern in &patterns {
|
||||
if !(2..=15).contains(&pattern.points.len())
|
||||
|| !pattern.tolerance_squared.is_finite()
|
||||
|| pattern.tolerance_squared < 0.0
|
||||
|| pattern.points.iter().flatten().any(|v| !v.is_finite())
|
||||
{
|
||||
return Err(format!("invalid native gesture pattern {}", pattern.name));
|
||||
}
|
||||
}
|
||||
Ok(Self {
|
||||
nodes: vec![Node::default(); patterns.len()],
|
||||
patterns,
|
||||
has_previous_sample: false,
|
||||
refractory: false,
|
||||
held: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn patterns(&self) -> &[Pattern] {
|
||||
&self.patterns
|
||||
}
|
||||
|
||||
/// Held messages are checked before recognition in 826962D8. Native point
|
||||
/// zero is the final authored coordinate (82699738 push-front).
|
||||
pub fn held(&mut self, sample: [f32; 2]) -> Option<usize> {
|
||||
if let Some(index) = self.held {
|
||||
let pattern = &self.patterns[index];
|
||||
if distance_squared(*pattern.points.last().unwrap(), sample) > pattern.tolerance_squared
|
||||
{
|
||||
self.held = None;
|
||||
}
|
||||
}
|
||||
self.held
|
||||
}
|
||||
|
||||
pub fn sample(&mut self, sample: [f32; 2], settings: Settings) -> Option<Recognition> {
|
||||
if self.refractory {
|
||||
self.nodes.fill(Node::default());
|
||||
self.refractory = false;
|
||||
return None;
|
||||
}
|
||||
if !self.has_previous_sample {
|
||||
self.has_previous_sample = true;
|
||||
return None;
|
||||
}
|
||||
let mut best: Option<usize> = None;
|
||||
for (node, pattern) in self.nodes.iter_mut().zip(&self.patterns) {
|
||||
node.tick(pattern, sample, settings.maximum_misses);
|
||||
}
|
||||
for (index, node) in self.nodes.iter().enumerate() {
|
||||
if node.complete
|
||||
&& best.is_none_or(|old| {
|
||||
node.score(self.patterns[index].points.len())
|
||||
> self.nodes[old].score(self.patterns[old].points.len())
|
||||
})
|
||||
{
|
||||
best = Some(index);
|
||||
}
|
||||
}
|
||||
let pattern = best?;
|
||||
let node = self.nodes[pattern];
|
||||
let ratio = node.elapsed as f32 / self.patterns[pattern].points.len() as f32;
|
||||
// 822249B4/82063B08 and 821E63E8/821E63EC, checked against TU3.
|
||||
let (low, high) = if settings.difficulty == 2 {
|
||||
(1.5, 3.0)
|
||||
} else {
|
||||
(1.75, 4.4)
|
||||
};
|
||||
let strength = if ratio <= low {
|
||||
1.0
|
||||
} else if ratio >= high {
|
||||
0.0
|
||||
} else {
|
||||
let slope = 1.0 / (low - high);
|
||||
slope.mul_add(ratio, -(slope * high))
|
||||
};
|
||||
self.refractory = true;
|
||||
self.held = Some(pattern);
|
||||
Some(Recognition {
|
||||
pattern,
|
||||
strength,
|
||||
distance: node.distance,
|
||||
elapsed: node.elapsed as f32,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
fn recognizer() -> Recognizer {
|
||||
Recognizer::new(vec![Pattern {
|
||||
name: "360Flip".into(),
|
||||
points: vec![
|
||||
[-0.965714, 0.268571],
|
||||
[-0.497143, 0.84],
|
||||
[0.211429, 0.988571],
|
||||
[0.908571, -0.405714],
|
||||
],
|
||||
tolerance_squared: 0.4 * 0.4,
|
||||
}])
|
||||
.unwrap()
|
||||
}
|
||||
const SETTINGS: Settings = Settings {
|
||||
maximum_misses: 3,
|
||||
difficulty: 0,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn anticipation_hold_does_not_weaken_authored_scoop() {
|
||||
let mut r = recognizer();
|
||||
let points = r.patterns[0].points.clone();
|
||||
r.sample([0.0; 2], SETTINGS);
|
||||
for _ in 0..120 {
|
||||
assert!(r.sample(points[0], SETTINGS).is_none());
|
||||
}
|
||||
assert!(r.sample(points[1], SETTINGS).is_none());
|
||||
assert!(r.sample(points[2], SETTINGS).is_none());
|
||||
let result = r.sample(points[3], SETTINGS).unwrap();
|
||||
assert_eq!(
|
||||
(result.pattern, result.strength, result.elapsed),
|
||||
(0, 1.0, 4.0)
|
||||
);
|
||||
assert_eq!(r.held(points[3]), Some(0));
|
||||
assert!(r.sample(points[3], SETTINGS).is_none());
|
||||
assert_eq!(r.held([0.0; 2]), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disconnected_scoop_and_reverse_path_do_not_trigger() {
|
||||
let mut r = recognizer();
|
||||
let points = r.patterns[0].points.clone();
|
||||
r.sample([0.0; 2], SETTINGS);
|
||||
r.sample(points[0], SETTINGS);
|
||||
for _ in 0..4 {
|
||||
assert!(r.sample([0.0; 2], SETTINGS).is_none());
|
||||
}
|
||||
for &point in &points[1..] {
|
||||
assert!(r.sample(point, SETTINGS).is_none());
|
||||
}
|
||||
let mut r = recognizer();
|
||||
r.sample([0.0; 2], SETTINGS);
|
||||
for &point in points.iter().rev() {
|
||||
assert!(r.sample(point, SETTINGS).is_none());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
//! Native input publication. Controller/graph producers remain separate stages.
|
||||
pub mod angle;
|
||||
pub mod gesture;
|
||||
pub mod anticipation_intentions;
|
||||
pub mod animation_packet;
|
||||
pub mod controller;
|
||||
pub mod gameplay_map;
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
//! Authored joystick PAT data consumed by TU3 82696B18.
|
||||
use skate_core::input::gesture::Pattern;
|
||||
use std::path::Path;
|
||||
|
||||
pub fn load(path: &Path) -> Result<Vec<Pattern>, String> {
|
||||
parse(&std::fs::read_to_string(path).map_err(|e| format!("{}: {e}", path.display()))?)
|
||||
.map_err(|e| format!("{}: {e}", path.display()))
|
||||
}
|
||||
|
||||
pub fn parse(source: &str) -> Result<Vec<Pattern>, String> {
|
||||
let mut patterns: Vec<Pattern> = Vec::new();
|
||||
let mut tolerance = None;
|
||||
for (line, source) in source.lines().enumerate() {
|
||||
let words: Vec<_> = source
|
||||
.split("//")
|
||||
.next()
|
||||
.unwrap_or("")
|
||||
.split_whitespace()
|
||||
.collect();
|
||||
if words.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let fail = || format!("invalid PAT directive at line {}: {source}", line + 1);
|
||||
let number = |index: usize| -> Result<f32, String> {
|
||||
let value: f32 = words
|
||||
.get(index)
|
||||
.ok_or_else(fail)?
|
||||
.parse()
|
||||
.map_err(|_| fail())?;
|
||||
if !value.is_finite() {
|
||||
return Err(fail());
|
||||
}
|
||||
Ok(value)
|
||||
};
|
||||
match words[0] {
|
||||
"global_tolerance_dist" if words.len() == 2 => {
|
||||
let v = number(1)?;
|
||||
tolerance = Some(v * v);
|
||||
}
|
||||
// The native loader reads but does not store these values.
|
||||
"global_tolerance_time"
|
||||
| "global_tolerance_speed"
|
||||
| "global_anticipation_delay"
|
||||
| "tolerance_time"
|
||||
if words.len() == 2 =>
|
||||
{
|
||||
number(1)?;
|
||||
}
|
||||
"pattern" if words.len() == 2 => patterns.push(Pattern {
|
||||
name: words[1].into(),
|
||||
points: Vec::new(),
|
||||
tolerance_squared: tolerance
|
||||
.ok_or_else(|| "PAT lacks global_tolerance_dist".to_owned())?,
|
||||
}),
|
||||
"tolerance_dist" if words.len() == 2 => {
|
||||
let v = number(1)?;
|
||||
patterns.last_mut().ok_or_else(fail)?.tolerance_squared = v * v;
|
||||
}
|
||||
"coord" if words.len() == 3 => {
|
||||
let point = [number(1)?, number(2)?];
|
||||
patterns.last_mut().ok_or_else(fail)?.points.push(point);
|
||||
}
|
||||
// Fail explicitly on polar or modified directives until their
|
||||
// transcendental implementation is provided. Stock skater.pat uses coord.
|
||||
_ => return Err(fail()),
|
||||
}
|
||||
}
|
||||
skate_core::input::gesture::Recognizer::new(patterns.clone())?;
|
||||
Ok(patterns)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn authored_order_and_per_pattern_tolerances_are_preserved() {
|
||||
let p = parse("global_tolerance_dist 0.4\nglobal_anticipation_delay 15\npattern Ollie\ntolerance_dist 0.55\ncoord 0 1\ncoord 0 -1\npattern Test\ncoord 1 0\ncoord -1 0").unwrap();
|
||||
assert_eq!(p[0].points, vec![[0.0, 1.0], [0.0, -1.0]]);
|
||||
assert_eq!(p[0].tolerance_squared, 0.55 * 0.55);
|
||||
assert_eq!(p[1].tolerance_squared, 0.4 * 0.4);
|
||||
assert!(parse("global_tolerance_dist 0.4\npattern Broken\ncoord 0 1").is_err());
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ pub mod animation_frames;
|
||||
pub mod animation_metadata;
|
||||
pub mod collections;
|
||||
pub mod input_config;
|
||||
pub mod gesture_patterns;
|
||||
pub mod physics_skeleton;
|
||||
mod sha256;
|
||||
pub mod state_graph;
|
||||
|
||||
@@ -45,6 +45,7 @@ pub struct ActionHost {
|
||||
/// JuiceHook instance+8 starts with the native null intent name. Its
|
||||
/// update removes that pending key and resets it,82BA39A8.
|
||||
juice_pending: Vec<String>,
|
||||
trick_handlers: Vec<crate::input::gesture_mapping::State>,
|
||||
}
|
||||
|
||||
impl ActionHost {
|
||||
@@ -109,6 +110,7 @@ impl ActionHost {
|
||||
body_flip: vec![body_flip_signal::State::default(); count],
|
||||
body_flip_settings: None,
|
||||
juice_pending: vec![String::new(); count],
|
||||
trick_handlers: vec![crate::input::gesture_mapping::State::default(); count],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,6 +196,7 @@ impl Host for ActionHost {
|
||||
self.time_handlers[behavior] = TimeMgIntent::default();
|
||||
self.body_flip[behavior] = body_flip_signal::State::default();
|
||||
self.juice_pending[behavior].clear();
|
||||
self.trick_handlers[behavior] = crate::input::gesture_mapping::State::default();
|
||||
let result = self.next_instance;
|
||||
self.next_instance = self.next_instance.wrapping_add(1).max(1);
|
||||
result
|
||||
@@ -202,6 +205,14 @@ impl Host for ActionHost {
|
||||
let Some(instance) = self.operation(behavior).cloned() else {
|
||||
return;
|
||||
};
|
||||
if let ActionOperation::CreateTrickIntentFromGesture { group, override_name } = &instance.operation {
|
||||
if let Some((_, mirrored)) = self.stance {
|
||||
self.trick_handlers[behavior].begin(*group, override_name.as_deref(), &self.action_intents, &mut self.motion_intents, mirrored);
|
||||
} else {
|
||||
self.errors.push("CreateTrickIntentFromGesture requires published skater stance".into());
|
||||
}
|
||||
return;
|
||||
}
|
||||
if instance.unsupported().is_some() {
|
||||
self.unsupported(behavior, &instance);
|
||||
return;
|
||||
@@ -250,6 +261,10 @@ impl Host for ActionHost {
|
||||
let Some(instance) = self.operation(behavior).cloned() else {
|
||||
return;
|
||||
};
|
||||
if matches!(instance.operation, ActionOperation::CreateTrickIntentFromGesture { .. }) {
|
||||
self.trick_handlers[behavior].update(&mut self.motion_intents);
|
||||
return;
|
||||
}
|
||||
if instance.unsupported().is_some() {
|
||||
self.unsupported(behavior, &instance);
|
||||
return;
|
||||
@@ -320,6 +335,10 @@ impl Host for ActionHost {
|
||||
let Some(instance) = self.operation(behavior).cloned() else {
|
||||
return;
|
||||
};
|
||||
if matches!(instance.operation, ActionOperation::CreateTrickIntentFromGesture { .. }) {
|
||||
self.trick_handlers[behavior].end(&mut self.motion_intents);
|
||||
return;
|
||||
}
|
||||
if let Some(name) = instance.config.mg_intent.as_deref() {
|
||||
let mutation = match instance.operation {
|
||||
ActionOperation::CreateConstMgIntent => self.const_handlers[behavior].end(),
|
||||
|
||||
@@ -73,7 +73,7 @@ pub enum ActionOperation {
|
||||
CreateMgTimeIntent,
|
||||
CreateConstMgIntent,
|
||||
BoardAdjust,
|
||||
CreateTrickIntentFromGesture,
|
||||
CreateTrickIntentFromGesture { group: crate::input::gesture_catalog::Group, override_name: Option<String> },
|
||||
JuiceHook,
|
||||
BodyFlippingSignal,
|
||||
/// Diagnostic presentation is owned by this host and has no intent effect.
|
||||
@@ -120,9 +120,6 @@ impl ActionFactory {
|
||||
ActionOperation::CreateConstMgIntent
|
||||
}
|
||||
(OperationKind::Behavior, "BoardAdjust") => ActionOperation::BoardAdjust,
|
||||
(OperationKind::Behavior, "CreateTrickIntentFromGesture") => {
|
||||
ActionOperation::CreateTrickIntentFromGesture
|
||||
}
|
||||
(OperationKind::Behavior, "JuiceHook") => ActionOperation::JuiceHook,
|
||||
(OperationKind::Behavior, "BodyFlippingSignal") => ActionOperation::BodyFlippingSignal,
|
||||
_ => ActionOperation::Unsupported {
|
||||
@@ -160,7 +157,12 @@ impl OperationFactory for ActionFactory {
|
||||
.unwrap_or_else(|| Self::operation(kind, name))
|
||||
}
|
||||
} else {
|
||||
if kind == OperationKind::Behavior && name == "PrintText2D" {
|
||||
if kind == OperationKind::Behavior && name == "CreateTrickIntentFromGesture" {
|
||||
ActionOperation::CreateTrickIntentFromGesture {
|
||||
group: crate::input::gesture_catalog::Group::parse(attributes.text("group").unwrap_or("Square")).map_err(ActionFactoryError)?,
|
||||
override_name: attributes.text("override").map(str::to_owned),
|
||||
}
|
||||
} else if kind == OperationKind::Behavior && name == "PrintText2D" {
|
||||
ActionOperation::PrintText(attributes.text("text").unwrap_or("").to_owned())
|
||||
} else {
|
||||
Self::operation(kind, name)
|
||||
@@ -200,10 +202,6 @@ impl ActionInstance {
|
||||
kind: OperationKind::Behavior,
|
||||
name: "BoardAdjust".into(),
|
||||
}),
|
||||
ActionOperation::CreateTrickIntentFromGesture => Some(UnsupportedOperation {
|
||||
kind: OperationKind::Behavior,
|
||||
name: "CreateTrickIntentFromGesture".into(),
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ pub(crate) mod motion_landing;
|
||||
mod motion_landing_execute;
|
||||
pub(crate) mod motion_native;
|
||||
mod motion_nodes;
|
||||
mod motion_tricks;
|
||||
#[path = "motion_offboard/push_off.rs"]
|
||||
pub(crate) mod motion_push_off;
|
||||
#[path = "motion_offboard/reset.rs"]
|
||||
|
||||
@@ -44,6 +44,7 @@ pub struct MotionPhysical {
|
||||
pub foot_frame: Option<PushFootFrame>,
|
||||
}
|
||||
enum Instance {
|
||||
Trick(i32),
|
||||
Stateless,
|
||||
IntentFilter(skate_core::animation::intent_filter::State),
|
||||
Landing(super::motion_landing::State),
|
||||
@@ -69,6 +70,7 @@ enum Instance {
|
||||
impl Instance {
|
||||
fn new(operation: &MotionOperation) -> Self {
|
||||
match operation {
|
||||
MotionOperation::Trick(_) => Self::Trick(0),
|
||||
MotionOperation::IntentFilter(_) => Self::IntentFilter(Default::default()),
|
||||
MotionOperation::Landing(_) => Self::Landing(Default::default()),
|
||||
MotionOperation::Wipeout(_) => Self::Wipeout(Default::default()),
|
||||
@@ -134,6 +136,8 @@ pub struct MotionHost {
|
||||
///SpecificCA4 bit26, getter8258F900/setter8258F910; reset clears it.
|
||||
pub is_power_sliding: bool,
|
||||
pub score_packet: super::motion_native::ScorePacket,
|
||||
pub(super) trick_requests: super::motion_tricks::Requests,
|
||||
pub(super) trick_height_settings: (bool, bool),
|
||||
pub action_intents: IntentMap,
|
||||
pub time_tags: Option<BTreeMap<String, f32>>,
|
||||
pub physical: Option<MotionPhysical>,
|
||||
@@ -223,6 +227,8 @@ impl MotionHost {
|
||||
slide_latch: set_turning::SlideLatch::default(),
|
||||
is_power_sliding: false,
|
||||
score_packet: super::motion_native::ScorePacket::default(),
|
||||
trick_requests: Default::default(),
|
||||
trick_height_settings: (data.boolean("anim_motion", "jumping", "use_gesture_speed")?, data.boolean("anim_motion", "jumping", "clamp_gesture_to_antic")?),
|
||||
action_intents: IntentMap::new(),
|
||||
time_tags: None,
|
||||
physical: None,
|
||||
@@ -372,3 +378,7 @@ mod slide_tests;
|
||||
#[cfg(test)]
|
||||
#[path = "tests/motion_bump.rs"]
|
||||
mod bump_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests/motion_tricks.rs"]
|
||||
mod trick_tests;
|
||||
@@ -15,6 +15,14 @@ impl MotionHost {
|
||||
.get(behavior)
|
||||
.ok_or("Unbound MotionGraph behavior")?;
|
||||
let operation = self.operations[id].clone();
|
||||
if let MotionOperation::Trick(operation) = operation {
|
||||
let Some(&Instance::Trick(mut updates)) = self.instances.get(behavior) else {
|
||||
return Err("Trick operation/instance mismatch".into());
|
||||
};
|
||||
let result = super::super::motion_tricks::execute(self, operation, &mut updates, phase);
|
||||
self.instances[behavior] = Instance::Trick(updates);
|
||||
return result;
|
||||
}
|
||||
if let MotionOperation::Slide(operation) = operation {
|
||||
return self.execute_slide(behavior, operation, frame, phase);
|
||||
}
|
||||
|
||||
@@ -40,6 +40,8 @@ pub enum GameplayCondition {
|
||||
CanEnterSlide { right: bool },
|
||||
GrabbingObject,
|
||||
Dark,
|
||||
UnderflipRequested,
|
||||
DarkCatchRequested,
|
||||
HandPlanting { state: u8, direction: u8 },
|
||||
EnteringSkitch,
|
||||
Skitching,
|
||||
@@ -60,6 +62,8 @@ impl GameplayCondition {
|
||||
| "CanEnterSlide"
|
||||
| "IsGrabbingObject"
|
||||
| "IsDark"
|
||||
| "IsUnderflipRequested"
|
||||
| "IsDarkCatchRequested"
|
||||
| "IsHandPlanting"
|
||||
| "IsEnteringSkitch"
|
||||
| "IsSkitching"
|
||||
@@ -81,6 +85,8 @@ impl GameplayCondition {
|
||||
},
|
||||
"IsGrabbingObject" => Self::GrabbingObject,
|
||||
"IsDark" => Self::Dark,
|
||||
"IsUnderflipRequested" => Self::UnderflipRequested,
|
||||
"IsDarkCatchRequested" => Self::DarkCatchRequested,
|
||||
"IsEnteringSkitch" => Self::EnteringSkitch,
|
||||
"IsSkitching" => Self::Skitching,
|
||||
"IsHandPlanting" => Self::HandPlanting {
|
||||
@@ -132,7 +138,7 @@ impl GameplayCondition {
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
Self::DroppingBoard | Self::Dark | Self::CanEnterSlide { .. } => return None,
|
||||
Self::DroppingBoard | Self::Dark | Self::UnderflipRequested | Self::DarkCatchRequested | Self::CanEnterSlide { .. } => return None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -149,6 +155,8 @@ impl GameplayCondition {
|
||||
Self::DroppingBoard => p.dropping_board || host.animation.channels.has("RetrieveBoard"),
|
||||
//82BA79A0 calls the specific MotionGraph getter8258FB68.
|
||||
Self::Dark => host.riding.dark,
|
||||
Self::UnderflipRequested => host.trick_requests.underflip,
|
||||
Self::DarkCatchRequested => host.trick_requests.dark_catch,
|
||||
Self::CanEnterSlide { right } => {
|
||||
//82BA6FE0 rejects RevertGround102, reverses the side for fakie,
|
||||
//then reads its start bit in the retained C84 slide packet.
|
||||
|
||||
@@ -79,6 +79,7 @@ impl Operation {
|
||||
/// this game.82595D08 sets a name and ORs a bit in the graph's score packet.
|
||||
#[derive(Default)]
|
||||
pub struct ScorePacket {
|
||||
pub trick: Option<skate_core::animation::output::attributes::AttributeName>,
|
||||
pub name: Option<u32>,
|
||||
pub flags: u32,
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ use skate_data::state_graph::{
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum MotionOperation {
|
||||
Trick(super::motion_tricks::Operation),
|
||||
Play(PlayAnimation),
|
||||
///TU3 vtable82309664: Begin/Update/End all point to the empty82B61BB8.
|
||||
PrintText2D,
|
||||
@@ -123,6 +124,7 @@ impl OperationFactory for MotionFactory {
|
||||
super::motion_air_leg::Operation::parse(a),
|
||||
)),
|
||||
"ClearTrickAttr" => Some(MotionOperation::ClearTrickAttr),
|
||||
"SetTrickHeight" | "SetTrickAttr" | "ScoringTrick" | "MonitorUnderflip" | "SetDark" | "UpdateIsWeightOnNose" => super::motion_tricks::Operation::parse(a).map(MotionOperation::Trick),
|
||||
"CharacterGesture" => Some(MotionOperation::CharacterGesture),
|
||||
"DisallowPumping" => Some(MotionOperation::DisallowPumping),
|
||||
"FakieHeadChannel" => Some(MotionOperation::FakieHeadChannel),
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
//! Original trick leaves. Registration strings, not community symbol guesses,
|
||||
//! identify SetTrickAttr82BC7848 and ScoringTrick82BCC030.
|
||||
use super::motion::MotionHost;
|
||||
use skate_core::animation::{
|
||||
output::attributes::AttributeName,
|
||||
playback_parameters::{AttributeSink, ParameterInputs, SettableAttribute},
|
||||
skeleton_input::name::encode,
|
||||
};
|
||||
use skate_data::state_graph::attributes::Attributes;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum Operation {
|
||||
Height {
|
||||
from: AttributeName,
|
||||
rename: AttributeName,
|
||||
value: Option<f32>,
|
||||
manual: bool,
|
||||
grind: bool,
|
||||
},
|
||||
Attribute(AttributeName),
|
||||
Scoring(AttributeName),
|
||||
MonitorUnderflip,
|
||||
Dark,
|
||||
WeightOnNose,
|
||||
}
|
||||
impl Operation {
|
||||
pub fn parse(a: &Attributes<'_>) -> Option<Self> {
|
||||
Some(match a.text("name")? {
|
||||
"SetTrickHeight" => Self::Height {
|
||||
from: encode(a.text("from").unwrap_or("").as_bytes()),
|
||||
rename: encode(a.text("rename").unwrap_or("").as_bytes()),
|
||||
value: a.get("setToValue").map(|v| f32::from_bits(v.float_bits)),
|
||||
manual: a.get("trickFromManual").is_some(),
|
||||
grind: a.get("trickFromGrind").is_some(),
|
||||
},
|
||||
"SetTrickAttr" => Self::Attribute(encode(a.text("trick").unwrap_or("").as_bytes())),
|
||||
"ScoringTrick" => Self::Scoring(encode(a.text("trick").unwrap_or("").as_bytes())),
|
||||
"MonitorUnderflip" => Self::MonitorUnderflip,
|
||||
"SetDark" => Self::Dark,
|
||||
"UpdateIsWeightOnNose" => Self::WeightOnNose,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Requests {
|
||||
pub underflip: bool,
|
||||
pub dark_catch: bool,
|
||||
}
|
||||
|
||||
pub fn execute(
|
||||
host: &mut MotionHost,
|
||||
operation: Operation,
|
||||
updates: &mut i32,
|
||||
phase: u8,
|
||||
) -> Result<(), String> {
|
||||
match operation {
|
||||
Operation::Height {
|
||||
from,
|
||||
rename,
|
||||
value,
|
||||
manual,
|
||||
grind,
|
||||
} if phase == 0 => {
|
||||
// 82BAEE90: graph intents override animation only on the authored paths.
|
||||
let gesture = host
|
||||
.animation
|
||||
.motion_intents
|
||||
.get("GestureSpeed")
|
||||
.copied()
|
||||
.unwrap_or(0.0);
|
||||
let height = if let Some(value) = value {
|
||||
value
|
||||
} else if let Some(value) = host.animation.motion_intents.get("TrickHeight") {
|
||||
*value
|
||||
} else {
|
||||
let mut height = host
|
||||
.animation
|
||||
.last_attribute(from)?
|
||||
.and_then(|a| a.payload.0[0])
|
||||
.map(f32::from_bits)
|
||||
.unwrap_or(0.0);
|
||||
if host.trick_height_settings.0 {
|
||||
height = if host.trick_height_settings.1 {
|
||||
height.min(gesture.max(0.0))
|
||||
} else {
|
||||
gesture
|
||||
};
|
||||
}
|
||||
height
|
||||
};
|
||||
// Independent packet output carries gesture speed, not selected height.
|
||||
host.animation.emit_packet(
|
||||
encode(b"JumpHeightOverride"),
|
||||
gesture
|
||||
* if manual {
|
||||
0.27
|
||||
} else if grind {
|
||||
1.0
|
||||
} else {
|
||||
0.2
|
||||
},
|
||||
);
|
||||
host.animation.set_attribute(SettableAttribute {
|
||||
name: rename,
|
||||
value: height,
|
||||
normalized: true,
|
||||
sequence_id: -1,
|
||||
});
|
||||
}
|
||||
Operation::Attribute(value) => {
|
||||
let name = encode(b"Trick");
|
||||
if phase == 0 {
|
||||
if let Some(entry) = host
|
||||
.animation
|
||||
.construction_values
|
||||
.iter_mut()
|
||||
.find(|e| e.0 == name)
|
||||
{
|
||||
entry.1 = value;
|
||||
} else {
|
||||
host.animation.construction_values.push((name, value));
|
||||
}
|
||||
} else if phase == 2
|
||||
&& host
|
||||
.animation
|
||||
.last_attribute(encode(b"defaultcyc"))?
|
||||
.is_some()
|
||||
{
|
||||
host.animation.construction_values.retain(|e| e.0 != name);
|
||||
}
|
||||
}
|
||||
Operation::Scoring(name) if phase == 1 => {
|
||||
// ISkaterMotionGraph236 ->8258FA20 stores both equal names and bit24.
|
||||
host.score_packet.trick = Some(name);
|
||||
host.score_packet.flags |= 0x0100_0000;
|
||||
}
|
||||
Operation::MonitorUnderflip => {
|
||||
if phase == 0 {
|
||||
*updates = 0;
|
||||
} else if phase == 2 {
|
||||
host.trick_requests = Requests::default();
|
||||
} else {
|
||||
*updates = updates.wrapping_add(1);
|
||||
if *updates > 1 {
|
||||
let intents = &host.animation.motion_intents;
|
||||
if [
|
||||
"U_L_F_Kickflip",
|
||||
"U_L_F_Heelflip",
|
||||
"U_L_B_Kickflip",
|
||||
"U_L_B_Heelflip",
|
||||
"U_Nollie",
|
||||
"U_Fingerflip",
|
||||
"U_Kickflip",
|
||||
"U_Heelflip",
|
||||
"U_N_Kickflip",
|
||||
"U_N_Heelflip",
|
||||
]
|
||||
.iter()
|
||||
.any(|n| intents.contains_key(n))
|
||||
{
|
||||
host.trick_requests.underflip = true;
|
||||
} else if intents.contains_key("DarkCatch") {
|
||||
host.trick_requests.dark_catch = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Operation::Dark => {
|
||||
host.riding.dark = phase != 2;
|
||||
if phase == 1 {
|
||||
host.animation.emit_packet(encode(b"IsDark"), 1.0);
|
||||
}
|
||||
}
|
||||
Operation::WeightOnNose if phase != 1 => {
|
||||
//82BB2670/26D0 -> ISkaterAnim20 ->82B97110, bit28.
|
||||
let flags = host
|
||||
.animation
|
||||
.skater_animation_flags
|
||||
.as_mut()
|
||||
.ok_or("UpdateIsWeightOnNose requires live ISkaterAnim")?;
|
||||
*flags = (*flags & !0x1000_0000) | if phase == 0 { 0x1000_0000 } else { 0 };
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
use super::*;
|
||||
use skate_core::animation::playback_tree::Evaluation;
|
||||
#[test]
|
||||
#[ignore = "requires private stock graphs and animation banks; no gameplay simulation"]
|
||||
fn stock_360_crouch_takeoff_and_air_animation_pipeline() {
|
||||
use crate::{animation_pose::PoseEvaluator, graph_runtime::CompiledGraph};
|
||||
use skate_data::{
|
||||
animation_banks::AnimationBanks,
|
||||
state_graph::{StateGraph, binding::Binding},
|
||||
};
|
||||
let root = std::path::PathBuf::from(
|
||||
std::env::var_os("SKATE3_ASSET_ROOT").expect("Set SKATE3_ASSET_ROOT"),
|
||||
);
|
||||
let banks = AnimationBanks::load(&root).unwrap();
|
||||
let evaluator = PoseEvaluator::from_banks(&banks).unwrap();
|
||||
let source =
|
||||
StateGraph::load(&root.join("private/stock/data/state/MotionGraph_OnBoard.stategraph"))
|
||||
.unwrap();
|
||||
let binding = Binding::from_graph(&source).unwrap();
|
||||
let runtime = CompiledGraph::from_binding(&binding).unwrap();
|
||||
let graph = LoadedGraph {
|
||||
source,
|
||||
binding,
|
||||
runtime,
|
||||
};
|
||||
let collections = Collections::load(&root).unwrap();
|
||||
let mut host = MotionHost::from_graph(
|
||||
&graph,
|
||||
&collections,
|
||||
banks.metadata().unwrap(),
|
||||
PlaybackContext {
|
||||
is_switch: Some(false),
|
||||
is_mirrored: Some(false),
|
||||
board_available: Some(true),
|
||||
pro_skater: encode(b""),
|
||||
transition_override: None,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
host.animation
|
||||
.set_hierarchy(
|
||||
&evaluator.frames.bone_names,
|
||||
&evaluator.frames.mirror_indices,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let frame = Frame {
|
||||
dt: 1.0 / 60.0,
|
||||
current: None,
|
||||
last: None,
|
||||
state_times: Vec::new(),
|
||||
};
|
||||
let evaluation = Evaluation {
|
||||
cull_threshold: 0.01,
|
||||
update_history: true,
|
||||
};
|
||||
let find = |suffix: &str, operation: &str| -> usize {
|
||||
use skate_data::state_graph::binding::Node;
|
||||
graph
|
||||
.binding
|
||||
.operations
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find_map(|(id, source)| {
|
||||
let Node::State(mut state) = source.parent else {
|
||||
return None;
|
||||
};
|
||||
let mut names = Vec::new();
|
||||
loop {
|
||||
names.push(graph.binding.states[state].name.clone());
|
||||
let Some(parent) = graph.binding.states[state].parent else {
|
||||
break;
|
||||
};
|
||||
state = parent;
|
||||
}
|
||||
names.reverse();
|
||||
if !names.join(".").ends_with(suffix) {
|
||||
return None;
|
||||
}
|
||||
let runtime = host.remap.behaviors.iter().position(|&op| op == id)?;
|
||||
let matches = match (&host.operations[id], operation) {
|
||||
(MotionOperation::Play(_), "PlayAnimation") => true,
|
||||
(
|
||||
MotionOperation::Trick(super::super::motion_tricks::Operation::Height {
|
||||
..
|
||||
}),
|
||||
"SetTrickHeight",
|
||||
) => true,
|
||||
(
|
||||
MotionOperation::Trick(super::super::motion_tricks::Operation::Attribute(
|
||||
_,
|
||||
)),
|
||||
"SetTrickAttr",
|
||||
) => true,
|
||||
(
|
||||
MotionOperation::Trick(super::super::motion_tricks::Operation::Scoring(_)),
|
||||
"ScoringTrick",
|
||||
) => true,
|
||||
(
|
||||
MotionOperation::Trick(
|
||||
super::super::motion_tricks::Operation::MonitorUnderflip,
|
||||
),
|
||||
"MonitorUnderflip",
|
||||
) => true,
|
||||
_ => false,
|
||||
};
|
||||
matches.then_some(runtime)
|
||||
})
|
||||
.unwrap_or_else(|| panic!("Missing {suffix}/{operation}"))
|
||||
};
|
||||
let crouch = find("Anticipation.AnticInto.AnticTail", "PlayAnimation");
|
||||
let scoop = find(
|
||||
"Anticipation.AnticCyc.360PopShuvitAnticCycle",
|
||||
"PlayAnimation",
|
||||
);
|
||||
let ground = find("TailTrick.360Flip.Takeoff.FromAntic", "PlayAnimation");
|
||||
let height = find("TailTrick.360Flip.Takeoff.FromAntic", "SetTrickHeight");
|
||||
let air = find("TailTrick.360Flip.LeftGround", "PlayAnimation");
|
||||
let attr = find("TailTrick.360Flip", "SetTrickAttr");
|
||||
let score = find("TailTrick.360Flip", "ScoringTrick");
|
||||
let monitor = find("TailTrick.360Flip", "MonitorUnderflip");
|
||||
for mirrored in [false, true] {
|
||||
host.animation.reset_from_stock();
|
||||
host.animation.skater_animation_flags = Some(if mirrored { 0x40000000 } else { 0 });
|
||||
host.playback_context.is_mirrored = Some(mirrored);
|
||||
host.animation.filtered_intents.insert("Compression", 1.0);
|
||||
for (behavior, duration) in [(crouch, 0.25), (scoop, 0.5)] {
|
||||
host.execute(behavior, &frame, 0).unwrap();
|
||||
host.animation.apply_parameters().unwrap();
|
||||
host.animation.advance(duration, 0.0);
|
||||
host.animation.refresh_tree_attributes().unwrap();
|
||||
let pose = evaluator
|
||||
.evaluate(&host.animation.evaluate_pose(evaluation).unwrap())
|
||||
.unwrap();
|
||||
assert!(
|
||||
pose.iter()
|
||||
.flat_map(|b| b.rotation.iter().chain(&b.translation))
|
||||
.all(|v| v.is_finite())
|
||||
);
|
||||
}
|
||||
let antic = host
|
||||
.animation
|
||||
.last_attribute(encode(b"AnticStrength"))
|
||||
.unwrap()
|
||||
.expect("stock crouch must supply AnticStrength");
|
||||
assert!(f32::from_bits(antic.payload.0[0].unwrap()) > 0.0);
|
||||
host.animation.motion_intents.insert("GestureSpeed", 1.0);
|
||||
for behavior in [attr, score, monitor, ground, height] {
|
||||
host.execute(behavior, &frame, 0).unwrap();
|
||||
}
|
||||
host.execute(score, &frame, 1).unwrap();
|
||||
assert_eq!(host.score_packet.trick, Some(encode(b"360Flip")));
|
||||
assert!(
|
||||
host.animation
|
||||
.construction_values
|
||||
.contains(&(encode(b"Trick"), encode(b"360Flip")))
|
||||
);
|
||||
host.animation.apply_parameters().unwrap();
|
||||
host.animation.advance(0.1, 0.0);
|
||||
host.animation.refresh_tree_attributes().unwrap();
|
||||
assert!(
|
||||
host.animation
|
||||
.last_attribute(encode(b"TrickHeight"))
|
||||
.unwrap()
|
||||
.is_some()
|
||||
);
|
||||
let pose = evaluator
|
||||
.evaluate(&host.animation.evaluate_pose(evaluation).unwrap())
|
||||
.unwrap();
|
||||
assert!(
|
||||
pose.iter()
|
||||
.flat_map(|b| b.rotation.iter().chain(&b.translation))
|
||||
.all(|v| v.is_finite())
|
||||
);
|
||||
host.execute(air, &frame, 0).unwrap();
|
||||
host.animation.apply_parameters().unwrap();
|
||||
host.animation.advance(0.1, 0.0);
|
||||
host.animation.refresh_tree_attributes().unwrap();
|
||||
let pose = evaluator
|
||||
.evaluate(&host.animation.evaluate_pose(evaluation).unwrap())
|
||||
.unwrap();
|
||||
assert!(
|
||||
pose.iter()
|
||||
.flat_map(|b| b.rotation.iter().chain(&b.translation))
|
||||
.all(|v| v.is_finite())
|
||||
);
|
||||
host.execute(monitor, &frame, 1).unwrap();
|
||||
assert!(!host.trick_requests.dark_catch);
|
||||
host.execute(monitor, &frame, 2).unwrap();
|
||||
}
|
||||
println!(
|
||||
"Stock crouch, scoop, B_360FLIP_G -> B_360FLIP_A, attributes and finite poses checked in both stances"
|
||||
);
|
||||
}
|
||||
@@ -4,6 +4,9 @@ use bevy::prelude::*;
|
||||
|
||||
mod controllers;
|
||||
pub(crate) mod gesture_catalog;
|
||||
mod gesture_mapping_data;
|
||||
pub(crate) mod gesture_mapping;
|
||||
pub(crate) mod gesture_input;
|
||||
mod platform;
|
||||
pub(crate) use controllers::{ControllerInput, ControllerStatus};
|
||||
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
//! GameInputManager82696030/826962D8 -> listener8259B878/8259B9D0 ->
|
||||
//! Fill8259B1F0..B7D8. Runs on the existing 60 Hz gameplay input boundary.
|
||||
use skate_core::{
|
||||
graph::intents::IntentMap,
|
||||
input::gesture::{Recognizer, Settings},
|
||||
};
|
||||
use skate_data::collections::Collections;
|
||||
use std::path::Path;
|
||||
|
||||
pub(crate) struct GestureInput {
|
||||
recognizers: Vec<(usize, Recognizer)>,
|
||||
maximum_misses: [u8; 2],
|
||||
held_pattern: Option<String>,
|
||||
}
|
||||
impl GestureInput {
|
||||
pub fn load(root: &Path) -> Result<Self, String> {
|
||||
let data = Collections::load(root)?;
|
||||
let misses = |key| -> Result<u8, String> {
|
||||
let field = data.field("recognizer", key, "NumTicksPatternNotInRangeBeforeCulling")?;
|
||||
if field.type_name != "EA::Reflection::UInt8" {
|
||||
return Err("Recognizer culling field must be UInt8".into());
|
||||
}
|
||||
u8::from_str_radix(&field.data, 16).map_err(|e| e.to_string())
|
||||
};
|
||||
let mut recognizers = Vec::new();
|
||||
// Original constructor82695A68 insertion order; each file competes
|
||||
// internally, and all winners are delivered to the listener in order.
|
||||
for (stick, file) in [
|
||||
(1, "skater.pat"),
|
||||
(1, "skater90.pat"),
|
||||
(1, "skaterN90.pat"),
|
||||
(1, "skater_air.pat"),
|
||||
(1, "skater_fingerflip.pat"),
|
||||
(0, "skaterls.pat"),
|
||||
(0, "skaterstep.pat"),
|
||||
] {
|
||||
recognizers.push((
|
||||
stick,
|
||||
Recognizer::new(skate_data::gesture_patterns::load(
|
||||
&root.join("private/stock/data/joystick").join(file),
|
||||
)?)?,
|
||||
));
|
||||
}
|
||||
Ok(Self {
|
||||
recognizers,
|
||||
maximum_misses: [misses("left_stick")?, misses("right_stick")?],
|
||||
held_pattern: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn publish(
|
||||
&mut self,
|
||||
axes: [[f32; 2]; 2],
|
||||
difficulty: u32,
|
||||
flags: u32,
|
||||
physical_state: u32,
|
||||
ag: &mut IntentMap,
|
||||
) {
|
||||
// Native manager negates mapped Y. Component deadzone0.1 is initialized
|
||||
// by82F75F60 from820641A8, separately from cInputMap's own deadzones.
|
||||
let samples = axes.map(|[x, y]| [x, -y].map(|v| if v.abs() < 0.1 { 0.0 } else { v }));
|
||||
let mut events = Vec::new();
|
||||
let mut held = false;
|
||||
for stick in [1, 0] {
|
||||
// Held checks precede *all* matches on this stick.
|
||||
for (_, recognizer) in self.recognizers.iter_mut().filter(|r| r.0 == stick) {
|
||||
if let Some(index) = recognizer.held(samples[stick]) {
|
||||
if self.held_pattern.as_deref().is_some_and(|name| {
|
||||
name.eq_ignore_ascii_case(&recognizer.patterns()[index].name)
|
||||
}) {
|
||||
held = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (_, recognizer) in self.recognizers.iter_mut().filter(|r| r.0 == stick) {
|
||||
if let Some(result) = recognizer.sample(
|
||||
samples[stick],
|
||||
Settings {
|
||||
maximum_misses: self.maximum_misses[stick],
|
||||
difficulty,
|
||||
},
|
||||
) {
|
||||
let name = recognizer.patterns()[result.pattern].name.clone();
|
||||
// Listener retains only these four hold-capable events.
|
||||
if ["Kickflip", "Heelflip", "N_Kickflip", "N_Heelflip"].contains(&name.as_str())
|
||||
{
|
||||
self.held_pattern = Some(name.clone());
|
||||
}
|
||||
events.push((name, result.strength));
|
||||
}
|
||||
}
|
||||
}
|
||||
if held {
|
||||
ag.insert("HoldPattern", 1.0);
|
||||
}
|
||||
for (name, strength) in events {
|
||||
if permitted(&name, flags, physical_state, ag) {
|
||||
ag.insert("Trick", 1.0);
|
||||
ag.insert(&name, 1.0);
|
||||
ag.insert("GestureSpeed", strength);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn permitted(name: &str, flags: u32, state: u32, ag: &IntentMap) -> bool {
|
||||
let bit = |n: u32| (flags & (1u32 << n)) != 0u32;
|
||||
let member = |names: &[&str]| names.iter().any(|n| n.eq_ignore_ascii_case(name));
|
||||
if bit(5) && member(&["FingerFlip", "FS_Varial", "BS_Varial"]) {
|
||||
return false;
|
||||
}
|
||||
if bit(4)
|
||||
&& (matches!(state, 200 | 201)
|
||||
|| member(&[
|
||||
"L_F_Kickflip",
|
||||
"L_B_Kickflip",
|
||||
"L_F_Heelflip",
|
||||
"L_B_Heelflip",
|
||||
"L_FS_Shuvit",
|
||||
"L_BS_Shuvit",
|
||||
]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if member(&["FrontFlip", "BackFlip"])
|
||||
&& (!(ag.contains_key("RightAirGrab") || ag.contains_key("LeftAirGrab"))
|
||||
|| ag.contains_key("LeftPush")
|
||||
|| ag.contains_key("RightPush")
|
||||
|| bit(3))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if bit(8) && member(&["SlideFs180", "SlideBs180"]) {
|
||||
return false;
|
||||
}
|
||||
if bit(12)
|
||||
&& !member(&[
|
||||
"Ollie",
|
||||
"Kickflip",
|
||||
"Heelflip",
|
||||
"Hardflip",
|
||||
"InwardHeelflip",
|
||||
"VarialKickflip",
|
||||
"VarialHeelflip",
|
||||
"PopShuvit",
|
||||
"FSPopShuvit",
|
||||
"360PopShuvit",
|
||||
"FS360PopShuvit",
|
||||
])
|
||||
{
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
#[ignore = "requires private stock joystick and collection data"]
|
||||
fn stock_360_gesture_reaches_native_square_mapping() {
|
||||
let root = std::path::PathBuf::from(
|
||||
std::env::var_os("SKATE3_ASSET_ROOT").expect("SKATE3_ASSET_ROOT"),
|
||||
);
|
||||
let mut input = GestureInput::load(&root).unwrap();
|
||||
let mut ag = IntentMap::new();
|
||||
let points = input.recognizers[0]
|
||||
.1
|
||||
.patterns()
|
||||
.iter()
|
||||
.find(|p| p.name == "360Flip")
|
||||
.unwrap()
|
||||
.points
|
||||
.clone();
|
||||
input.publish([[0.0; 2]; 2], 0, 0, 100, &mut ag);
|
||||
for _ in 0..30 {
|
||||
ag.clear();
|
||||
input.publish(
|
||||
[[0.0; 2], [points[0][0], -points[0][1]]],
|
||||
0,
|
||||
0,
|
||||
100,
|
||||
&mut ag,
|
||||
);
|
||||
assert!(!ag.contains_key("Trick"));
|
||||
}
|
||||
for point in &points[1..] {
|
||||
ag.clear();
|
||||
input.publish([[0.0; 2], [point[0], -point[1]]], 0, 0, 100, &mut ag);
|
||||
}
|
||||
assert!(ag.contains_key("360Flip"), "{ag:?}");
|
||||
assert_eq!(
|
||||
super::super::gesture_mapping::select(
|
||||
super::super::gesture_catalog::Group::Square,
|
||||
&ag,
|
||||
false
|
||||
),
|
||||
Some("360Flip")
|
||||
);
|
||||
assert_eq!(ag.get("GestureSpeed"), Some(&1.0));
|
||||
println!("Authored scoop through all seven PAT recognizers: {ag:?}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
//! GestureTrickMapping82BA07F0 and CreateTrickIntentFromGesture82BA1C30..2228.
|
||||
use super::{gesture_catalog::Group, gesture_mapping_data::TABLES};
|
||||
use skate_core::{animation::playback_parameters::intent_key, graph::intents::IntentMap};
|
||||
|
||||
pub(crate) fn select(group: Group, intents: &IntentMap, mirrored: bool) -> Option<&'static str> {
|
||||
let rows = TABLES[group as usize];
|
||||
// 82BC0D90 uses greatest prime <=46, then maximum load10000.
|
||||
// 82BC1BE8 prepends entries; 82BA07F0 visits ascending buckets.
|
||||
let mut selected = None;
|
||||
let mut bucket = usize::MAX;
|
||||
for row in rows.iter().rev() {
|
||||
let current = intent_key(row.0)
|
||||
.iter()
|
||||
.fold(0u32, |sum, word| sum.wrapping_add(*word)) as usize
|
||||
% 43;
|
||||
if current < bucket && intents.contains_key(row.0) {
|
||||
selected = Some(row);
|
||||
bucket = current;
|
||||
}
|
||||
}
|
||||
let row = selected?;
|
||||
let mirrored = mirrored && !intents.contains_key("DontMirrorTrick");
|
||||
Some(if intents.contains_key("DarkCatch") && row.3 != 0 {
|
||||
match (row.3, mirrored) {
|
||||
(2, false) | (1, true) => "Heelflip",
|
||||
(3, true) | (4, false) => "N_Heelflip",
|
||||
(3, false) | (4, true) => "N_Kickflip",
|
||||
_ => "Kickflip",
|
||||
}
|
||||
} else if mirrored {
|
||||
row.2
|
||||
} else {
|
||||
row.1
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct State {
|
||||
first_update: bool,
|
||||
selected: Option<String>,
|
||||
successor: Option<&'static str>,
|
||||
}
|
||||
impl Default for State {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
first_update: true,
|
||||
selected: None,
|
||||
successor: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl State {
|
||||
pub fn begin(
|
||||
&mut self,
|
||||
group: Group,
|
||||
override_name: Option<&str>,
|
||||
ag: &IntentMap,
|
||||
mg: &mut IntentMap,
|
||||
mirrored: bool,
|
||||
) {
|
||||
let Some(name) = select(group, ag, mirrored) else {
|
||||
return;
|
||||
};
|
||||
let name = override_name.unwrap_or(name);
|
||||
self.selected = Some(name.to_owned());
|
||||
self.successor = match name {
|
||||
"Kickflip" => Some("KickflipHold"),
|
||||
"Heelflip" => Some("HeelflipHold"),
|
||||
"N_Kickflip" => Some("N_KickflipHold"),
|
||||
"N_Heelflip" => Some("N_HeelflipHold"),
|
||||
_ => None,
|
||||
};
|
||||
mg.insert(name, 0.0);
|
||||
}
|
||||
pub fn update(&mut self, mg: &mut IntentMap) {
|
||||
if !self.first_update {
|
||||
if let (Some(name), Some(successor)) = (&self.selected, self.successor) {
|
||||
mg.remove(name);
|
||||
mg.insert(successor, 0.0);
|
||||
}
|
||||
}
|
||||
self.first_update = false;
|
||||
}
|
||||
pub fn end(&mut self, mg: &mut IntentMap) {
|
||||
if let Some(name) = &self.selected {
|
||||
if let Some(successor) = self.successor {
|
||||
mg.remove(successor);
|
||||
}
|
||||
mg.remove(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn tre_flip_uses_original_mirror_mapping_and_presence_not_strength() {
|
||||
let mut ag = IntentMap::new();
|
||||
ag.insert("360Flip", 0.0);
|
||||
assert_eq!(select(Group::Square, &ag, false), Some("360Flip"));
|
||||
assert_eq!(select(Group::Square, &ag, true), Some("Laserflip"));
|
||||
ag.insert("DontMirrorTrick", 0.0);
|
||||
assert_eq!(select(Group::Square, &ag, true), Some("360Flip"));
|
||||
}
|
||||
#[test]
|
||||
fn first_update_retains_launch_intent_then_successor_is_cleaned_on_exit() {
|
||||
let mut ag = IntentMap::new();
|
||||
ag.insert("Kickflip", 1.0);
|
||||
let mut mg = IntentMap::new();
|
||||
let mut state = State::default();
|
||||
state.begin(Group::Square, None, &ag, &mut mg, false);
|
||||
state.update(&mut mg);
|
||||
assert!(mg.contains_key("Kickflip"));
|
||||
state.update(&mut mg);
|
||||
assert!(!mg.contains_key("Kickflip"));
|
||||
assert!(mg.contains_key("KickflipHold"));
|
||||
state.end(&mut mg);
|
||||
assert!(mg.is_empty());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,413 @@
|
||||
//! TU3 82B98F70 records, re-extracted from original bytes and all 270 map inserts.
|
||||
// Each tuple is (gesture key, normal intent, mirrored intent, dark-catch category).
|
||||
pub(super) const TABLES: [&[(&str, &str, &str, u32)]; 7] = [
|
||||
&[
|
||||
("Ollie", "Ollie", "Ollie", 1),
|
||||
("PopShuvit", "PopShuvit", "FSPopShuvit", 1),
|
||||
("FSPopShuvit", "FSPopShuvit", "PopShuvit", 2),
|
||||
("VarialKickflip", "VarialKickflip", "VarialHeelflip", 1),
|
||||
("VarialHeelflip", "VarialHeelflip", "VarialKickflip", 2),
|
||||
("Hardflip", "Hardflip", "InwardHeelflip", 1),
|
||||
("InwardHeelflip", "InwardHeelflip", "Hardflip", 2),
|
||||
("360PopShuvit", "360PopShuvit", "FS360PopShuvit", 1),
|
||||
("FS360PopShuvit", "FS360PopShuvit", "360PopShuvit", 2),
|
||||
("360Flip", "360Flip", "Laserflip", 0),
|
||||
("Laserflip", "Laserflip", "360Flip", 0),
|
||||
("360Hardflip", "360Hardflip", "360InwardHeelflip", 0),
|
||||
("360InwardHeelflip", "360InwardHeelflip", "360Hardflip", 0),
|
||||
("Kickflip", "Kickflip", "Heelflip", 0),
|
||||
("Heelflip", "Heelflip", "Kickflip", 0),
|
||||
("Nollie", "Nollie", "Nollie", 3),
|
||||
("N_PopShuvit", "N_PopShuvit", "N_FSPopShuvit", 3),
|
||||
("N_FSPopShuvit", "N_FSPopShuvit", "N_PopShuvit", 4),
|
||||
(
|
||||
"N_VarialKickflip",
|
||||
"N_VarialKickflip",
|
||||
"N_VarialHeelflip",
|
||||
3,
|
||||
),
|
||||
(
|
||||
"N_VarialHeelflip",
|
||||
"N_VarialHeelflip",
|
||||
"N_VarialKickflip",
|
||||
4,
|
||||
),
|
||||
("N_Hardflip", "N_Hardflip", "N_InwardHeelflip", 3),
|
||||
("N_InwardHeelflip", "N_InwardHeelflip", "N_Hardflip", 4),
|
||||
("N_360PopShuvit", "N_360PopShuvit", "N_FS360PopShuvit", 3),
|
||||
("N_FS360PopShuvit", "N_FS360PopShuvit", "N_360PopShuvit", 4),
|
||||
("N_360Flip", "N_360Flip", "N_Laserflip", 0),
|
||||
("N_Laserflip", "N_Laserflip", "N_360Flip", 0),
|
||||
("N_360Hardflip", "N_360Hardflip", "N_360InwardHeelflip", 0),
|
||||
(
|
||||
"N_360InwardHeelflip",
|
||||
"N_360InwardHeelflip",
|
||||
"N_360Hardflip",
|
||||
0,
|
||||
),
|
||||
("N_Kickflip", "N_Kickflip", "N_Heelflip", 0),
|
||||
("N_Heelflip", "N_Heelflip", "N_Kickflip", 0),
|
||||
],
|
||||
&[
|
||||
("Nollie", "Nollie", "Nollie", 3),
|
||||
("N_PopShuvit", "N_PopShuvit", "N_FSPopShuvit", 3),
|
||||
("N_FSPopShuvit", "N_FSPopShuvit", "N_PopShuvit", 4),
|
||||
(
|
||||
"N_VarialKickflip",
|
||||
"N_VarialKickflip",
|
||||
"N_VarialHeelflip",
|
||||
3,
|
||||
),
|
||||
(
|
||||
"N_VarialHeelflip",
|
||||
"N_VarialHeelflip",
|
||||
"N_VarialKickflip",
|
||||
4,
|
||||
),
|
||||
("N_Hardflip", "N_Hardflip", "N_InwardHeelflip", 3),
|
||||
("N_InwardHeelflip", "N_InwardHeelflip", "N_Hardflip", 4),
|
||||
("N_360PopShuvit", "N_360PopShuvit", "N_FS360PopShuvit", 3),
|
||||
("N_FS360PopShuvit", "N_FS360PopShuvit", "N_360PopShuvit", 4),
|
||||
("N_360Flip", "N_360Flip", "N_Laserflip", 0),
|
||||
("N_Laserflip", "N_Laserflip", "N_360Flip", 0),
|
||||
("N_360Hardflip", "N_360Hardflip", "N_360InwardHeelflip", 0),
|
||||
(
|
||||
"N_360InwardHeelflip",
|
||||
"N_360InwardHeelflip",
|
||||
"N_360Hardflip",
|
||||
0,
|
||||
),
|
||||
("N_Kickflip", "N_Kickflip", "N_Heelflip", 0),
|
||||
("N_Heelflip", "N_Heelflip", "N_Kickflip", 0),
|
||||
("Ollie", "Nollie", "Nollie", 3),
|
||||
("PopShuvit", "N_PopShuvit", "N_FSPopShuvit", 3),
|
||||
("FSPopShuvit", "N_FSPopShuvit", "N_PopShuvit", 4),
|
||||
("VarialKickflip", "Nollie", "Nollie", 3),
|
||||
("VarialHeelflip", "Nollie", "Nollie", 4),
|
||||
("Hardflip", "Nollie", "Nollie", 3),
|
||||
("InwardHeelflip", "Nollie", "Nollie", 4),
|
||||
("360PopShuvit", "N_PopShuvit", "N_FSPopShuvit", 3),
|
||||
("FS360PopShuvit", "N_FSPopShuvit", "N_PopShuvit", 4),
|
||||
("360Flip", "Nollie", "Nollie", 3),
|
||||
("Laserflip", "Nollie", "Nollie", 4),
|
||||
("360Hardflip", "Nollie", "Nollie", 3),
|
||||
("360InwardHeelflip", "Nollie", "Nollie", 4),
|
||||
("Kickflip", "Nollie", "Nollie", 3),
|
||||
("Heelflip", "Nollie", "Nollie", 4),
|
||||
],
|
||||
&[
|
||||
("Ollie", "Ollie", "Ollie", 1),
|
||||
("PopShuvit", "PopShuvit", "FSPopShuvit", 1),
|
||||
("FSPopShuvit", "FSPopShuvit", "PopShuvit", 2),
|
||||
("VarialKickflip", "VarialKickflip", "VarialHeelflip", 1),
|
||||
("VarialHeelflip", "VarialHeelflip", "VarialKickflip", 2),
|
||||
("Hardflip", "Hardflip", "InwardHeelflip", 1),
|
||||
("InwardHeelflip", "InwardHeelflip", "Hardflip", 2),
|
||||
("360PopShuvit", "360PopShuvit", "FS360PopShuvit", 1),
|
||||
("FS360PopShuvit", "FS360PopShuvit", "360PopShuvit", 2),
|
||||
("360Flip", "360Flip", "Laserflip", 0),
|
||||
("Laserflip", "Laserflip", "360Flip", 0),
|
||||
("360Hardflip", "360Hardflip", "360InwardHeelflip", 0),
|
||||
("360InwardHeelflip", "360InwardHeelflip", "360Hardflip", 0),
|
||||
("Kickflip", "Kickflip", "Heelflip", 0),
|
||||
("Heelflip", "Heelflip", "Kickflip", 0),
|
||||
("Nollie", "Ollie", "Ollie", 1),
|
||||
("N_PopShuvit", "PopShuvit", "FSPopShuvit", 1),
|
||||
("N_FSPopShuvit", "FSPopShuvit", "PopShuvit", 2),
|
||||
("N_VarialKickflip", "Ollie", "Ollie", 1),
|
||||
("N_VarialHeelflip", "Ollie", "Ollie", 2),
|
||||
("N_Hardflip", "Ollie", "Ollie", 1),
|
||||
("N_InwardHeelflip", "Ollie", "Ollie", 2),
|
||||
("N_360PopShuvit", "PopShuvit", "FSPopShuvit", 1),
|
||||
("N_FS360PopShuvit", "FSPopShuvit", "PopShuvit", 2),
|
||||
("N_360Flip", "Ollie", "Ollie", 1),
|
||||
("N_Laserflip", "Ollie", "Ollie", 2),
|
||||
("N_360Hardflip", "Ollie", "Ollie", 1),
|
||||
("N_360InwardHeelflip", "Ollie", "Ollie", 2),
|
||||
("N_Kickflip", "Ollie", "Ollie", 1),
|
||||
("N_Heelflip", "Ollie", "Ollie", 2),
|
||||
],
|
||||
&[
|
||||
("90_Nollie", "Nollie", "Nollie", 3),
|
||||
("90_N_PopShuvit", "N_PopShuvit", "N_FSPopShuvit", 3),
|
||||
("90_N_FSPopShuvit", "N_FSPopShuvit", "N_PopShuvit", 4),
|
||||
(
|
||||
"90_N_VarialKickflip",
|
||||
"N_VarialKickflip",
|
||||
"N_VarialHeelflip",
|
||||
3,
|
||||
),
|
||||
(
|
||||
"90_N_VarialHeelflip",
|
||||
"N_VarialHeelflip",
|
||||
"N_VarialKickflip",
|
||||
4,
|
||||
),
|
||||
("90_N_Hardflip", "N_Hardflip", "N_InwardHeelflip", 3),
|
||||
("90_N_InwardHeelflip", "N_InwardHeelflip", "N_Hardflip", 4),
|
||||
("90_N_360PopShuvit", "N_360PopShuvit", "N_FS360PopShuvit", 3),
|
||||
(
|
||||
"90_N_FS360PopShuvit",
|
||||
"N_FS360PopShuvit",
|
||||
"N_360PopShuvit",
|
||||
4,
|
||||
),
|
||||
("90_N_360Flip", "N_360Flip", "N_Laserflip", 0),
|
||||
("90_N_Laserflip", "N_Laserflip", "N_360Flip", 0),
|
||||
(
|
||||
"90_N_360Hardflip",
|
||||
"N_360Hardflip",
|
||||
"N_360InwardHeelflip",
|
||||
0,
|
||||
),
|
||||
(
|
||||
"90_N_360InwardHeelflip",
|
||||
"N_360InwardHeelflip",
|
||||
"N_360Hardflip",
|
||||
0,
|
||||
),
|
||||
("90_N_Kickflip", "N_Kickflip", "N_Heelflip", 0),
|
||||
("90_N_Heelflip", "N_Heelflip", "N_Kickflip", 0),
|
||||
("Nollie", "Nollie", "Nollie", 3),
|
||||
("N_PopShuvit", "N_PopShuvit", "N_FSPopShuvit", 3),
|
||||
("N_FSPopShuvit", "N_FSPopShuvit", "N_PopShuvit", 4),
|
||||
(
|
||||
"N_VarialKickflip",
|
||||
"N_VarialKickflip",
|
||||
"N_VarialHeelflip",
|
||||
3,
|
||||
),
|
||||
(
|
||||
"N_VarialHeelflip",
|
||||
"N_VarialHeelflip",
|
||||
"N_VarialKickflip",
|
||||
4,
|
||||
),
|
||||
("N_Hardflip", "N_Hardflip", "N_InwardHeelflip", 3),
|
||||
("N_InwardHeelflip", "N_InwardHeelflip", "N_Hardflip", 4),
|
||||
("N_360PopShuvit", "N_360PopShuvit", "N_FS360PopShuvit", 3),
|
||||
("N_FS360PopShuvit", "N_FS360PopShuvit", "N_360PopShuvit", 4),
|
||||
("N_360Flip", "N_360Flip", "N_Laserflip", 0),
|
||||
("N_Laserflip", "N_Laserflip", "N_360Flip", 0),
|
||||
("N_360Hardflip", "N_360Hardflip", "N_360InwardHeelflip", 0),
|
||||
(
|
||||
"N_360InwardHeelflip",
|
||||
"N_360InwardHeelflip",
|
||||
"N_360Hardflip",
|
||||
0,
|
||||
),
|
||||
("N_Kickflip", "N_Kickflip", "N_Heelflip", 0),
|
||||
("N_Heelflip", "N_Heelflip", "N_Kickflip", 0),
|
||||
("Ollie", "Nollie", "Nollie", 3),
|
||||
("PopShuvit", "N_PopShuvit", "N_FSPopShuvit", 3),
|
||||
("FSPopShuvit", "N_FSPopShuvit", "N_PopShuvit", 4),
|
||||
("VarialKickflip", "Nollie", "Nollie", 3),
|
||||
("VarialHeelflip", "Nollie", "Nollie", 4),
|
||||
("Hardflip", "Nollie", "Nollie", 3),
|
||||
("InwardHeelflip", "Nollie", "Nollie", 4),
|
||||
("360PopShuvit", "N_PopShuvit", "N_FSPopShuvit", 3),
|
||||
("FS360PopShuvit", "N_FSPopShuvit", "N_PopShuvit", 4),
|
||||
("360Flip", "Nollie", "Nollie", 3),
|
||||
("Laserflip", "Nollie", "Nollie", 4),
|
||||
("360Hardflip", "Nollie", "Nollie", 3),
|
||||
("360InwardHeelflip", "Nollie", "Nollie", 4),
|
||||
("Kickflip", "Nollie", "Nollie", 3),
|
||||
("Heelflip", "Nollie", "Nollie", 4),
|
||||
],
|
||||
&[
|
||||
("90_Ollie", "Ollie", "Ollie", 1),
|
||||
("90_PopShuvit", "PopShuvit", "FSPopShuvit", 1),
|
||||
("90_FSPopShuvit", "FSPopShuvit", "PopShuvit", 2),
|
||||
("90_VarialKickflip", "VarialKickflip", "VarialHeelflip", 1),
|
||||
("90_VarialHeelflip", "VarialHeelflip", "VarialKickflip", 2),
|
||||
("90_Hardflip", "Hardflip", "InwardHeelflip", 1),
|
||||
("90_InwardHeelflip", "InwardHeelflip", "Hardflip", 2),
|
||||
("90_360PopShuvit", "360PopShuvit", "FS360PopShuvit", 1),
|
||||
("90_FS360PopShuvit", "FS360PopShuvit", "360PopShuvit", 2),
|
||||
("90_360Flip", "360Flip", "Laserflip", 0),
|
||||
("90_Laserflip", "Laserflip", "360Flip", 0),
|
||||
("90_360Hardflip", "360Hardflip", "360InwardHeelflip", 0),
|
||||
(
|
||||
"90_360InwardHeelflip",
|
||||
"360InwardHeelflip",
|
||||
"360Hardflip",
|
||||
0,
|
||||
),
|
||||
("90_Kickflip", "Kickflip", "Heelflip", 0),
|
||||
("90_Heelflip", "Heelflip", "Kickflip", 0),
|
||||
("Ollie", "Ollie", "Ollie", 1),
|
||||
("PopShuvit", "PopShuvit", "FSPopShuvit", 1),
|
||||
("FSPopShuvit", "FSPopShuvit", "PopShuvit", 2),
|
||||
("VarialKickflip", "VarialKickflip", "VarialHeelflip", 1),
|
||||
("VarialHeelflip", "VarialHeelflip", "VarialKickflip", 2),
|
||||
("Hardflip", "Hardflip", "InwardHeelflip", 1),
|
||||
("InwardHeelflip", "InwardHeelflip", "Hardflip", 2),
|
||||
("360PopShuvit", "360PopShuvit", "FS360PopShuvit", 1),
|
||||
("FS360PopShuvit", "FS360PopShuvit", "360PopShuvit", 2),
|
||||
("360Flip", "360Flip", "Laserflip", 0),
|
||||
("Laserflip", "Laserflip", "360Flip", 0),
|
||||
("360Hardflip", "360Hardflip", "360InwardHeelflip", 0),
|
||||
("360InwardHeelflip", "360InwardHeelflip", "360Hardflip", 0),
|
||||
("Kickflip", "Kickflip", "Heelflip", 0),
|
||||
("Heelflip", "Heelflip", "Kickflip", 0),
|
||||
("Nollie", "Ollie", "Ollie", 1),
|
||||
("N_PopShuvit", "PopShuvit", "FSPopShuvit", 1),
|
||||
("N_FSPopShuvit", "FSPopShuvit", "PopShuvit", 2),
|
||||
("N_VarialKickflip", "Ollie", "Ollie", 1),
|
||||
("N_VarialHeelflip", "Ollie", "Ollie", 2),
|
||||
("N_Hardflip", "Ollie", "Ollie", 1),
|
||||
("N_InwardHeelflip", "Ollie", "Ollie", 2),
|
||||
("N_360PopShuvit", "PopShuvit", "FSPopShuvit", 1),
|
||||
("N_FS360PopShuvit", "FSPopShuvit", "PopShuvit", 2),
|
||||
("N_360Flip", "Ollie", "Ollie", 1),
|
||||
("N_Laserflip", "Ollie", "Ollie", 2),
|
||||
("N_360Hardflip", "Ollie", "Ollie", 1),
|
||||
("N_360InwardHeelflip", "Ollie", "Ollie", 2),
|
||||
("N_Kickflip", "Ollie", "Ollie", 1),
|
||||
("N_Heelflip", "Ollie", "Ollie", 2),
|
||||
],
|
||||
&[
|
||||
("N90_Nollie", "Nollie", "Nollie", 3),
|
||||
("N90_N_PopShuvit", "N_PopShuvit", "N_FSPopShuvit", 3),
|
||||
("N90_N_FSPopShuvit", "N_FSPopShuvit", "N_PopShuvit", 4),
|
||||
(
|
||||
"N90_N_VarialKickflip",
|
||||
"N_VarialKickflip",
|
||||
"N_VarialHeelflip",
|
||||
3,
|
||||
),
|
||||
(
|
||||
"N90_N_VarialHeelflip",
|
||||
"N_VarialHeelflip",
|
||||
"N_VarialKickflip",
|
||||
4,
|
||||
),
|
||||
("N90_N_Hardflip", "N_Hardflip", "N_InwardHeelflip", 3),
|
||||
("N90_N_InwardHeelflip", "N_InwardHeelflip", "N_Hardflip", 4),
|
||||
(
|
||||
"N90_N_360PopShuvit",
|
||||
"N_360PopShuvit",
|
||||
"N_FS360PopShuvit",
|
||||
3,
|
||||
),
|
||||
(
|
||||
"N90_N_FS360PopShuvit",
|
||||
"N_FS360PopShuvit",
|
||||
"N_360PopShuvit",
|
||||
4,
|
||||
),
|
||||
("N90_N_360Flip", "N_360Flip", "N_Laserflip", 0),
|
||||
("N90_N_Laserflip", "N_Laserflip", "N_360Flip", 0),
|
||||
(
|
||||
"N90_N_360Hardflip",
|
||||
"N_360Hardflip",
|
||||
"N_360InwardHeelflip",
|
||||
0,
|
||||
),
|
||||
(
|
||||
"N90_N_360InwardHeelflip",
|
||||
"N_360InwardHeelflip",
|
||||
"N_360Hardflip",
|
||||
0,
|
||||
),
|
||||
("N90_N_Kickflip", "N_Kickflip", "N_Heelflip", 0),
|
||||
("N90_N_Heelflip", "N_Heelflip", "N_Kickflip", 0),
|
||||
("Nollie", "Nollie", "Nollie", 3),
|
||||
("N_PopShuvit", "N_PopShuvit", "N_FSPopShuvit", 3),
|
||||
("N_FSPopShuvit", "N_FSPopShuvit", "N_PopShuvit", 4),
|
||||
(
|
||||
"N_VarialKickflip",
|
||||
"N_VarialKickflip",
|
||||
"N_VarialHeelflip",
|
||||
3,
|
||||
),
|
||||
(
|
||||
"N_VarialHeelflip",
|
||||
"N_VarialHeelflip",
|
||||
"N_VarialKickflip",
|
||||
4,
|
||||
),
|
||||
("N_Hardflip", "N_Hardflip", "N_InwardHeelflip", 3),
|
||||
("N_InwardHeelflip", "N_InwardHeelflip", "N_Hardflip", 4),
|
||||
("N_360PopShuvit", "N_360PopShuvit", "N_FS360PopShuvit", 3),
|
||||
("N_FS360PopShuvit", "N_FS360PopShuvit", "N_360PopShuvit", 4),
|
||||
("N_360Flip", "N_360Flip", "N_Laserflip", 0),
|
||||
("N_Laserflip", "N_Laserflip", "N_360Flip", 0),
|
||||
("N_360Hardflip", "N_360Hardflip", "N_360InwardHeelflip", 0),
|
||||
(
|
||||
"N_360InwardHeelflip",
|
||||
"N_360InwardHeelflip",
|
||||
"N_360Hardflip",
|
||||
0,
|
||||
),
|
||||
("N_Kickflip", "N_Kickflip", "N_Heelflip", 0),
|
||||
("N_Heelflip", "N_Heelflip", "N_Kickflip", 0),
|
||||
("Ollie", "Nollie", "Nollie", 3),
|
||||
("PopShuvit", "N_PopShuvit", "N_FSPopShuvit", 3),
|
||||
("FSPopShuvit", "N_FSPopShuvit", "N_PopShuvit", 4),
|
||||
("VarialKickflip", "Nollie", "Nollie", 3),
|
||||
("VarialHeelflip", "Nollie", "Nollie", 4),
|
||||
("Hardflip", "Nollie", "Nollie", 3),
|
||||
("InwardHeelflip", "Nollie", "Nollie", 4),
|
||||
("360PopShuvit", "N_PopShuvit", "N_FSPopShuvit", 3),
|
||||
("FS360PopShuvit", "N_FSPopShuvit", "N_PopShuvit", 4),
|
||||
("360Flip", "Nollie", "Nollie", 3),
|
||||
("Laserflip", "Nollie", "Nollie", 4),
|
||||
("360Hardflip", "Nollie", "Nollie", 3),
|
||||
("360InwardHeelflip", "Nollie", "Nollie", 4),
|
||||
("Kickflip", "Nollie", "Nollie", 3),
|
||||
("Heelflip", "Nollie", "Nollie", 4),
|
||||
],
|
||||
&[
|
||||
("N90_Ollie", "Ollie", "Ollie", 1),
|
||||
("N90_PopShuvit", "PopShuvit", "FSPopShuvit", 1),
|
||||
("N90_FSPopShuvit", "FSPopShuvit", "PopShuvit", 2),
|
||||
("N90_VarialKickflip", "VarialKickflip", "VarialHeelflip", 1),
|
||||
("N90_VarialHeelflip", "VarialHeelflip", "VarialKickflip", 2),
|
||||
("N90_Hardflip", "Hardflip", "InwardHeelflip", 1),
|
||||
("N90_InwardHeelflip", "InwardHeelflip", "Hardflip", 2),
|
||||
("N90_360PopShuvit", "360PopShuvit", "FS360PopShuvit", 1),
|
||||
("N90_FS360PopShuvit", "FS360PopShuvit", "360PopShuvit", 2),
|
||||
("N90_360Flip", "360Flip", "Laserflip", 0),
|
||||
("N90_Laserflip", "Laserflip", "360Flip", 0),
|
||||
("N90_360Hardflip", "360Hardflip", "360InwardHeelflip", 0),
|
||||
(
|
||||
"N90_360InwardHeelflip",
|
||||
"360InwardHeelflip",
|
||||
"360Hardflip",
|
||||
0,
|
||||
),
|
||||
("N90_Kickflip", "Kickflip", "Heelflip", 0),
|
||||
("N90_Heelflip", "Heelflip", "Kickflip", 0),
|
||||
("Ollie", "Ollie", "Ollie", 1),
|
||||
("PopShuvit", "PopShuvit", "FSPopShuvit", 1),
|
||||
("FSPopShuvit", "FSPopShuvit", "PopShuvit", 2),
|
||||
("VarialKickflip", "VarialKickflip", "VarialHeelflip", 1),
|
||||
("VarialHeelflip", "VarialHeelflip", "VarialKickflip", 2),
|
||||
("Hardflip", "Hardflip", "InwardHeelflip", 1),
|
||||
("InwardHeelflip", "InwardHeelflip", "Hardflip", 2),
|
||||
("360PopShuvit", "360PopShuvit", "FS360PopShuvit", 1),
|
||||
("FS360PopShuvit", "FS360PopShuvit", "360PopShuvit", 2),
|
||||
("360Flip", "360Flip", "Laserflip", 0),
|
||||
("Laserflip", "Laserflip", "360Flip", 0),
|
||||
("360Hardflip", "360Hardflip", "360InwardHeelflip", 0),
|
||||
("360InwardHeelflip", "360InwardHeelflip", "360Hardflip", 0),
|
||||
("Kickflip", "Kickflip", "Heelflip", 0),
|
||||
("Heelflip", "Heelflip", "Kickflip", 0),
|
||||
("Nollie", "Ollie", "Ollie", 1),
|
||||
("N_PopShuvit", "PopShuvit", "FSPopShuvit", 1),
|
||||
("N_FSPopShuvit", "FSPopShuvit", "PopShuvit", 2),
|
||||
("N_VarialKickflip", "Ollie", "Ollie", 1),
|
||||
("N_VarialHeelflip", "Ollie", "Ollie", 2),
|
||||
("N_Hardflip", "Ollie", "Ollie", 1),
|
||||
("N_InwardHeelflip", "Ollie", "Ollie", 2),
|
||||
("N_360PopShuvit", "PopShuvit", "FSPopShuvit", 1),
|
||||
("N_FS360PopShuvit", "FSPopShuvit", "PopShuvit", 2),
|
||||
("N_360Flip", "Ollie", "Ollie", 1),
|
||||
("N_Laserflip", "Ollie", "Ollie", 2),
|
||||
("N_360Hardflip", "Ollie", "Ollie", 1),
|
||||
("N_360InwardHeelflip", "Ollie", "Ollie", 2),
|
||||
("N_Kickflip", "Ollie", "Ollie", 1),
|
||||
("N_Heelflip", "Ollie", "Ollie", 2),
|
||||
],
|
||||
];
|
||||
@@ -59,7 +59,13 @@ fn main() -> bevy::app::AppExit {
|
||||
return bevy::app::AppExit::error();
|
||||
}
|
||||
};
|
||||
app::build(config, manifest, graphs, physics, skater).run()
|
||||
let controls = match physics::PlayerControls::load(&config.asset_root) {
|
||||
Ok(controls) => controls,
|
||||
Err(error) => { eprintln!("{error}"); return bevy::app::AppExit::error(); }
|
||||
};
|
||||
let mut app = app::build(config, manifest, graphs, physics, skater);
|
||||
app.insert_resource(controls);
|
||||
app.run()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -19,6 +19,7 @@ pub(crate) struct PlayerControls {
|
||||
pub bumper_state_502: bool,
|
||||
pub bumper_state_104: bool,
|
||||
pub preferences: PushPreferences,
|
||||
gestures: Option<crate::input::gesture_input::GestureInput>,
|
||||
}
|
||||
impl Default for PlayerControls {
|
||||
fn default() -> Self {
|
||||
@@ -33,6 +34,7 @@ impl Default for PlayerControls {
|
||||
bumper_state_502: false,
|
||||
bumper_state_104: false,
|
||||
preferences: PushPreferences::default(),
|
||||
gestures: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -50,9 +52,22 @@ pub(super) fn sample(
|
||||
physics.settings.input_magnitude_threshold,
|
||||
skater.player_input.physical.scoring.capabilities_204,
|
||||
);
|
||||
player.publish_gestures(physics.animation_profile.physics_mode, skater.player_input.physical.state.state_16);
|
||||
}
|
||||
|
||||
impl PlayerControls {
|
||||
pub fn load(root: &std::path::Path) -> Result<Self, String> {
|
||||
Ok(Self { gestures: Some(crate::input::gesture_input::GestureInput::load(root)?), ..Self::default() })
|
||||
}
|
||||
|
||||
pub fn publish_gestures(&mut self, difficulty: u32, physical_state: u32) {
|
||||
if let Some(gestures) = &mut self.gestures {
|
||||
let words = self.controller.words();
|
||||
let axes = [[words[7],words[8]], [words[9],words[10]]].map(|p|p.map(f32::from_bits));
|
||||
gestures.publish(axes,difficulty,self.actor_flags,physical_state,&mut self.action_intents);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, map: &mut impl ActionMap, dt: f32, magnitude_threshold: f32, physical_capabilities: u32) {
|
||||
self.controller.update(
|
||||
map,
|
||||
@@ -70,6 +85,7 @@ impl PlayerControls {
|
||||
self.intents.extend(wipeout_intentions::produce(
|
||||
&self.controller, self.actor_flags, physical_capabilities,
|
||||
));
|
||||
self.intents.extend(skate_core::input::anticipation_intentions::produce(&self.controller));
|
||||
//GenerateActionGraphIntents82594310 clears the AG map through82BC1B68
|
||||
//before Listener::Fill. MG lifecycle intents use a different persistent map.
|
||||
self.action_intents.clear();
|
||||
|
||||
@@ -19,6 +19,35 @@ fn parameter(mg_intent: &str, value: f32) -> Parameter {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gesture_behavior_publishes_and_releases_the_stock_tre_flip_intent() {
|
||||
let mut config = parameter("", 0.0);
|
||||
config.mg_intent = None;
|
||||
let instance = ActionInstance {
|
||||
operation: ActionOperation::CreateTrickIntentFromGesture {
|
||||
group: crate::input::gesture_catalog::Group::Square,
|
||||
override_name: None,
|
||||
},
|
||||
config,
|
||||
parameters: Vec::new(),
|
||||
};
|
||||
let mut host = ActionHost::new(ActionInstances::new(vec![instance]), OperationRemap {
|
||||
behaviors: vec![0], conditions: Vec::new(), hooks: Vec::new(),
|
||||
});
|
||||
let frame = Frame { dt: 1.0 / 60.0, current: None, last: None, state_times: Vec::new() };
|
||||
for (mirrored, expected) in [(false, "360Flip"), (true, "Laserflip")] {
|
||||
host.stance = Some((false, mirrored));
|
||||
host.action_intents.insert("360Flip", 1.0);
|
||||
Host::allocate(&mut host, 0, &frame);
|
||||
Host::begin(&mut host, 0, [0; 6], &frame);
|
||||
Host::update(&mut host, 0, [0; 6], &frame);
|
||||
assert_eq!(host.motion_intents.get(expected), Some(&0.0));
|
||||
Host::end(&mut host, 0, [0; 6], &frame);
|
||||
assert!(host.motion_intents.is_empty());
|
||||
}
|
||||
assert!(host.errors.is_empty(), "{:?}", host.errors);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn const_callbacks_publish_then_remove_motion_intent() {
|
||||
let instance = ActionInstance {
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
# 360 flip through the stock graphs
|
||||
|
||||
The ordinary on-board 360-flip route is wired for user visual testing.
|
||||
PLAY.bat and PLAY-MAP.bat use the staged build. The supplied graphs and
|
||||
animation banks remain the source of animation selection, transitions,
|
||||
postures, timing, and physical animation attributes.
|
||||
|
||||
## Input and playback
|
||||
|
||||
- Right-stick magnitude and angle publish `AnticMag` and `AnticAngle`.
|
||||
The existing anticipation states choose and blend the crouch/scoop poses.
|
||||
- The seven supplied joystick `.pat` files now run through the recovered
|
||||
recognizer. There is no trick hotkey, synthetic launch impulse, or new clip
|
||||
selected outside the graphs.
|
||||
- Recognized events pass through the original listener gates, then publish
|
||||
the gesture name, `Trick`, and `GestureSpeed` into the ActionGraph.
|
||||
- `CreateTrickIntentFromGesture` uses all 270 recovered mapping records,
|
||||
including native bucket order, mirrored mappings, overrides, dark-catch
|
||||
categories, and the four hold successors. A non-mirrored `360Flip` gesture
|
||||
maps to `360Flip`; when mirrored, that gesture maps to `Laserflip`, and the
|
||||
opposite gesture maps to `360Flip`, as in the original mapping.
|
||||
- The stock ground path selects `B_360FLIP_G`, then sequences into
|
||||
`B_360FLIP_A`. `SetTrickHeight` consumes the preceding animation's
|
||||
`AnticStrength`, gesture speed, and the stock jumping settings. Its
|
||||
`JumpHeightOverride` packet enters the existing skeleton-input handler.
|
||||
- Trick construction identity, scoring metadata, underflip/dark-catch
|
||||
requests, and nose-weight anticipation now have their original handlers.
|
||||
|
||||
The stock scoop coordinates for the non-mirrored 360-flip gesture correspond
|
||||
to left/slightly down, down-left, down/slightly right, then an up-right flick
|
||||
on the physical right stick. The recognizer permits holding the initial
|
||||
point without weakening the eventual gesture. Direction changes with the
|
||||
stock mirrored mapping; this is not an extra control setting.
|
||||
|
||||
## Evidence
|
||||
|
||||
TU3 instruction bytes were checked in a disposable IDA database against
|
||||
the recompiled PPC instruction listings. Relevant boundaries:
|
||||
|
||||
| Behavior | Native source |
|
||||
| --- | --- |
|
||||
| PAT loading and push-front coordinate storage | 82696B18, 82699738 |
|
||||
| Recognition, competition, strength, refractory frame | 82697168, 826972B8, 826974A8 |
|
||||
| Axis sign and component deadzone | 82696030, 826962D8, 82F75F60 |
|
||||
| Anticipation inputs | 82599AD0–82599C4C, 8259AD0C–8259AD40 |
|
||||
| Pattern/held listeners and intent publication | 8259B878, 8259B9D0, 8259B1F0–8259B7D8 |
|
||||
| Gesture mappings and graph lifecycle | 82B98F70, 82BA07F0, 82BA1C30–82BA2228 |
|
||||
| Trick height | 82BAED78, 82BAEE90 |
|
||||
| Trick construction identity | 82BB5C20, 82BB5CA0 |
|
||||
| Scoring metadata | 82BBFA88, 8258FA20 |
|
||||
| Underflip requests | 82BBE098, 82BBE0A8, 82BBE358 |
|
||||
| Nose-weight anticipation | 82BB2670, 82BB26D0, 82B97110 |
|
||||
|
||||
Factory names were verified from registration strings. In particular,
|
||||
the community label on 82BC7848 incorrectly calls it `ScoringTrick`; the
|
||||
actual registration is `SetTrickAttr`. Scoring's factory is 82BCC030.
|
||||
Local research outputs and the re-extracted mapping records are under
|
||||
`logs/`; original private binaries and animation data are not committed.
|
||||
|
||||
## Verification and user checks
|
||||
|
||||
Nonvisual checks pass for 36 core input tests, the PAT parser, the gesture
|
||||
mapping/ActionHost lifecycle, and two private-stock fixtures. Those fixtures
|
||||
recognize the authored scoop through all seven files and exercise the
|
||||
stock crouch, scoop, takeoff, and air-animation handlers in both stances,
|
||||
including finite decoded poses and required attributes. These are data/code
|
||||
checks, not a claim of visually verified gameplay parity. No gameplay or
|
||||
visual test was automated. The existing game instance was closed to stage
|
||||
the new build; restarting it for the user is separate from these checks.
|
||||
|
||||
Please check:
|
||||
|
||||
1. Hold the right stick to crouch while stationary and while rolling.
|
||||
2. Perform a 360 flip; check preparation, pop, board flip/spin, and foot motion.
|
||||
3. Land and push away, then repeat in the opposite stance.
|
||||
|
||||
This work validates the ordinary ground route, not every trick in the newly
|
||||
connected catalog. Advanced manual/grind-out branches still contain the
|
||||
known unsupported `JumpInto` behavior, and the existing separate
|
||||
`AddRunoutAttribs` gap remains. Those handlers were not bypassed. The current
|
||||
test does not certify dark catches or the rest of the trick catalog visually.
|
||||
Reference in new issue
Block a user