mirror of
https://github.com/SK8-ENGINE/skate-3-rust-engine.git
synced 2026-10-26 19:47:44 +00:00
Port native scoring ledger, carrier lifecycle and authored tuning loader
This commit is contained in:
1 parent
43943a71bc
commit
3c535a392b
9 files changed
+1007
No files matched your search
@@ -14,4 +14,5 @@ pub mod physics;
|
||||
pub mod player;
|
||||
pub mod point_graph;
|
||||
pub mod riding;
|
||||
pub mod scoring;
|
||||
pub mod trigonometry;
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
];
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
Reference in new issue
Block a user