From e66e81c8aee48a67e85d94d00c3662ede4a39c76 Mon Sep 17 00:00:00 2001 From: chazmm Date: Tue, 8 Sep 2026 16:38:04 +0100 Subject: [PATCH] Port native sequence publication ordering and line settlement --- crates/skate-core/src/scoring.rs | 10 ++- crates/skate-core/src/scoring/carrier.rs | 6 ++ crates/skate-core/src/scoring/session.rs | 109 +++++++++++++++++++++++ crates/skate-data/src/scoring.rs | 9 ++ docs/native-scoring-hud.md | 11 ++- 5 files changed, 141 insertions(+), 4 deletions(-) create mode 100644 crates/skate-core/src/scoring/session.rs diff --git a/crates/skate-core/src/scoring.rs b/crates/skate-core/src/scoring.rs index f1e7406b..b4cd64e8 100644 --- a/crates/skate-core/src/scoring.rs +++ b/crates/skate-core/src/scoring.rs @@ -8,6 +8,7 @@ pub const SCORABLE_COUNT: usize = 332; pub const SCORE_TYPE_COUNT: usize = 14; pub mod carrier; pub mod catalog; +pub mod session; pub mod timer; #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -133,9 +134,16 @@ impl ScoreHolder { } /// Module82DA3B38 banks the line when its timer expires or reset is requested. pub fn finish_line(&mut self) { + self.bank_line(true); + } + /// A zero line timer banks the current line even while a collector is + /// active, but 82DA3B38 retains repetition in that case. + pub fn bank_line(&mut self, clear_repetition: bool) { self.snapshot.completed_lines += self.snapshot.line; self.snapshot.line = 0.0; - self.repetitions.fill(0); + if clear_repetition { + self.repetitions.fill(0); + } } /// Native Reset82DA5C78 retains lifetime completed-lines (+24). pub fn reset(&mut self) { diff --git a/crates/skate-core/src/scoring/carrier.rs b/crates/skate-core/src/scoring/carrier.rs index 713b0050..afe320b6 100644 --- a/crates/skate-core/src/scoring/carrier.rs +++ b/crates/skate-core/src/scoring/carrier.rs @@ -2,6 +2,12 @@ //! Collectors supply the native clock and already-conditioned point factor. use super::Scorable; +/// 82DA45C8 multiplies the sum of authored and collector delay by the +/// binary32 constant at 8303745C (0x42700000), then truncates to integer. +pub fn delay_ticks(authored_seconds: f32, extra_seconds: f32) -> u32 { + (((authored_seconds + extra_seconds) * f32::from_bits(0x42700000)) as i64) as u32 +} + #[derive(Clone, Debug)] pub struct Carrier { pub scorable: Scorable, diff --git a/crates/skate-core/src/scoring/session.rs b/crates/skate-core/src/scoring/session.rs new file mode 100644 index 00000000..d7e979a9 --- /dev/null +++ b/crates/skate-core/src/scoring/session.rs @@ -0,0 +1,109 @@ +//! Module publication and line settlement, TU3 82DA37B0/82DA3B38. +use super::{ + ScoreHolder, + timer::{ComboTimer, PointTimer}, +}; + +#[derive(Clone, Copy, Debug)] +pub struct Rules { + pub combo_capacity: f32, + pub combo_levels: [(f32, f32); 3], + pub combo_refresh_threshold: f32, + pub line_capacity: f32, + pub bail_factor: f32, +} + +#[derive(Clone, Debug, Default)] +pub struct Session { + pub holder: ScoreHolder, + pub combo: ComboTimer, + pub line: PointTimer, +} + +impl Session { + /// The collector owns the decision to publish. Capture the multiplier + /// before crediting timers: a threshold reached by this reward affects + /// the next publication, not this publication's score (f29 at 82DA37B0). + pub fn publish_sequence( + &mut self, + rules: &Rules, + landing_factor: f32, + penalized: bool, + multiplier_enabled: bool, + ) -> f32 { + self.holder.reward_sequence(landing_factor); + let snapshot = &self.holder.snapshot; + let mut raw = + (snapshot.fingerflip_pending + snapshot.general_pending) + snapshot.accumulated; + if penalized && rules.bail_factor < 1.0 { + raw *= rules.bail_factor; + } + let multiplier = if multiplier_enabled { + self.combo.multiplier + } else { + 1.0 + }; + if multiplier_enabled { + self.combo.credit( + raw, + rules.combo_capacity, + rules.combo_levels, + rules.combo_refresh_threshold, + ); + if self.combo.multiplier > f32::from_bits(0x3f8147ae) { + self.line.credit(raw, rules.line_capacity); + } + } + let reward = multiplier * raw; + self.holder + .publish(reward, multiplier_enabled && self.line.points > 0.0); + reward + } + + /// Reset/bail/output requests or the expiry edge reset line time and the + /// multiplier. A merely empty line timer does not reset the multiplier. + pub fn settle_line(&mut self, reset_requested: bool, collector_active: bool) { + if reset_requested || self.line.expired { + self.line = PointTimer::default(); + self.combo.multiplier = 1.0; + self.holder.finish_line(); + } else if self.line.points <= 0.0 { + self.holder.bank_line(!collector_active); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::scoring::Scorable; + #[test] + fn newly_crossed_threshold_applies_to_the_following_publication() { + let rules = Rules { + combo_capacity: 801.0, + combo_levels: [(50.0, 1.5), (450.0, 2.0), (800.0, 3.0)], + combo_refresh_threshold: 799.0, + line_capacity: 400.0, + bail_factor: 0.0, + }; + let mut session = Session::default(); + let trick = Scorable { + id: 96, + class: 3, + score_type: 2, + }; + session.holder.end_trick(trick, 50.0); + session.holder.finish_collector(); + assert_eq!(session.publish_sequence(&rules, 1.0, false, true), 50.0); + assert_eq!(session.combo.multiplier, 1.5); + session.holder.end_trick(trick, 10.0); + session.holder.finish_collector(); + assert_eq!(session.publish_sequence(&rules, 1.0, false, true), 15.0); + assert_eq!(session.holder.snapshot.line, 65.0); + session.line.points = 0.0; + session.settle_line(false, true); + assert_eq!(session.holder.repetition_count(trick), Some(2)); + session.settle_line(false, false); + assert_eq!(session.holder.repetition_count(trick), Some(0)); + } +} diff --git a/crates/skate-data/src/scoring.rs b/crates/skate-data/src/scoring.rs index b4e4d2fb..02a005c8 100644 --- a/crates/skate-data/src/scoring.rs +++ b/crates/skate-data/src/scoring.rs @@ -40,6 +40,15 @@ pub struct ScoringData { } impl ScoringData { + pub fn session_rules(&self) -> skate_core::scoring::session::Rules { + skate_core::scoring::session::Rules { + combo_capacity: self.combo_capacity, + combo_levels: self.combo_levels, + combo_refresh_threshold: self.combo_refresh_threshold, + line_capacity: self.line_capacity, + bail_factor: self.bail_factor, + } + } pub fn load(data: &Collections) -> Result { let mut definitions = Vec::new(); for (id, &(identifier, class, score_type)) in IDENTIFIERS.iter().enumerate() { diff --git a/docs/native-scoring-hud.md b/docs/native-scoring-hud.md index 33d9a547..bb2e3fda 100644 --- a/docs/native-scoring-hud.md +++ b/docs/native-scoring-hud.md @@ -18,6 +18,9 @@ Implemented source components: * `skate-core::scoring`: ScoreHolder accounting at `82DA6198`, `82DA6260`, `82DA6408`, `82DA6468`, `82DA6538`; point timers at `82DA4C28`; carrier announcement/completion at `82DA45C8`, `82DA46D0`, `82DA5DE0`, `82DA5F98`. + Session publication at `82DA37B0` captures the multiplier before crediting + this reward to the combo timer. Settlement at `82DA3B38` preserves repetition + on an empty line timer while a collector is active. * `skate-data::scoring`: resolves authored VLT points, labels, delay, repetition, announcement curves, combo thresholds and timer settings. Missing data is an error. The executable's 332-entry identifier metadata includes unused @@ -61,8 +64,9 @@ with a new overlay would not meet this port's requirements. Collectors are not wired into the production tick yet. Recognition still requires conditioner gates, descriptor conversion, air/ground/grind collector transitions, spin/flip bonuses, gaps, landing and sequence publication ordering. -The native clock frequency must be recovered before converting authored -announcement delays to ticks. A physics-frame count is not a safe substitute. +Announcement delay conversion uses the verified binary32 60 at `8303745C`. +The carrier's start/current tick source still needs to be connected at the +matching production phase. The HUD still needs a complete movie object hierarchy, native bindings, timeline execution and rendering with multiplicative and additive colors. @@ -84,7 +88,8 @@ release build/launcher remain to be integrated and checked. No game/recomp, controller harness, gameplay automation, `--check-assets`, or screenshot capture has been run for this work. -Validation so far: eight scoring unit tests pass (including unsigned clock +Validation so far: nine scoring unit tests pass (including unsigned clock rollover, early completion, conversion, timer thresholds, one-time banking and cancelled rewards); the owned-data loader audit resolves 300 definitions; the APT data-only audit passes placement validation and constructor checks. +