Port native scoring ledger, carrier lifecycle and authored tuning loader

This commit is contained in:
chazmm committed 2026-09-08 16:25:15 +01:00
1 parent 43943a71bc
commit 3c535a392b
9 files changed
+1007

No files matched your search

+1
View File
@@ -14,4 +14,5 @@ pub mod physics;
pub mod player;
pub mod point_graph;
pub mod riding;
pub mod scoring;
pub mod trigonometry;
+212
View File
@@ -0,0 +1,212 @@
//! Native ScoreHolder accounting, TU3 82DA6198/6260/6408/6468/6538.
//!
//! Collectors own recognition and reward calculation. This ledger deliberately
//! accepts their rewards rather than substituting a table of guessed points.
//! Rendering and networking consume snapshots; neither advances accounting.
pub const SCORABLE_COUNT: usize = 332;
pub const SCORE_TYPE_COUNT: usize = 14;
pub mod carrier;
pub mod catalog;
pub mod timer;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Scorable {
pub id: usize,
/// Fixed metadata table +12. Distinct from the display type at +16.
pub class: u32,
pub score_type: usize,
}
impl Scorable {
pub fn valid(self) -> bool {
self.id < SCORABLE_COUNT && self.score_type < SCORE_TYPE_COUNT
}
/// CalcPointPenalty82DA5D18 excludes metric class5 and class6.
pub fn repetition_applies(self) -> bool {
self.valid() && self.class != 5 && self.class != 6
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct Snapshot {
pub completed_lines: f32, //24
pub line: f32, //28
pub accumulated: f32, //32
pub last_reward: f32, //36
pub general_pending: f32, //40
pub fingerflip_pending: f32, //44
pub grind_reward: f32, //4776
}
#[derive(Clone, Debug)]
pub struct ScoreHolder {
pub snapshot: Snapshot,
repetitions: [i8; SCORABLE_COUNT], //4096
sequence_history: [i8; SCORABLE_COUNT], //4428
type_history: [i8; SCORE_TYPE_COUNT], //4760
pending_sequence: bool, //7260
suppressed: bool, //7261 (physical output, not difficulty)
}
impl Default for ScoreHolder {
fn default() -> Self {
Self {
snapshot: Snapshot::default(),
repetitions: [0; SCORABLE_COUNT],
sequence_history: [0; SCORABLE_COUNT],
type_history: [0; SCORE_TYPE_COUNT],
pending_sequence: false,
suppressed: false,
}
}
}
impl ScoreHolder {
pub fn set_suppressed(&mut self, suppressed: bool) {
self.suppressed = suppressed;
}
pub fn repetition_count(&self, scorable: Scorable) -> Option<i8> {
scorable.valid().then(|| self.repetitions[scorable.id])
}
/// EndAirTrick82DA6260. Invalid IDs do not touch either history or rewards.
pub fn end_trick(&mut self, scorable: Scorable, reward: f32) {
if !scorable.valid() || self.suppressed {
return;
}
self.repetitions[scorable.id] = self.repetitions[scorable.id].saturating_add(1);
if scorable.score_type != 0 {
self.sequence_history[scorable.id] =
self.sequence_history[scorable.id].saturating_add(1);
self.type_history[scorable.score_type] =
self.type_history[scorable.score_type].saturating_add(1);
}
let s = &mut self.snapshot;
if scorable.class != 5 {
s.general_pending += s.fingerflip_pending;
s.fingerflip_pending = 0.0;
}
if scorable.class == 3 {
s.fingerflip_pending += reward;
} else {
s.general_pending += reward;
}
}
/// Collector Exit publishes a pending sequence even for a cancelled exit.
pub fn finish_collector(&mut self) {
self.pending_sequence = true;
}
pub fn cancel_pending(&mut self) {
if self.pending_sequence {
self.snapshot.general_pending = 0.0;
self.snapshot.fingerflip_pending = 0.0;
}
}
/// RewaredAirSequence82DA6468. Repeated calls cannot bank twice.
pub fn reward_sequence(&mut self, multiplier: f32) {
if !self.pending_sequence {
return;
}
let s = &mut self.snapshot;
s.accumulated += (s.general_pending + s.fingerflip_pending) * multiplier;
s.general_pending = 0.0;
s.fingerflip_pending = 0.0;
self.pending_sequence = false;
}
/// PublishAndResetAirSequence82DA6538 receives the final reward from the
/// module; it does not itself calculate the multiplier or bail penalty.
pub fn publish(&mut self, reward: f32, add_to_line: bool) {
let s = &mut self.snapshot;
if add_to_line {
s.line += reward;
}
s.last_reward = reward;
s.accumulated = 0.0;
s.general_pending = 0.0;
s.fingerflip_pending = 0.0;
self.clear_sequence_history();
}
pub fn clear_sequence_history(&mut self) {
self.sequence_history.fill(0);
self.type_history.fill(0);
self.snapshot.grind_reward = 0.0;
}
/// Module82DA3B38 banks the line when its timer expires or reset is requested.
pub fn finish_line(&mut self) {
self.snapshot.completed_lines += self.snapshot.line;
self.snapshot.line = 0.0;
self.repetitions.fill(0);
}
/// Native Reset82DA5C78 retains lifetime completed-lines (+24).
pub fn reset(&mut self) {
let completed_lines = self.snapshot.completed_lines;
*self = Self::default();
self.snapshot.completed_lines = completed_lines;
}
}
#[cfg(test)]
mod tests {
use super::*;
const FLIP: Scorable = Scorable {
id: 96,
class: 3,
score_type: 2,
};
#[test]
fn collector_bank_is_single_use_and_publication_retains_repetition() {
let mut h = ScoreHolder::default();
h.end_trick(FLIP, 20.0);
h.finish_collector();
h.reward_sequence(1.1);
h.reward_sequence(1.1);
assert_eq!(h.snapshot.accumulated, 22.0);
h.publish(22.0, true);
assert_eq!(h.snapshot.line, 22.0);
assert_eq!(h.repetition_count(FLIP), Some(1));
h.finish_line();
assert_eq!(h.snapshot.completed_lines, 22.0);
assert_eq!(h.repetition_count(FLIP), Some(0));
}
#[test]
fn metric_preserves_flip_bucket_other_classes_flush_it() {
let mut h = ScoreHolder::default();
h.end_trick(FLIP, 20.0);
h.end_trick(
Scorable {
id: 129,
class: 5,
score_type: 0,
},
4.0,
);
assert_eq!(
(h.snapshot.general_pending, h.snapshot.fingerflip_pending),
(4.0, 20.0)
);
h.end_trick(
Scorable {
id: 128,
class: 4,
score_type: 1,
},
2.0,
);
assert_eq!(
(h.snapshot.general_pending, h.snapshot.fingerflip_pending),
(26.0, 0.0)
);
}
#[test]
fn cancellation_after_collector_exit_prevents_pending_reward() {
let mut h = ScoreHolder::default();
h.end_trick(FLIP, 20.0);
h.finish_collector();
h.cancel_pending();
h.reward_sequence(1.1);
assert_eq!(h.snapshot.accumulated, 0.0);
h.set_suppressed(true);
h.end_trick(FLIP, 20.0);
assert_eq!(h.repetition_count(FLIP), Some(1));
}
}
+143
View File
@@ -0,0 +1,143 @@
//! Scorable carrier lifecycle, TU3 82DA45C8/46D0/5DE0/5F98.
//! Collectors supply the native clock and already-conditioned point factor.
use super::Scorable;
#[derive(Clone, Debug)]
pub struct Carrier {
pub scorable: Scorable,
pub points: i32,
pub factor: f32,
pub reward: f32,
pub announcement_threshold: f32,
pub start_tick: u32,
pub delay_ticks: u32,
pub announced: bool,
pub completed: bool,
pub unannounced: bool,
pub switch: bool,
pub fakie: bool,
}
impl Carrier {
/// Delay is supplied in native ticks: simulation frame count must not be
/// substituted for the source clock's frequency.
pub fn new(
scorable: Scorable,
points: i32,
factor: f32,
announcement_threshold: f32,
start_tick: u32,
delay_ticks: u32,
switch: bool,
fakie: bool,
) -> Self {
Self {
scorable,
points,
factor,
reward: 0.0,
announcement_threshold: announcement_threshold * factor,
start_tick,
delay_ticks,
announced: false,
completed: false,
unannounced: false,
switch,
fakie,
}
}
fn credit(&mut self, unannounced_factor: f32) {
let mut reward = self.points as f32 * self.factor;
if self.unannounced {
reward *= unannounced_factor;
}
self.reward += reward;
}
/// Returns true only on the announcement edge. Unsigned subtraction is
/// intentional and preserves the executable's behavior at clock rollover.
pub fn announce(&mut self, tick: u32, unannounced_factor: f32) -> bool {
if self.announced
|| !self.scorable.valid()
|| tick.wrapping_sub(self.start_tick) < self.delay_ticks
{
return false;
}
self.announced = true;
self.credit(unannounced_factor);
true
}
/// Called once by the collector when removing its current carrier. The
/// executable does not mark an early completion as an announcement.
pub fn complete(&mut self, unannounced_factor: f32) -> bool {
if !self.scorable.valid() {
return false;
}
if !self.announced {
self.unannounced = true;
self.credit(unannounced_factor);
}
self.completed = true;
true
}
/// Replace a carrier through its conversion link (82DA6320). Conversion
/// reports the previous carrier as completed and credits the replacement
/// immediately, even before its normal announcement delay.
pub fn convert_to(&mut self, replacement: &mut Self, unannounced_factor: f32) {
self.completed = true;
replacement.announced = true;
replacement.credit(unannounced_factor);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn carrier(start: u32) -> Carrier {
Carrier::new(
Scorable {
id: 96,
class: 3,
score_type: 2,
},
100,
0.2,
2.25,
start,
4,
false,
false,
)
}
#[test]
fn announcement_clock_wraps_and_does_not_credit_twice() {
let mut c = carrier(u32::MAX - 2);
assert!(!c.announce(0, 0.3));
assert!(c.announce(1, 0.3));
assert!(!c.announce(2, 0.3));
assert!(c.complete(0.3));
assert_eq!(c.reward, 20.0);
assert!(!c.unannounced);
}
#[test]
fn early_completion_uses_authored_discount_without_announcement() {
let mut c = carrier(0);
assert!(c.complete(0.3));
assert_eq!(c.reward, 6.0);
assert!(c.completed && c.unannounced && !c.announced);
}
#[test]
fn conversion_announces_replacement_without_completing_it() {
let mut previous = carrier(0);
let mut next = carrier(1);
previous.convert_to(&mut next, 0.3);
assert!(previous.completed);
assert_eq!(previous.reward, 0.0);
assert!(next.announced && !next.completed);
assert_eq!(next.reward, 20.0);
assert!(!next.announce(5, 0.3));
}
}
+337
View File
@@ -0,0 +1,337 @@
//! TU3 EScorableID enum metadata, table820862A8 (332 x24 bytes).
//! Numeric class/type relationships and native identifier spellings only.
//! Points, localization, timers and UI assets are loaded from owned data.
pub const IDENTIFIERS: [(&str, u32, usize); 332] = [
("fspowerslide", 0, 9), // 0
("bspowerslide", 0, 9), // 1
("nosemanual", 0, 8), // 2
("tailmanual", 0, 8), // 3
("fsrevert", 0, 11), // 4
("bsrevert", 0, 11), // 5
("bs_5_o", 1, 5), // 6
("bs_50_50", 1, 5), // 7
("bs_blunt", 1, 5), // 8
("bs_board", 1, 5), // 9
("bs_crook", 1, 5), // 10
("bs_feeble", 1, 5), // 11
("bs_lip", 1, 5), // 12
("bs_noseblunt", 1, 5), // 13
("bs_nosegrind", 1, 5), // 14
("bs_noseslide", 1, 5), // 15
("bs_overcrook", 1, 5), // 16
("bs_overwilly", 1, 5), // 17
("bs_salad", 1, 5), // 18
("bs_smith", 1, 5), // 19
("bs_tailslide", 1, 5), // 20
("bs_willy", 1, 5), // 21
("fs_5_o", 1, 5), // 22
("fs_50_50", 1, 5), // 23
("fs_blunt", 1, 5), // 24
("fs_board", 1, 5), // 25
("fs_crook", 1, 5), // 26
("fs_feeble", 1, 5), // 27
("fs_lip", 1, 5), // 28
("fs_noseblunt", 1, 5), // 29
("fs_nosegrind", 1, 5), // 30
("fs_noseslide", 1, 5), // 31
("fs_overcrook", 1, 5), // 32
("fs_overwilly", 1, 5), // 33
("fs_salad", 1, 5), // 34
("fs_smith", 1, 5), // 35
("fs_tailslide", 1, 5), // 36
("fs_willy", 1, 5), // 37
("grind5_o", 1, 5), // 38
("grind5050", 1, 5), // 39
("grindcrooks", 1, 5), // 40
("grindfeeble", 1, 5), // 41
("grindnose", 1, 5), // 42
("grindovercrooks", 1, 5), // 43
("grindoverwilly", 1, 5), // 44
("grindsalad", 1, 5), // 45
("grindsmith", 1, 5), // 46
("grindwilly", 1, 5), // 47
("slideblunt", 1, 5), // 48
("slideboard", 1, 5), // 49
("slidelip", 1, 5), // 50
("slidenblunt", 1, 5), // 51
("slidenose", 1, 5), // 52
("slidetail", 1, 5), // 53
("bsgrab", 2, 4), // 54
("bsgrab_down", 2, 4), // 55
("bsgrab_left", 2, 4), // 56
("bsgrab_right", 2, 4), // 57
("bsgrab_up", 2, 4), // 58
("christ_air", 2, 4), // 59
("coffin", 2, 4), // 60
("dblgrab", 2, 4), // 61
("fsgrab", 2, 4), // 62
("fsgrab_down", 2, 4), // 63
("fsgrab_left", 2, 4), // 64
("fsgrab_right", 2, 4), // 65
("fsgrab_up", 2, 4), // 66
("gnd_bsgrab", 2, 4), // 67
("gnd_dblgrab", 2, 4), // 68
("gnd_fsgrab", 2, 4), // 69
("mutegrab", 2, 4), // 70
("mutegrab_down", 2, 4), // 71
("mutegrab_left", 2, 4), // 72
("mutegrab_up", 2, 4), // 73
("nosegrab", 2, 4), // 74
("nosegrab_left", 2, 4), // 75
("nosegrab_right", 2, 4), // 76
("rocketair", 2, 4), // 77
("stalegrab", 2, 4), // 78
("stalegrab_down", 2, 4), // 79
("stalegrab_left", 2, 4), // 80
("stalegrab_up", 2, 4), // 81
("tailgrab", 2, 4), // 82
("tailgrab_left", 2, 4), // 83
("tailgrab_right", 2, 4), // 84
("360flip", 3, 2), // 85
("360hardflip", 3, 2), // 86
("360inwardheelflip", 3, 2), // 87
("360popshuvit", 3, 2), // 88
("fs360popshuvit", 3, 2), // 89
("fspopshuvit", 3, 2), // 90
("hardflip", 3, 2), // 91
("heelflip", 3, 2), // 92
("heelflip2", 3, 2), // 93
("heelflip3", 3, 2), // 94
("inwardheelflip", 3, 2), // 95
("kickflip", 3, 2), // 96
("kickflip2", 3, 2), // 97
("kickflip3", 3, 2), // 98
("laserflip", 3, 2), // 99
("latebackfootheelflip", 3, 2), // 100
("latebackfootkickflip", 3, 2), // 101
("latebsshuv", 3, 2), // 102
("latefsshuv", 3, 2), // 103
("lateheelflip", 3, 2), // 104
("latekickflip", 3, 2), // 105
("n_360flip", 3, 2), // 106
("n_360hardflip", 3, 2), // 107
("n_360inwardheelflip", 3, 2), // 108
("n_360popshuvit", 3, 2), // 109
("n_fs360popshuvit", 3, 2), // 110
("n_fspopshuvit", 3, 2), // 111
("n_hardflip", 3, 2), // 112
("n_heelflip", 3, 2), // 113
("n_heelflip2", 3, 2), // 114
("n_heelflip3", 3, 2), // 115
("n_inwardheelflip", 3, 2), // 116
("n_kickflip", 3, 2), // 117
("n_kickflip2", 3, 2), // 118
("n_kickflip3", 3, 2), // 119
("n_laserflip", 3, 2), // 120
("n_popshuvit", 3, 2), // 121
("n_varialheelflip", 3, 2), // 122
("n_varialkickflip", 3, 2), // 123
("popshuvit", 3, 2), // 124
("varialheelflip", 3, 2), // 125
("varialkickflip", 3, 2), // 126
("nollie", 4, 1), // 127
("ollie", 4, 1), // 128
("air_horizontal_distance", 5, 0), // 129
("air_height_to_peak", 5, 0), // 130
("air_total_height_gain", 5, 0), // 131
("air_player_spin", 5, 0), // 132
("air_player_flip", 5, 0), // 133
("heelflip4", 3, 2), // 134
("kickflip4", 3, 2), // 135
("n_heelflip4", 3, 2), // 136
("n_kickflip4", 3, 2), // 137
("nofoot_air", 2, 4), // 138
("fs_handplant", 6, 6), // 139
("fs_handplant_up", 6, 6), // 140
("fs_handplant_right", 6, 6), // 141
("fs_handplant_left", 6, 6), // 142
("fs_handplant_down", 6, 6), // 143
("fs_handplant_onefoot_front", 6, 6), // 144
("fs_handplant_onefoot_back", 6, 6), // 145
("fs_handplant_nofoot", 6, 6), // 146
("fs_handplant_to_fakie", 6, 6), // 147
("fs_handplant_to_revert", 6, 6), // 148
("bs_handplant", 6, 6), // 149
("bs_handplant_up", 6, 6), // 150
("bs_handplant_right", 6, 6), // 151
("bs_handplant_left", 6, 6), // 152
("bs_handplant_down", 6, 6), // 153
("bs_handplant_onefoot_front", 6, 6), // 154
("bs_handplant_onefoot_back", 6, 6), // 155
("bs_handplant_nofoot", 6, 6), // 156
("bs_handplant_to_fakie", 6, 6), // 157
("bs_handplant_to_revert", 6, 6), // 158
("bs_handplant_mount", 6, 6), // 159
("bs_handplant_dismount", 6, 6), // 160
("bs_handplant_to_flat", 6, 6), // 161
("fs_onefoot_right", 2, 4), // 162
("fs_backfoot", 2, 4), // 163
("bs_onefoot_front", 2, 4), // 164
("bs_onefoot_behind", 2, 4), // 165
("nosegrab_onefoot_front", 2, 4), // 166
("nosegrab_onefoot_behind", 2, 4), // 167
("tailgrab_onefoot_front", 2, 4), // 168
("tailgrab_onefoot_behind", 2, 4), // 169
("ollie_onefoot_front", 4, 1), // 170
("ollie_onefoot_back", 4, 1), // 171
("tailgrab_airwalk", 2, 4), // 172
("fsgrab_fingerflip", 4, 3), // 173
("bsgrab_fingerflip", 4, 3), // 174
("nosegrab_fingerflip", 4, 3), // 175
("tailgrab_fingerflip", 4, 3), // 176
("mutegrab_fingerflip", 4, 3), // 177
("stalegrab_fingerflip", 4, 3), // 178
("fsgrabvarial_fingerflip", 4, 3), // 179
("bsgrabvarial_fingerflip", 4, 3), // 180
("fs_rock_n_roll", 1, 5), // 181
("bs_rock_n_roll", 1, 5), // 182
("fakie_rock", 1, 5), // 183
("dropin", 1, 5), // 184
("dropin_fs_revert", 1, 5), // 185
("dropin_bs_revert", 1, 5), // 186
("crailgrab", 2, 4), // 187
("crailgrab_left", 2, 4), // 188
("crailgrab_right", 2, 4), // 189
("seatbeltgrab", 2, 4), // 190
("seatbeltgrab_left", 2, 4), // 191
("seatbeltgrab_right", 2, 4), // 192
("fs_50_50_fsgrab", 1, 5), // 193
("fs_50_50_bsgrab", 1, 5), // 194
("fs_nosegrind_tailgrab", 1, 5), // 195
("fs_nosegrind_bsgrab", 1, 5), // 196
("fs_crook_tailgrab", 1, 5), // 197
("fs_crook_bsgrab", 1, 5), // 198
("bs_crook_tailgrab", 1, 5), // 199
("bs_crook_bsgrab", 1, 5), // 200
("fs_noseslide_tailgrab", 1, 5), // 201
("bs_noseslide_tailgrab", 1, 5), // 202
("fs_noseblunt_tailgrab", 1, 5), // 203
("bs_noseblunt_tailgrab", 1, 5), // 204
("fs_5_o_fsgrab", 1, 5), // 205
("fs_5_o_nosegrab", 1, 5), // 206
("fs_salad_fsgrab", 1, 5), // 207
("fs_salad_nosegrab", 1, 5), // 208
("bs_salad_fsgrab", 1, 5), // 209
("bs_salad_nosegrab", 1, 5), // 210
("fs_tailslide_nosegrab", 1, 5), // 211
("bs_tailslide_nosegrab", 1, 5), // 212
("fs_blunt_tailgrab", 1, 5), // 213
("bs_blunt_tailgrab", 1, 5), // 214
("fsboneless", 4, 12), // 215
("bsboneless", 4, 12), // 216
("fsfastplant", 4, 12), // 217
("bsfastplant", 4, 12), // 218
("bsgrab_fs_footplant", 4, 13), // 219
("bsgrab_bs_footplant", 4, 13), // 220
("fsgrab_fs_footplant", 4, 13), // 221
("fsgrab_bs_footplant", 4, 13), // 222
("nosegrab_fs_footplant", 4, 13), // 223
("nosegrab_bs_footplant", 4, 13), // 224
("tailgrab_fs_footplant", 4, 13), // 225
("tailgrab_bs_footplant", 4, 13), // 226
("ollie_fs_footplant", 4, 13), // 227
("ollie_bs_footplant", 4, 13), // 228
("nocomply", 4, 7), // 229
("no_comply_fs180", 4, 7), // 230
("no_comply_bs180", 4, 7), // 231
("no_comply_fs360", 4, 7), // 232
("no_comply_bs360", 4, 7), // 233
("hippyjump", 4, 10), // 234
("hippyjump_fs180", 4, 10), // 235
("hippyjump_bs180", 4, 10), // 236
("air_metric", 5, 0), // 237
("fsgrabtostalegrab_fingerflip", 4, 3), // 238
("bsgrabtomutegrab_fingerflip", 4, 3), // 239
("stalegrabtofsgrab_fingerflip", 4, 3), // 240
("mutegrabtobsgrab_fingerflip", 4, 3), // 241
("bs_50_50_fsgrab", 1, 5), // 242
("bs_50_50_bsgrab", 1, 5), // 243
("bs_nosegrind_tailgrab", 1, 5), // 244
("bs_nosegrind_bsgrab", 1, 5), // 245
("bs_5_o_fsgrab", 1, 5), // 246
("bs_5_o_nosegrab", 1, 5), // 247
("fs_overcrook_tailgrab", 1, 5), // 248
("fs_overcrook_bsgrab", 1, 5), // 249
("bs_overcrook_tailgrab", 1, 5), // 250
("bs_overcrook_bsgrab", 1, 5), // 251
("superman", 2, 4), // 252
("generic_metric", 5, 0), // 253
("nosegrab_airwalk", 2, 4), // 254
("seatbelttonose_fingerflip", 4, 3), // 255
("crailtotail_fingerflip", 4, 3), // 256
("fs_board_double", 1, 5), // 257
("fs_board_tail", 1, 5), // 258
("fs_board_nose", 1, 5), // 259
("bs_board_double", 1, 5), // 260
("bs_board_tail", 1, 5), // 261
("bs_board_nose", 1, 5), // 262
("fs_lip_double", 1, 5), // 263
("fs_lip_tail", 1, 5), // 264
("fs_lip_nose", 1, 5), // 265
("bs_lip_double", 1, 5), // 266
("bs_lip_tail", 1, 5), // 267
("bs_lip_nose", 1, 5), // 268
("bs_darkslide", 1, 5), // 269
("fs_darkslide", 1, 5), // 270
("bs_darkslide_double", 1, 5), // 271
("bs_darkslide_tail", 1, 5), // 272
("bs_darkslide_nose", 1, 5), // 273
("fs_darkslide_double", 1, 5), // 274
("fs_darkslide_tail", 1, 5), // 275
("fs_darkslide_nose", 1, 5), // 276
("heelflip_underflip", 3, 2), // 277
("heelflip2_underflip", 3, 2), // 278
("heelflip3_underflip", 3, 2), // 279
("heelflip4_underflip", 3, 2), // 280
("kickflip_underflip", 3, 2), // 281
("kickflip2_underflip", 3, 2), // 282
("kickflip3_underflip", 3, 2), // 283
("kickflip4_underflip", 3, 2), // 284
("n_heelflip_underflip", 3, 2), // 285
("n_heelflip2_underflip", 3, 2), // 286
("n_heelflip3_underflip", 3, 2), // 287
("n_heelflip4_underflip", 3, 2), // 288
("n_kickflip_underflip", 3, 2), // 289
("n_kickflip2_underflip", 3, 2), // 290
("n_kickflip3_underflip", 3, 2), // 291
("n_kickflip4_underflip", 3, 2), // 292
("popshuvit_underflip", 3, 2), // 293
("fspopshuvit_underflip", 3, 2), // 294
("n_popshuvit_underflip", 3, 2), // 295
("n_fspopshuvit_underflip", 3, 2), // 296
("heelflip_darkcatch", 3, 2), // 297
("heelflip2_darkcatch", 3, 2), // 298
("heelflip3_darkcatch", 3, 2), // 299
("heelflip4_darkcatch", 3, 2), // 300
("kickflip_darkcatch", 3, 2), // 301
("kickflip2_darkcatch", 3, 2), // 302
("kickflip3_darkcatch", 3, 2), // 303
("kickflip4_darkcatch", 3, 2), // 304
("n_heelflip_darkcatch", 3, 2), // 305
("n_heelflip2_darkcatch", 3, 2), // 306
("n_heelflip3_darkcatch", 3, 2), // 307
("n_heelflip4_darkcatch", 3, 2), // 308
("n_kickflip_darkcatch", 3, 2), // 309
("n_kickflip2_darkcatch", 3, 2), // 310
("n_kickflip3_darkcatch", 3, 2), // 311
("n_kickflip4_darkcatch", 3, 2), // 312
("360flip_darkcatch", 3, 2), // 313
("360hardflip_darkcatch", 3, 2), // 314
("360inwardheelflip_darkcatch", 3, 2), // 315
("laserflip_darkcatch", 3, 2), // 316
("n_360flip_darkcatch", 3, 2), // 317
("n_360hardflip_darkcatch", 3, 2), // 318
("n_360inwardheelflip_darkcatch", 3, 2), // 319
("n_laserflip_darkcatch", 3, 2), // 320
("darktolightbsshuv", 3, 2), // 321
("darktolightfsshuv", 3, 2), // 322
("darktolightheelflip", 3, 2), // 323
("darktolightkickflip", 3, 2), // 324
("darkslideout_bsstraight", 3, 2), // 325
("darkslideout_bsleft", 3, 2), // 326
("darkslideout_bsright", 3, 2), // 327
("darkslideout_fsstraight", 3, 2), // 328
("darkslideout_fsleft", 3, 2), // 329
("darkslideout_fsright", 3, 2), // 330
("lateflip_darkcatch", 3, 2), // 331
];
+93
View File
@@ -0,0 +1,93 @@
//! Point-backed countdowns, 82DA4C28 and module82DA37B0.
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct PointTimer {
pub points: f32,
pub expired: bool,
}
impl PointTimer {
/// Returns the native near-one hold result used by the ground collector.
pub fn advance(&mut self, dt: f32, drain: f32, scale: f32, hold_near_one: bool) -> bool {
self.expired = false;
if self.points > 0.0 {
let previous = self.points;
self.points = -((drain * dt) * scale - self.points);
if hold_near_one && self.points < 1.001 && previous > 1.000001 {
self.points = previous;
return true;
}
if self.points <= 0.0 {
self.points = 0.0;
self.expired = true;
}
}
false
}
pub fn credit(&mut self, points: f32, capacity: f32) {
self.points = (self.points + points).min(capacity);
}
pub fn seconds(&self, drain: f32) -> f32 {
self.points / drain
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ComboTimer {
pub timer: PointTimer,
pub multiplier: f32,
}
impl Default for ComboTimer {
fn default() -> Self {
Self {
timer: PointTimer::default(),
multiplier: 1.0,
}
}
}
impl ComboTimer {
/// The multiplier may increase, or decrease when a sufficiently large
/// new reward authorizes refreshing it. Timer decay alone does not lower it.
pub fn credit(&mut self, reward: f32, capacity: f32, levels: [(f32, f32); 3], refresh: f32) {
self.timer.credit(reward, capacity);
let mut next = 1.0;
for (threshold, multiplier) in levels {
if self.timer.points >= threshold {
next = multiplier;
}
}
if next > self.multiplier || reward > refresh {
self.multiplier = next;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn native_ground_hold_preserves_previous_points_and_expiry_is_an_edge() {
let mut timer = PointTimer {
points: 2.0,
expired: false,
};
assert!(timer.advance(0.1, 50.0, 1.0, true));
assert_eq!(timer.points, 2.0);
assert!(!timer.advance(0.1, 50.0, 1.0, false));
assert!(timer.expired);
timer.advance(0.1, 50.0, 1.0, false);
assert!(!timer.expired);
}
#[test]
fn multiplier_uses_credited_points_and_survives_timer_decay() {
let levels = [(50.0, 1.5), (450.0, 2.0), (800.0, 3.0)];
let mut combo = ComboTimer::default();
combo.credit(50.0, 801.0, levels, 799.0);
assert_eq!(combo.multiplier, 1.5);
combo.credit(400.0, 801.0, levels, 799.0);
assert_eq!(combo.multiplier, 2.0);
combo.credit(350.0, 801.0, levels, 799.0);
assert_eq!(combo.multiplier, 3.0);
combo.timer.advance(8.0, 100.0, 1.0, false);
combo.credit(1.0, 801.0, levels, 799.0);
assert_eq!(combo.multiplier, 3.0);
}
}
@@ -0,0 +1,24 @@
//! Data-only audit: no Bevy, input polling, physics ticks or window.
fn main() -> Result<(), String> {
let root = std::env::args_os()
.nth(1)
.ok_or("Expected owned assets directory")?;
let collections = skate_data::collections::Collections::load(std::path::Path::new(&root))?;
let scoring = skate_data::scoring::ScoringData::load(&collections)?;
for id in [96, 128] {
let d = scoring
.by_id(id)
.ok_or(format!("Missing required scorable {id}"))?;
println!(
"{}: {} points; label {}; delay {}",
d.identifier, d.points, d.label, d.completion_delay
);
}
println!(
"{} authored definitions; repetition {:?}; combo levels {:?}",
scoring.definitions.len(),
scoring.repetition,
scoring.combo_levels
);
Ok(())
}
+1
View File
@@ -7,6 +7,7 @@ pub mod animation_banks;
pub mod animation_frames;
pub mod animation_metadata;
pub mod collections;
pub mod scoring;
pub mod attrib_hash;
pub mod input_config;
pub mod gesture_patterns;
+120
View File
@@ -0,0 +1,120 @@
//! VLT fields identified through Scorable82DA2340 and ScoreHolder82DA5D18.
//! No fallback point values: a missing/invalid authored field is an error.
use crate::collections::Collections;
use skate_core::{
animation::{output::attributes::AttributeName, skeleton_input::name::encode},
point_graph::PointGraph,
scoring::{Scorable, catalog::IDENTIFIERS},
};
const SCORABLE: &str = "Hash_6918469984A8C596";
const TUNING: &str = "Hash_349215E2E817703C";
#[derive(Clone, Debug)]
pub struct Definition {
pub metadata: Scorable,
pub identifier: &'static str,
pub encoded_name: AttributeName,
pub points: i32,
pub label: String,
pub trick_type: u32,
pub completion_delay: f32,
pub variant: i32,
pub flags: u32,
}
#[derive(Clone, Debug)]
pub struct ScoringData {
pub definitions: Vec<Definition>,
pub repetition: PointGraph<8>,
pub announcement: PointGraph<8>,
pub line_drain: f32,
pub line_capacity: f32,
pub combo_drain: f32,
pub combo_capacity: f32,
/// Ascending point thresholds and their authored multiplier.
pub combo_levels: [(f32, f32); 3],
pub combo_refresh_threshold: f32,
pub unannounced_factor: f32,
pub bail_factor: f32,
}
impl ScoringData {
pub fn load(data: &Collections) -> Result<Self, String> {
let mut definitions = Vec::new();
for (id, &(identifier, class, score_type)) in IDENTIFIERS.iter().enumerate() {
// The executable enum includes unused entries without VLT records.
// Only actual records are eligible for recognition.
if !data.entries().iter().any(|r| {
r.class_name == SCORABLE
&& (r.key == identifier
|| r.key == crate::attrib_hash::numeric_name(identifier))
}) {
continue;
}
let words = |name| data.words::<1>(SCORABLE, identifier, name).map(|v| v[0]);
let actual_id = words("Hash_B2383F16252DFE8E")?;
if actual_id != id as u32 {
return Err(format!(
"Scorable {identifier}: enum {id} disagrees with VLT {actual_id}"
));
}
let label = data.field(SCORABLE, identifier, "Hash_843613E915014627")?;
if label.type_name != "EA::Reflection::Text" {
return Err(format!("Invalid scorable label {identifier}"));
}
definitions.push(Definition {
metadata: Scorable {
id,
class,
score_type,
},
identifier,
encoded_name: encode(identifier.as_bytes()),
points: data.integer(SCORABLE, identifier, "Hash_937F62AE5C1ED284")? as i32,
label: label.data.clone(),
trick_type: words("TrickType")?,
completion_delay: data.float(SCORABLE, identifier, "Hash_DC7F402E5680BA02")?,
variant: words("Hash_2E90BC04042A0B5A")? as i32,
flags: words("Hash_D34A84B044B60CE3")?,
});
}
if definitions.is_empty() {
return Err("No native scorable definitions in owned VLT data".into());
}
let graph = |field| -> Result<PointGraph<8>, String> {
let w = data.words::<20>(TUNING, "default", field)?;
let x = std::array::from_fn(|i| f32::from_bits(w[4 + i]));
let y = std::array::from_fn(|i| f32::from_bits(w[12 + i]));
if x.iter().chain(y.iter()).any(|v| !v.is_finite()) || x.windows(2).any(|v| v[0] > v[1])
{
return Err(format!("Invalid native scoring curve {field}"));
}
Ok(PointGraph { x, y })
};
let f = |field| data.float(TUNING, "default", field);
Ok(Self {
definitions,
repetition: graph("Hash_59D91EAABF033A24")?,
announcement: graph("Hash_263B277F8CA17126")?,
line_drain: f("Hash_FE02A45231F7B06A")?,
line_capacity: f("Hash_57478337ACCFFF18")?,
combo_drain: f("Hash_88407506DC9780ED")?,
combo_capacity: f("Hash_28767A5F961C7129")?,
combo_levels: [
(f("Hash_829887D8C24A5B8A")?, f("Hash_4752056364FF91FE")?),
(f("Hash_FB8408A15C17A9D4")?, f("Hash_F4993A13ED7B44C1")?),
(f("Hash_30AAE071A7868771")?, f("Hash_DC0C3853F6C5FC2E")?),
],
combo_refresh_threshold: f("Hash_E36197BFC8EA1CAD")?,
unannounced_factor: f("Hash_2577DF0FCE5251E4")?,
bail_factor: f("Hash_B0B56FF046508506")?,
})
}
pub fn by_name(&self, name: AttributeName) -> Option<&Definition> {
self.definitions.iter().find(|d| d.encoded_name == name)
}
pub fn by_id(&self, id: usize) -> Option<&Definition> {
self.definitions.iter().find(|d| d.metadata.id == id)
}
}
+76
View File
@@ -0,0 +1,76 @@
# Native scoring and HUD port
Work branch: `lol/native-scoring-hud`. This document distinguishes implemented
components from remaining integration; it is not a claim of gameplay parity.
## Evidence
The source executable is the locally owned TU3 image loaded at `0x82000000`,
size `0x011B0000`, SHA-256
`f4aa113eb541bfba03dbc108cf5ab43f58c965b20fa3b82f9c40938a0ad841c4`.
Static analysis uses a disposable IDA database and the existing generated PPC
translation. Original binaries, analysis databases and extracted assets are
not part of this repository. IDA incorrectly decodes some VMX instructions;
PPC instructions and the generated translation must corroborate those paths.
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`.
* `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
entries; the owned installed database resolves 300 records.
* The scoring display/timer defaults belong to class `349215E2E817703C`.
They are global tuning, not user difficulty settings. Physical collector
tuning is a separate class `546C36B656038E04`.
* `tools/prepare_hud.py`: decodes the original `hud2/trickdisplay2` APT,
geometry, font metadata, textures and compact action stream into a private
cache. Source asset hashes are retained in the compiled manifest.
* `apt_vm`: partial bounded interpreter for the original compact instructions.
The data-only audit initializes the original class and runs its constructor
against a dummy host. This does not validate a functioning display.
## Extractor provenance
`tools/vendor/skate3_ui` reuses the Python extractor from the local Custom
Engine Layer preview.18 source distribution. Its accompanying project MIT
license is preserved. `actions.py` is the new compact instruction decoder,
based on TU3 `_parseStream` at `82E67868` and dispatch table `82FC9BF0`.
Extracted game assets are not covered by that source license and must stay in
the ignored private output directory.
Example preparation (data extraction only):
```powershell
python tools/prepare_hud.py --game <owned-game-directory> --output assets/private/hud
```
The original movie is authored at 1280 by 720. Its score, line, stance,
multiplier and trick-name timelines must control presentation; replacing them
with a new overlay would not meet this port's requirements.
## Remaining implementation
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.
The HUD still needs a complete movie object hierarchy, native bindings,
timeline execution and rendering with multiplicative and additive colors.
Function scope/preloads and supported action coverage also remain incomplete.
The audit's dummy host intentionally does not establish native binding parity.
The movie references `FuturaOuterGlow`, absent under that exact name from the
five extracted bitmap-font banks. The old extractor infers an alias to
`Futura Glow` from matching menu metrics; that is not yet evidence of native
font-server resolution. The manifest reports it as unresolved rather than
silently selecting a substitute.
Pause, map-change generation reset, resource reuse and the final task-specific
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.