mirror of
https://github.com/SK8-ENGINE/skate-3-rust-engine.git
synced 2026-10-26 19:47:44 +00:00
Integrate scoring collectors and original APT HUD for interactive testing
This commit is contained in:
1 parent
cfeac6abe3
commit
2dbe4aaceb
24 files changed
+2678
-42
No files matched your search
@@ -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 conversions;
|
||||
pub mod session;
|
||||
pub mod timer;
|
||||
|
||||
@@ -64,6 +65,26 @@ impl Default for ScoreHolder {
|
||||
}
|
||||
|
||||
impl ScoreHolder {
|
||||
pub fn has_pending_sequence(&self) -> bool {
|
||||
self.pending_sequence
|
||||
}
|
||||
/// Non-air collectors use6198 then add directly to accumulator32.
|
||||
pub fn credit_trick(&mut self, scorable: Scorable, reward: f32) {
|
||||
if !scorable.valid() || self.suppressed {
|
||||
return;
|
||||
}
|
||||
self.count_trick(scorable);
|
||||
self.snapshot.accumulated += reward;
|
||||
}
|
||||
fn count_trick(&mut self, scorable: Scorable) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
pub fn set_suppressed(&mut self, suppressed: bool) {
|
||||
self.suppressed = suppressed;
|
||||
}
|
||||
@@ -75,13 +96,7 @@ impl ScoreHolder {
|
||||
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);
|
||||
}
|
||||
self.count_trick(scorable);
|
||||
let s = &mut self.snapshot;
|
||||
if scorable.class != 5 {
|
||||
s.general_pending += s.fingerflip_pending;
|
||||
|
||||
@@ -0,0 +1,335 @@
|
||||
//! Scorable descriptor conversion links at820862A8, stride24.
|
||||
pub const LINKS: [(i32, i32); 332] = [
|
||||
(-1, -1), //0
|
||||
(-1, -1), //1
|
||||
(-1, -1), //2
|
||||
(-1, -1), //3
|
||||
(-1, -1), //4
|
||||
(-1, -1), //5
|
||||
(-1, -1), //6
|
||||
(-1, -1), //7
|
||||
(-1, -1), //8
|
||||
(-1, -1), //9
|
||||
(-1, -1), //10
|
||||
(-1, -1), //11
|
||||
(-1, -1), //12
|
||||
(-1, -1), //13
|
||||
(-1, -1), //14
|
||||
(-1, -1), //15
|
||||
(-1, -1), //16
|
||||
(-1, -1), //17
|
||||
(-1, -1), //18
|
||||
(-1, -1), //19
|
||||
(-1, -1), //20
|
||||
(-1, -1), //21
|
||||
(-1, -1), //22
|
||||
(-1, -1), //23
|
||||
(-1, -1), //24
|
||||
(-1, -1), //25
|
||||
(-1, -1), //26
|
||||
(-1, -1), //27
|
||||
(-1, -1), //28
|
||||
(-1, -1), //29
|
||||
(-1, -1), //30
|
||||
(-1, -1), //31
|
||||
(-1, -1), //32
|
||||
(-1, -1), //33
|
||||
(-1, -1), //34
|
||||
(-1, -1), //35
|
||||
(-1, -1), //36
|
||||
(-1, -1), //37
|
||||
(-1, -1), //38
|
||||
(-1, -1), //39
|
||||
(-1, -1), //40
|
||||
(-1, -1), //41
|
||||
(-1, -1), //42
|
||||
(-1, -1), //43
|
||||
(-1, -1), //44
|
||||
(-1, -1), //45
|
||||
(-1, -1), //46
|
||||
(-1, -1), //47
|
||||
(-1, -1), //48
|
||||
(-1, -1), //49
|
||||
(-1, -1), //50
|
||||
(-1, -1), //51
|
||||
(-1, -1), //52
|
||||
(-1, -1), //53
|
||||
(-1, -1), //54
|
||||
(-1, -1), //55
|
||||
(-1, -1), //56
|
||||
(-1, -1), //57
|
||||
(-1, -1), //58
|
||||
(-1, -1), //59
|
||||
(-1, -1), //60
|
||||
(-1, -1), //61
|
||||
(-1, -1), //62
|
||||
(-1, -1), //63
|
||||
(-1, -1), //64
|
||||
(-1, -1), //65
|
||||
(-1, -1), //66
|
||||
(-1, -1), //67
|
||||
(-1, -1), //68
|
||||
(-1, -1), //69
|
||||
(-1, -1), //70
|
||||
(-1, -1), //71
|
||||
(-1, -1), //72
|
||||
(-1, -1), //73
|
||||
(-1, -1), //74
|
||||
(-1, -1), //75
|
||||
(-1, -1), //76
|
||||
(-1, -1), //77
|
||||
(-1, -1), //78
|
||||
(-1, -1), //79
|
||||
(-1, -1), //80
|
||||
(-1, -1), //81
|
||||
(-1, -1), //82
|
||||
(-1, -1), //83
|
||||
(-1, -1), //84
|
||||
(-1, -1), //85
|
||||
(-1, -1), //86
|
||||
(-1, -1), //87
|
||||
(-1, -1), //88
|
||||
(-1, -1), //89
|
||||
(-1, -1), //90
|
||||
(-1, -1), //91
|
||||
(-1, -1), //92
|
||||
(92, 92), //93
|
||||
(93, 92), //94
|
||||
(-1, -1), //95
|
||||
(-1, -1), //96
|
||||
(96, 96), //97
|
||||
(97, 96), //98
|
||||
(-1, -1), //99
|
||||
(-1, 128), //100
|
||||
(-1, 128), //101
|
||||
(-1, 128), //102
|
||||
(-1, 128), //103
|
||||
(-1, 128), //104
|
||||
(-1, 128), //105
|
||||
(-1, -1), //106
|
||||
(-1, -1), //107
|
||||
(-1, -1), //108
|
||||
(-1, -1), //109
|
||||
(-1, -1), //110
|
||||
(-1, -1), //111
|
||||
(-1, -1), //112
|
||||
(-1, -1), //113
|
||||
(113, 113), //114
|
||||
(114, 113), //115
|
||||
(-1, -1), //116
|
||||
(-1, -1), //117
|
||||
(117, 117), //118
|
||||
(118, 117), //119
|
||||
(-1, -1), //120
|
||||
(-1, -1), //121
|
||||
(-1, -1), //122
|
||||
(-1, -1), //123
|
||||
(-1, -1), //124
|
||||
(-1, -1), //125
|
||||
(-1, -1), //126
|
||||
(-1, -1), //127
|
||||
(-1, -1), //128
|
||||
(-1, -1), //129
|
||||
(-1, -1), //130
|
||||
(-1, -1), //131
|
||||
(-1, -1), //132
|
||||
(-1, -1), //133
|
||||
(94, 92), //134
|
||||
(98, 96), //135
|
||||
(115, 113), //136
|
||||
(119, 117), //137
|
||||
(-1, -1), //138
|
||||
(-1, -1), //139
|
||||
(-1, -1), //140
|
||||
(-1, -1), //141
|
||||
(-1, -1), //142
|
||||
(-1, -1), //143
|
||||
(-1, -1), //144
|
||||
(-1, -1), //145
|
||||
(-1, -1), //146
|
||||
(-1, -1), //147
|
||||
(-1, -1), //148
|
||||
(-1, -1), //149
|
||||
(-1, -1), //150
|
||||
(-1, -1), //151
|
||||
(-1, -1), //152
|
||||
(-1, -1), //153
|
||||
(-1, -1), //154
|
||||
(-1, -1), //155
|
||||
(-1, -1), //156
|
||||
(-1, -1), //157
|
||||
(-1, -1), //158
|
||||
(-1, -1), //159
|
||||
(-1, -1), //160
|
||||
(-1, -1), //161
|
||||
(-1, -1), //162
|
||||
(-1, -1), //163
|
||||
(-1, -1), //164
|
||||
(-1, -1), //165
|
||||
(-1, -1), //166
|
||||
(-1, -1), //167
|
||||
(-1, -1), //168
|
||||
(-1, -1), //169
|
||||
(-1, -1), //170
|
||||
(-1, -1), //171
|
||||
(-1, -1), //172
|
||||
(-1, -1), //173
|
||||
(-1, -1), //174
|
||||
(-1, -1), //175
|
||||
(-1, -1), //176
|
||||
(-1, -1), //177
|
||||
(-1, -1), //178
|
||||
(-1, -1), //179
|
||||
(-1, -1), //180
|
||||
(-1, -1), //181
|
||||
(-1, -1), //182
|
||||
(-1, -1), //183
|
||||
(-1, -1), //184
|
||||
(-1, -1), //185
|
||||
(-1, -1), //186
|
||||
(-1, -1), //187
|
||||
(-1, -1), //188
|
||||
(-1, -1), //189
|
||||
(-1, -1), //190
|
||||
(-1, -1), //191
|
||||
(-1, -1), //192
|
||||
(-1, -1), //193
|
||||
(-1, -1), //194
|
||||
(-1, -1), //195
|
||||
(-1, -1), //196
|
||||
(-1, -1), //197
|
||||
(-1, -1), //198
|
||||
(-1, -1), //199
|
||||
(-1, -1), //200
|
||||
(-1, -1), //201
|
||||
(-1, -1), //202
|
||||
(-1, -1), //203
|
||||
(-1, -1), //204
|
||||
(-1, -1), //205
|
||||
(-1, -1), //206
|
||||
(-1, -1), //207
|
||||
(-1, -1), //208
|
||||
(-1, -1), //209
|
||||
(-1, -1), //210
|
||||
(-1, -1), //211
|
||||
(-1, -1), //212
|
||||
(-1, -1), //213
|
||||
(-1, -1), //214
|
||||
(-1, -1), //215
|
||||
(-1, -1), //216
|
||||
(-1, -1), //217
|
||||
(-1, -1), //218
|
||||
(-1, -1), //219
|
||||
(-1, -1), //220
|
||||
(-1, -1), //221
|
||||
(-1, -1), //222
|
||||
(-1, -1), //223
|
||||
(-1, -1), //224
|
||||
(-1, -1), //225
|
||||
(-1, -1), //226
|
||||
(-1, -1), //227
|
||||
(-1, -1), //228
|
||||
(-1, -1), //229
|
||||
(-1, -1), //230
|
||||
(-1, -1), //231
|
||||
(-1, -1), //232
|
||||
(-1, -1), //233
|
||||
(-1, -1), //234
|
||||
(-1, -1), //235
|
||||
(-1, -1), //236
|
||||
(-1, -1), //237
|
||||
(-1, -1), //238
|
||||
(-1, -1), //239
|
||||
(-1, -1), //240
|
||||
(-1, -1), //241
|
||||
(-1, -1), //242
|
||||
(-1, -1), //243
|
||||
(-1, -1), //244
|
||||
(-1, -1), //245
|
||||
(-1, -1), //246
|
||||
(-1, -1), //247
|
||||
(-1, -1), //248
|
||||
(-1, -1), //249
|
||||
(-1, -1), //250
|
||||
(-1, -1), //251
|
||||
(-1, -1), //252
|
||||
(-1, -1), //253
|
||||
(-1, -1), //254
|
||||
(-1, -1), //255
|
||||
(-1, -1), //256
|
||||
(-1, -1), //257
|
||||
(-1, -1), //258
|
||||
(-1, -1), //259
|
||||
(-1, -1), //260
|
||||
(-1, -1), //261
|
||||
(-1, -1), //262
|
||||
(-1, -1), //263
|
||||
(-1, -1), //264
|
||||
(-1, -1), //265
|
||||
(-1, -1), //266
|
||||
(-1, -1), //267
|
||||
(-1, -1), //268
|
||||
(-1, -1), //269
|
||||
(-1, -1), //270
|
||||
(-1, -1), //271
|
||||
(-1, -1), //272
|
||||
(-1, -1), //273
|
||||
(-1, -1), //274
|
||||
(-1, -1), //275
|
||||
(-1, -1), //276
|
||||
(-1, -1), //277
|
||||
(-1, -1), //278
|
||||
(-1, -1), //279
|
||||
(-1, -1), //280
|
||||
(-1, -1), //281
|
||||
(-1, -1), //282
|
||||
(-1, -1), //283
|
||||
(-1, -1), //284
|
||||
(-1, -1), //285
|
||||
(-1, -1), //286
|
||||
(-1, -1), //287
|
||||
(-1, -1), //288
|
||||
(-1, -1), //289
|
||||
(-1, -1), //290
|
||||
(-1, -1), //291
|
||||
(-1, -1), //292
|
||||
(-1, -1), //293
|
||||
(-1, -1), //294
|
||||
(-1, -1), //295
|
||||
(-1, -1), //296
|
||||
(-1, -1), //297
|
||||
(-1, -1), //298
|
||||
(-1, -1), //299
|
||||
(-1, -1), //300
|
||||
(-1, -1), //301
|
||||
(-1, -1), //302
|
||||
(-1, -1), //303
|
||||
(-1, -1), //304
|
||||
(-1, -1), //305
|
||||
(-1, -1), //306
|
||||
(-1, -1), //307
|
||||
(-1, -1), //308
|
||||
(-1, -1), //309
|
||||
(-1, -1), //310
|
||||
(-1, -1), //311
|
||||
(-1, -1), //312
|
||||
(-1, -1), //313
|
||||
(-1, -1), //314
|
||||
(-1, -1), //315
|
||||
(-1, -1), //316
|
||||
(-1, -1), //317
|
||||
(-1, -1), //318
|
||||
(-1, -1), //319
|
||||
(-1, -1), //320
|
||||
(-1, -1), //321
|
||||
(-1, -1), //322
|
||||
(-1, -1), //323
|
||||
(-1, -1), //324
|
||||
(-1, -1), //325
|
||||
(-1, -1), //326
|
||||
(-1, -1), //327
|
||||
(-1, -1), //328
|
||||
(-1, -1), //329
|
||||
(-1, -1), //330
|
||||
(-1, -1), //331
|
||||
];
|
||||
@@ -0,0 +1,102 @@
|
||||
//! Executes owned movie bytecode against a data-only host. No game or renderer.
|
||||
#[path = "../../skate-game/src/apt_display.rs"]
|
||||
mod apt_display;
|
||||
#[path = "../../skate-game/src/apt_movie.rs"]
|
||||
mod apt_movie;
|
||||
#[path = "../../skate-game/src/apt_text.rs"]
|
||||
mod apt_text;
|
||||
#[path = "../../skate-game/src/apt_vm.rs"]
|
||||
mod apt_vm;
|
||||
use apt_vm::*;
|
||||
struct Audit {
|
||||
movie: apt_movie::Movie,
|
||||
}
|
||||
impl Host for Audit {
|
||||
fn call(
|
||||
&mut self,
|
||||
vm: &mut Vm,
|
||||
object: usize,
|
||||
method: &str,
|
||||
args: Vec<Value>,
|
||||
) -> Result<Value, String> {
|
||||
if self.movie.method(vm, object, method, &args)? {
|
||||
return Ok(Value::Undefined);
|
||||
}
|
||||
match method {
|
||||
"ASSetPropFlags" => Ok(Value::Undefined),
|
||||
"GetLineTimerMaxPoints" => Ok(Value::Number(400.0)),
|
||||
"GetLineTimeRemaining" | "GetLineScore" => Ok(Value::Number(0.0)),
|
||||
"GetLanguage" => Ok(Value::Text("english".into())),
|
||||
_ => Err(format!(
|
||||
"Unimplemented audit binding {object}.{method}({args:?})"
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
fn drain(vm: &mut Vm, host: &mut Audit) -> Result<(), String> {
|
||||
let mut count = 0;
|
||||
while let Some((object, offset)) = host.movie.pending.pop_front() {
|
||||
count += 1;
|
||||
if count > 4096 {
|
||||
return Err("Frame action limit".into());
|
||||
}
|
||||
if !host.movie.instances.contains_key(&object) {
|
||||
continue;
|
||||
}
|
||||
let code = host
|
||||
.movie
|
||||
.actions
|
||||
.get(&offset.to_string())
|
||||
.ok_or("Missing movie action block")?
|
||||
.clone();
|
||||
vm.run_on(object, &code, host)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn main() -> Result<(), String> {
|
||||
let path = std::env::args_os()
|
||||
.nth(1)
|
||||
.ok_or("Expected owned trickdisplay.json")?;
|
||||
let json: serde_json::Value =
|
||||
serde_json::from_slice(&std::fs::read(path).map_err(|e| e.to_string())?)
|
||||
.map_err(|e| e.to_string())?;
|
||||
let mut host = Audit {
|
||||
movie: apt_movie::Movie::load(&json)?,
|
||||
};
|
||||
let mut vm = Vm::new();
|
||||
for name in ["MovieClip", "Tricks", "FELanguage"] {
|
||||
let object = vm.object(ObjectKind::Native(name.into()));
|
||||
let proto = vm.object(ObjectKind::Plain);
|
||||
vm.set(object, "prototype", Value::Object(proto))?;
|
||||
vm.set(vm.global, name, Value::Object(object))?;
|
||||
}
|
||||
vm.set(vm.global, "Screen_EdgeOffset", Value::Number(0.0))?;
|
||||
for c in host.movie.characters.values().cloned().collect::<Vec<_>>() {
|
||||
for frame in c.frames {
|
||||
for control in frame.controls {
|
||||
if control.type_name == "do_init_action" {
|
||||
let code = host.movie.actions[&control.actions_offset.to_string()].clone();
|
||||
vm.run(&code, &mut host)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
vm.begin_update();
|
||||
host.movie.initialize(&mut vm)?;
|
||||
drain(&mut vm, &mut host)?;
|
||||
let Value::Object(controller) = vm.get(host.movie.root, "screen") else {
|
||||
return Err("Root action did not construct controller".into());
|
||||
};
|
||||
vm.call_method(controller, "ScreenShow", vec![Value::Bool(true)], &mut host)?;
|
||||
drain(&mut vm, &mut host)?;
|
||||
for _ in 0..90 {
|
||||
vm.begin_update();
|
||||
host.movie.advance(&mut vm)?;
|
||||
drain(&mut vm, &mut host)?;
|
||||
}
|
||||
println!(
|
||||
"Original movie initialized and advanced 90 authored frames; {} retained display instances",
|
||||
host.movie.instances.len()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
//! Original action-stream validation with supplied data, without game systems.
|
||||
#[path = "../../skate-game/src/apt_display.rs"]
|
||||
mod apt_display;
|
||||
#[path = "../../skate-game/src/apt_movie.rs"]
|
||||
mod apt_movie;
|
||||
#[path = "../../skate-game/src/apt_scene.rs"]
|
||||
mod apt_scene;
|
||||
#[path = "../../skate-game/src/apt_text.rs"]
|
||||
mod apt_text;
|
||||
#[path = "../../skate-game/src/apt_vm.rs"]
|
||||
mod apt_vm;
|
||||
#[path = "../../skate-game/src/hud_runtime.rs"]
|
||||
mod hud_runtime;
|
||||
use apt_vm::Value;
|
||||
fn main() -> Result<(), String> {
|
||||
let path = std::env::args_os()
|
||||
.nth(1)
|
||||
.ok_or("Expected owned trickdisplay.json")?;
|
||||
let json: serde_json::Value =
|
||||
serde_json::from_slice(&std::fs::read(path).map_err(|e| e.to_string())?)
|
||||
.map_err(|e| e.to_string())?;
|
||||
let mut input = hud_runtime::Input {
|
||||
sequence_score: 0,
|
||||
line_score: 0,
|
||||
sequence_timer: 0,
|
||||
line_time: 0.0,
|
||||
line_capacity: 400.0,
|
||||
multiplier: 1.0,
|
||||
clean: false,
|
||||
sketchy: false,
|
||||
stance: [false; 4],
|
||||
trick_name: String::new(),
|
||||
trick_metrics: std::array::from_fn(|_| Value::Undefined),
|
||||
context_tricks: Vec::new(),
|
||||
};
|
||||
let mut runtime = hud_runtime::Runtime::load(&json, input.clone())?;
|
||||
let shapes: apt_scene::Shapes =
|
||||
serde_json::from_value(json["shapes"].clone()).map_err(|e| e.to_string())?;
|
||||
let mut max_draws = 0;
|
||||
for tick in 0..1800 {
|
||||
let new_trick = tick % 180 == 10;
|
||||
if new_trick {
|
||||
input.trick_name = "Kickflip".into();
|
||||
input.trick_metrics = [
|
||||
Value::Text(input.trick_name.clone()),
|
||||
Value::Number(0.0),
|
||||
Value::Number(0.0),
|
||||
Value::Bool(false),
|
||||
Value::Bool(false),
|
||||
];
|
||||
input.sequence_score += 100;
|
||||
input.line_score += 100;
|
||||
input.clean = tick % 360 == 10;
|
||||
input.sketchy = !input.clean;
|
||||
input.line_time = 400.0;
|
||||
input.multiplier = if input.multiplier == 1.5 { 2.0 } else { 1.5 };
|
||||
}
|
||||
input.line_time = (input.line_time - 1.0).max(0.0);
|
||||
runtime
|
||||
.update(input.clone(), new_trick, false, tick % 180 == 120)
|
||||
.map_err(|e| format!("Data frame {tick}: {e}"))?;
|
||||
let draws = apt_scene::draw(&runtime.bindings.movie, &runtime.vm, &shapes)?;
|
||||
max_draws = max_draws.max(draws.len());
|
||||
if draws
|
||||
.iter()
|
||||
.flat_map(|d| &d.vertices)
|
||||
.any(|v| v.position.iter().chain(v.uv.iter()).any(|x| !x.is_finite()))
|
||||
{
|
||||
return Err("Nonfinite authored draw geometry".into());
|
||||
}
|
||||
}
|
||||
println!(
|
||||
"Original HUD action audit passed 1800 authored frames; {} object slots, {} display instances",
|
||||
runtime.vm.objects.len(),
|
||||
runtime.bindings.movie.instances.len()
|
||||
);
|
||||
println!("Validated glyph/shape traversal: maximum {max_draws} draw batches");
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
//! Original action-stream validation with supplied data, without game systems.
|
||||
#[path = "../../skate-game/src/apt_display.rs"]
|
||||
mod apt_display;
|
||||
#[path = "../../skate-game/src/apt_movie.rs"]
|
||||
mod apt_movie;
|
||||
#[path = "../../skate-game/src/apt_scene.rs"]
|
||||
mod apt_scene;
|
||||
#[path = "../../skate-game/src/apt_text.rs"]
|
||||
mod apt_text;
|
||||
#[path = "../../skate-game/src/apt_vm.rs"]
|
||||
mod apt_vm;
|
||||
#[path = "../../skate-game/src/hud_runtime.rs"]
|
||||
mod hud_runtime;
|
||||
#[path = "../../skate-game/src/scoring_runtime.rs"]
|
||||
mod scoring_runtime;
|
||||
use skate_core::{
|
||||
animation::output::attributes::AttributeName, physics::filtered_state::FilteredCategory,
|
||||
};
|
||||
fn frame(
|
||||
tick: u32,
|
||||
category: FilteredCategory,
|
||||
descriptor: Option<AttributeName>,
|
||||
) -> scoring_runtime::Frame {
|
||||
scoring_runtime::Frame {
|
||||
tick,
|
||||
dt: 1. / 60.,
|
||||
category,
|
||||
state: 100,
|
||||
descriptor,
|
||||
grind_id: -1,
|
||||
flags: 0,
|
||||
position: [0., 0., 0.],
|
||||
velocity: [0., 0., 5.],
|
||||
forward: [0., 0., 1.],
|
||||
switch: false,
|
||||
fakie: false,
|
||||
nollie: false,
|
||||
body_flip: false,
|
||||
landing: Default::default(),
|
||||
teleported: false,
|
||||
reverting: false,
|
||||
}
|
||||
}
|
||||
fn main() -> Result<(), String> {
|
||||
let root = std::env::args_os()
|
||||
.nth(1)
|
||||
.ok_or("Expected owned assets directory")?;
|
||||
let data = skate_data::collections::Collections::load(std::path::Path::new(&root))?;
|
||||
let mut scoring = scoring_runtime::Runtime::load(&data)?;
|
||||
let kickflip = scoring
|
||||
.data
|
||||
.by_id(96)
|
||||
.ok_or("Missing kickflip")?
|
||||
.encoded_name;
|
||||
scoring.advance(frame(0, FilteredCategory::Ground, None))?;
|
||||
for tick in 1..61 {
|
||||
scoring.advance(frame(tick, FilteredCategory::Air, Some(kickflip)))?;
|
||||
}
|
||||
for tick in 61..70 {
|
||||
scoring.advance(frame(tick, FilteredCategory::Ground, None))?;
|
||||
}
|
||||
let banked = scoring.session.holder.snapshot.last_reward;
|
||||
if banked != 100. {
|
||||
return Err(format!(
|
||||
"Stationary unswitched kickflip, no landing bonus: expected authored 100, got {banked}"
|
||||
));
|
||||
}
|
||||
for tick in 70..80 {
|
||||
scoring.advance(frame(tick, FilteredCategory::Ground, None))?;
|
||||
}
|
||||
if scoring.session.holder.snapshot.last_reward != banked {
|
||||
return Err("Idle frame published the sequence again".into());
|
||||
}
|
||||
for tick in 80..140 {
|
||||
scoring.advance(frame(tick, FilteredCategory::Air, Some(kickflip)))?;
|
||||
}
|
||||
let mut cancelled = frame(140, FilteredCategory::Ground, None);
|
||||
cancelled.teleported = true;
|
||||
scoring.advance(cancelled)?;
|
||||
if scoring.session.holder.snapshot.last_reward != 0.
|
||||
|| scoring.session.holder.has_pending_sequence()
|
||||
{
|
||||
return Err("Teleport retained pending trick rewards".into());
|
||||
}
|
||||
if scoring.session.combo.multiplier != 1. {
|
||||
return Err("Teleport retained multiplier".into());
|
||||
}
|
||||
println!(
|
||||
"Scoring data audit: authored kickflip credited once; idle publication stable; teleport cancels pending rewards and multiplier"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
@@ -8,6 +8,7 @@ pub mod animation_frames;
|
||||
pub mod animation_metadata;
|
||||
pub mod collections;
|
||||
pub mod scoring;
|
||||
mod scoring_fields;
|
||||
pub mod attrib_hash;
|
||||
pub mod input_config;
|
||||
pub mod gesture_patterns;
|
||||
|
||||
@@ -25,6 +25,7 @@ pub struct Definition {
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ScoringData {
|
||||
pub collector: CollectorTuning,
|
||||
pub definitions: Vec<Definition>,
|
||||
pub repetition: PointGraph<8>,
|
||||
pub announcement: PointGraph<8>,
|
||||
@@ -37,6 +38,7 @@ pub struct ScoringData {
|
||||
pub combo_refresh_threshold: f32,
|
||||
pub unannounced_factor: f32,
|
||||
pub bail_factor: f32,
|
||||
pub sketchy_side_speed: f32,
|
||||
}
|
||||
|
||||
impl ScoringData {
|
||||
@@ -103,6 +105,7 @@ impl ScoringData {
|
||||
};
|
||||
let f = |field| data.float(TUNING, "default", field);
|
||||
Ok(Self {
|
||||
collector: CollectorTuning::load(data)?,
|
||||
definitions,
|
||||
repetition: graph("Hash_59D91EAABF033A24")?,
|
||||
announcement: graph("Hash_263B277F8CA17126")?,
|
||||
@@ -118,6 +121,7 @@ impl ScoringData {
|
||||
combo_refresh_threshold: f("Hash_E36197BFC8EA1CAD")?,
|
||||
unannounced_factor: f("Hash_2577DF0FCE5251E4")?,
|
||||
bail_factor: f("Hash_B0B56FF046508506")?,
|
||||
sketchy_side_speed: f("Hash_6DC5982591F80A4C")?,
|
||||
})
|
||||
}
|
||||
pub fn by_name(&self, name: AttributeName) -> Option<&Definition> {
|
||||
@@ -127,3 +131,41 @@ impl ScoringData {
|
||||
self.definitions.iter().find(|d| d.metadata.id == id)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CollectorTuning {
|
||||
scalars: std::collections::BTreeMap<u16, f32>,
|
||||
curves: std::collections::BTreeMap<u16, PointGraph<8>>,
|
||||
}
|
||||
impl CollectorTuning {
|
||||
fn load(data: &Collections) -> Result<Self, String> {
|
||||
let mut out = Self {
|
||||
scalars: Default::default(),
|
||||
curves: Default::default(),
|
||||
};
|
||||
for &(offset, field, size) in crate::scoring_fields::FIELDS {
|
||||
if size == 4 {
|
||||
out.scalars.insert(
|
||||
offset,
|
||||
data.float("Hash_546C36B656038E04", "default", field)?,
|
||||
);
|
||||
} else {
|
||||
let w = data.words::<20>("Hash_546C36B656038E04", "default", field)?;
|
||||
out.curves.insert(
|
||||
offset,
|
||||
PointGraph {
|
||||
x: std::array::from_fn(|i| f32::from_bits(w[4 + i])),
|
||||
y: std::array::from_fn(|i| f32::from_bits(w[12 + i])),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
pub fn scalar(&self, native_offset: u16) -> f32 {
|
||||
self.scalars[&native_offset]
|
||||
}
|
||||
pub fn curve(&self, native_offset: u16, input: f32) -> f32 {
|
||||
self.curves[&native_offset].evaluate(input)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
//! Score collector VLT class546C36B656038E04 schema layout.
|
||||
pub const FIELDS: &[(u16, &str, usize)] = &[
|
||||
(0x000, "Hash_8D544250D732B9A8", 80),
|
||||
(0x050, "Hash_130B361B9E4D9369", 80),
|
||||
(0x0a0, "Hash_156F53F56E861668", 80),
|
||||
(0x0f0, "Hash_6DA2DFA9C8E3D863", 80),
|
||||
(0x140, "Hash_9C9EFF9DB25BF70A", 80),
|
||||
(0x190, "Hash_004A238881254FB5", 80),
|
||||
(0x1e0, "Hash_198F48320EBB7C3A", 80),
|
||||
(0x230, "Hash_F94C77A0FB410FE3", 80),
|
||||
(0x280, "Hash_D7FBBD6E5E1EDF37", 80),
|
||||
(0x2d0, "Hash_E78EDEC321055E09", 80),
|
||||
(0x320, "Hash_8C24CF7B8227B348", 80),
|
||||
(0x370, "Hash_5A756D4B3A5036DD", 80),
|
||||
(0x3c0, "Hash_B552FB2712A897D6", 80),
|
||||
(0x410, "Hash_205C814AA3ACA8D4", 80),
|
||||
(0x460, "Hash_94F7B93068FE6FB1", 80),
|
||||
(0x4b0, "Hash_7E17CC5298BBA36E", 80),
|
||||
(0x500, "Hash_2D52116C69E3E1BE", 80),
|
||||
(0x550, "Hash_1F647002596E4157", 80),
|
||||
(0x5a0, "Hash_91529F3BD9F4DD25", 80),
|
||||
(0x5f0, "Hash_66216D696EEAD314", 4),
|
||||
(0x5f4, "Hash_3A444577C4D4661E", 4),
|
||||
(0x5f8, "Hash_B26A66F7634E760E", 4),
|
||||
(0x5fc, "Hash_BFDA1A507E2E55A1", 4),
|
||||
(0x600, "Hash_1CA631042D292121", 4),
|
||||
(0x604, "Hash_04A1775CB52A8E4D", 4),
|
||||
(0x608, "Hash_AD14550EFF3920DE", 4),
|
||||
(0x60c, "Hash_4BEA9C285A3CDA06", 4),
|
||||
(0x610, "Hash_F813B960D0450300", 4),
|
||||
(0x614, "Hash_391821526C05A7F1", 4),
|
||||
(0x618, "Hash_B3C69AAF221F34F9", 4),
|
||||
(0x61c, "Hash_E2AFE1B06292209D", 4),
|
||||
(0x620, "Hash_62B9C3DAA846E240", 4),
|
||||
(0x624, "Hash_B0D7A3876D2D274E", 4),
|
||||
(0x628, "Hash_77DD0728B4736957", 4),
|
||||
(0x62c, "Hash_05BBAF2E866E083E", 4),
|
||||
(0x630, "Hash_5A2D1476F08690EC", 4),
|
||||
(0x634, "Hash_001BF89D79FA1CCB", 4),
|
||||
(0x638, "Hash_40E8618821693CD3", 4),
|
||||
(0x63c, "Hash_25B11E405C140E61", 4),
|
||||
(0x640, "Hash_694410FC8D57E794", 4),
|
||||
(0x644, "Hash_2BE55EDC393F3D9B", 4),
|
||||
(0x648, "Hash_F1F451420D10D2C5", 4),
|
||||
(0x64c, "Hash_3BD67FAE1E671F26", 4),
|
||||
(0x650, "Hash_F577378023AADAA2", 4),
|
||||
(0x654, "Hash_16CB3F5D3D560676", 4),
|
||||
(0x658, "Hash_67EBCA6DBB3EE9F1", 4),
|
||||
(0x65c, "Hash_BA0B9725B1F7ABA7", 4),
|
||||
(0x660, "Hash_C5BEC05471041171", 4),
|
||||
(0x664, "Hash_BC91E9C84E099F2C", 4),
|
||||
(0x668, "Hash_C33E4AA507A2CB4C", 4),
|
||||
(0x66c, "Hash_AB8EACF72B291746", 4),
|
||||
(0x670, "Hash_E5897F067B3C4F77", 4),
|
||||
(0x674, "Hash_2B55A2C725912CBF", 4),
|
||||
(0x678, "Hash_092D5E5FDE988D1B", 4),
|
||||
(0x67c, "Hash_44746535286E1613", 4),
|
||||
(0x680, "Hash_0BA6F149F1462EB0", 4),
|
||||
(0x684, "Hash_EE42BA52A1313FDB", 4),
|
||||
(0x688, "Hash_945369EBD1E9A062", 4),
|
||||
(0x68c, "Hash_123E054E8895CAFE", 4),
|
||||
(0x690, "Hash_63E61D37475E6AB9", 4),
|
||||
(0x694, "Hash_AC54DE5C98B5DFAD", 4),
|
||||
(0x698, "Hash_C4BDC0CD02C83A58", 4),
|
||||
];
|
||||
@@ -105,6 +105,7 @@ pub(crate) fn build(
|
||||
crate::render_capacity::RenderCapacityPlugin,
|
||||
verification::VerificationPlugin,
|
||||
crate::performance::PerformancePlugin,
|
||||
));
|
||||
))
|
||||
.add_plugins(crate::scoring_hud::ScoringHudPlugin);
|
||||
app
|
||||
}
|
||||
@@ -5,6 +5,7 @@ use std::collections::BTreeMap;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct Control {
|
||||
pub label: Option<String>,
|
||||
pub type_name: String,
|
||||
#[serde(default)]
|
||||
pub flags: u32,
|
||||
|
||||
@@ -0,0 +1,320 @@
|
||||
//! Original APT movie hierarchy and timeline control, independent of Bevy.
|
||||
use crate::{
|
||||
apt_display::{Control, DisplayList, Placement},
|
||||
apt_vm::{Instruction, ObjectKind, Value, Vm},
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use std::collections::{BTreeMap, VecDeque};
|
||||
|
||||
#[derive(Clone, Deserialize)]
|
||||
pub struct Frame {
|
||||
pub controls: Vec<Control>,
|
||||
}
|
||||
#[derive(Clone, Deserialize)]
|
||||
pub struct Character {
|
||||
pub id: i32,
|
||||
pub type_name: String,
|
||||
#[serde(default)]
|
||||
pub frames: Vec<Frame>,
|
||||
pub text: Option<serde_json::Value>,
|
||||
pub bounds: Option<[f32; 4]>,
|
||||
}
|
||||
#[derive(Clone)]
|
||||
pub struct Instance {
|
||||
pub character: i32,
|
||||
pub frame: usize,
|
||||
pub playing: bool,
|
||||
pub children: BTreeMap<i32, usize>,
|
||||
pub placement: Option<Placement>,
|
||||
}
|
||||
pub struct Movie {
|
||||
pub characters: BTreeMap<i32, Character>,
|
||||
pub instances: BTreeMap<usize, Instance>,
|
||||
pub actions: BTreeMap<String, Vec<Instruction>>,
|
||||
pub pending: VecDeque<(usize, u32)>,
|
||||
pub root: usize,
|
||||
pub text_assets: crate::apt_text::TextAssets,
|
||||
states: BTreeMap<i32, Vec<DisplayList>>,
|
||||
}
|
||||
impl Movie {
|
||||
pub fn load(json: &serde_json::Value) -> Result<Self, String> {
|
||||
let characters: Vec<Character> =
|
||||
serde_json::from_value(json["characters"].clone()).map_err(|e| e.to_string())?;
|
||||
let mut states = BTreeMap::new();
|
||||
for c in &characters {
|
||||
let mut list = DisplayList::default();
|
||||
let mut frames = Vec::new();
|
||||
for frame in &c.frames {
|
||||
for control in &frame.controls {
|
||||
list.apply(control)?;
|
||||
}
|
||||
frames.push(list.clone());
|
||||
}
|
||||
states.insert(c.id, frames);
|
||||
}
|
||||
Ok(Self {
|
||||
characters: characters.into_iter().map(|c| (c.id, c)).collect(),
|
||||
instances: BTreeMap::new(),
|
||||
actions: serde_json::from_value(json["actions"].clone()).map_err(|e| e.to_string())?,
|
||||
pending: VecDeque::new(),
|
||||
root: usize::MAX,
|
||||
text_assets: crate::apt_text::TextAssets::load(json)?,
|
||||
states,
|
||||
})
|
||||
}
|
||||
pub fn initialize(&mut self, vm: &mut Vm) -> Result<(), String> {
|
||||
self.root = self.create(vm, 0, None, 0)?;
|
||||
vm.set(self.root, "_root", Value::Object(self.root))?;
|
||||
// Every clip sees the same authored root, including unnamed children.
|
||||
for id in self.instances.keys() {
|
||||
vm.set(*id, "_root", Value::Object(self.root))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn create(
|
||||
&mut self,
|
||||
vm: &mut Vm,
|
||||
character: i32,
|
||||
parent: Option<usize>,
|
||||
depth: usize,
|
||||
) -> Result<usize, String> {
|
||||
if depth > 32 || self.instances.len() > 2048 {
|
||||
return Err("APT movie hierarchy limit".into());
|
||||
}
|
||||
let c = self
|
||||
.characters
|
||||
.get(&character)
|
||||
.ok_or("APT unknown character")?
|
||||
.clone();
|
||||
let id = vm.object(ObjectKind::Native(format!("movie:{character}")));
|
||||
if self.root != usize::MAX {
|
||||
vm.set(id, "_root", Value::Object(self.root))?;
|
||||
}
|
||||
if let Some(parent) = parent {
|
||||
vm.set(id, "_parent", Value::Object(parent))?;
|
||||
}
|
||||
vm.set(id, "_x", Value::Number(0.0))?;
|
||||
vm.set(id, "_y", Value::Number(0.0))?;
|
||||
vm.set(id, "_visible", Value::Bool(true))?;
|
||||
vm.set(id, "_alpha", Value::Number(100.0))?;
|
||||
if let Some(text) = &c.text {
|
||||
vm.set(
|
||||
id,
|
||||
"text",
|
||||
Value::Text(text["initial_text"].as_str().unwrap_or("").into()),
|
||||
)?;
|
||||
}
|
||||
self.instances.insert(
|
||||
id,
|
||||
Instance {
|
||||
character,
|
||||
frame: 0,
|
||||
playing: !c.frames.is_empty(),
|
||||
children: BTreeMap::new(),
|
||||
placement: None,
|
||||
},
|
||||
);
|
||||
self.text_changed(vm, id)?;
|
||||
if !c.frames.is_empty() {
|
||||
self.seek(vm, id, 0, depth + 1)?;
|
||||
}
|
||||
Ok(id)
|
||||
}
|
||||
pub fn text_changed(&self, vm: &mut Vm, id: usize) -> Result<(), String> {
|
||||
let Some(instance) = self.instances.get(&id) else {
|
||||
return Ok(());
|
||||
};
|
||||
let character = &self.characters[&instance.character];
|
||||
let Some(text) = &character.text else {
|
||||
return Ok(());
|
||||
};
|
||||
let value = self.text_assets.localize(&vm.get(id, "text").text());
|
||||
vm.set(id, "_displayText", Value::Text(value.clone()))?;
|
||||
let font = self
|
||||
.text_assets
|
||||
.fonts
|
||||
.get(&(text["font_id"].as_i64().ok_or("Invalid text font id")? as i32))
|
||||
.ok_or("Missing original text font")?;
|
||||
let height = text["font_height"].as_f64().ok_or("Invalid text size")? as f32;
|
||||
let width = font.width(&value, height);
|
||||
vm.set(id, "textWidth", Value::Number(width as f64))?;
|
||||
let bounds = character.bounds.ok_or("Missing text bounds")?;
|
||||
let autosize = vm.get(id, "autoSize").text();
|
||||
vm.set(
|
||||
id,
|
||||
"_width",
|
||||
Value::Number(
|
||||
if autosize == "left" || autosize == "right" || autosize == "center" {
|
||||
width
|
||||
} else {
|
||||
bounds[2] - bounds[0]
|
||||
} as f64,
|
||||
),
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
fn remove(&mut self, vm: &mut Vm, id: usize) {
|
||||
if let Some(instance) = self.instances.remove(&id) {
|
||||
for child in instance.children.values() {
|
||||
self.remove(vm, *child);
|
||||
}
|
||||
}
|
||||
// Retired handles can remain referenced by ActionScript, but no longer
|
||||
// participate in timeline advancement or rendering.
|
||||
let _ = vm.set(id, "_visible", Value::Bool(false));
|
||||
}
|
||||
pub fn seek(
|
||||
&mut self,
|
||||
vm: &mut Vm,
|
||||
id: usize,
|
||||
frame: usize,
|
||||
nesting: usize,
|
||||
) -> Result<(), String> {
|
||||
let instance = self
|
||||
.instances
|
||||
.get(&id)
|
||||
.ok_or("APT absent movie instance")?
|
||||
.clone();
|
||||
let character = self
|
||||
.characters
|
||||
.get(&instance.character)
|
||||
.ok_or("APT absent movie character")?;
|
||||
if frame >= character.frames.len() {
|
||||
return Err(format!(
|
||||
"APT frame {frame} outside character {}",
|
||||
character.id
|
||||
));
|
||||
}
|
||||
let frame_count = character.frames.len();
|
||||
let actions: Vec<_> = character.frames[frame]
|
||||
.controls
|
||||
.iter()
|
||||
.filter(|c| c.type_name == "do_action" && c.actions_offset != 0)
|
||||
.map(|c| c.actions_offset)
|
||||
.collect();
|
||||
let list = self.states[&instance.character][frame].clone();
|
||||
let mut children = BTreeMap::new();
|
||||
for (depth, placement) in list.depths {
|
||||
let previous = instance.children.get(&depth).copied();
|
||||
if let Some(name) = previous
|
||||
.and_then(|old| self.instances.get(&old))
|
||||
.and_then(|i| i.placement.as_ref())
|
||||
.map(|p| p.name.clone())
|
||||
{
|
||||
if !name.is_empty() && name != placement.name {
|
||||
vm.objects[id].fields.remove(&name);
|
||||
}
|
||||
}
|
||||
let child = if let Some(old) = previous.filter(|old| {
|
||||
self.instances
|
||||
.get(old)
|
||||
.is_some_and(|i| i.character == placement.character)
|
||||
}) {
|
||||
old
|
||||
} else {
|
||||
if let Some(old) = previous {
|
||||
self.remove(vm, old);
|
||||
}
|
||||
self.create(vm, placement.character, Some(id), nesting + 1)?
|
||||
};
|
||||
let old = self.instances[&child].placement.as_ref();
|
||||
if old.is_none_or(|old| old.matrix != placement.matrix) {
|
||||
vm.set(child, "_x", Value::Number(placement.matrix[4] as f64))?;
|
||||
vm.set(child, "_y", Value::Number(placement.matrix[5] as f64))?;
|
||||
}
|
||||
if !placement.name.is_empty() {
|
||||
vm.set(id, &placement.name, Value::Object(child))?;
|
||||
}
|
||||
self.instances.get_mut(&child).unwrap().placement = Some(placement);
|
||||
children.insert(depth, child);
|
||||
}
|
||||
for (depth, child) in &instance.children {
|
||||
if !children.contains_key(depth) {
|
||||
if let Some(name) = self
|
||||
.instances
|
||||
.get(child)
|
||||
.and_then(|i| i.placement.as_ref())
|
||||
.map(|p| p.name.clone())
|
||||
{
|
||||
if !name.is_empty() {
|
||||
vm.objects[id].fields.remove(&name);
|
||||
}
|
||||
}
|
||||
self.remove(vm, *child);
|
||||
}
|
||||
}
|
||||
let current = self.instances.get_mut(&id).unwrap();
|
||||
current.frame = frame;
|
||||
current.children = children;
|
||||
vm.set(id, "_currentframe", Value::Number((frame + 1) as f64))?;
|
||||
vm.set(id, "_totalframes", Value::Number(frame_count as f64))?;
|
||||
for offset in actions {
|
||||
self.pending.push_back((id, offset));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
pub fn advance(&mut self, vm: &mut Vm) -> Result<(), String> {
|
||||
let playing: Vec<_> = self
|
||||
.instances
|
||||
.iter()
|
||||
.filter(|(_, i)| i.playing)
|
||||
.map(|(id, i)| (*id, i.character, i.frame))
|
||||
.collect();
|
||||
for (id, character, frame) in playing {
|
||||
if !self.instances.contains_key(&id) {
|
||||
continue;
|
||||
}
|
||||
let count = self.characters[&character].frames.len();
|
||||
if count > 1 {
|
||||
self.seek(vm, id, (frame + 1) % count, 0)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
pub fn method(
|
||||
&mut self,
|
||||
vm: &mut Vm,
|
||||
id: usize,
|
||||
method: &str,
|
||||
args: &[Value],
|
||||
) -> Result<bool, String> {
|
||||
if !self.instances.contains_key(&id) {
|
||||
return Ok(false);
|
||||
}
|
||||
match method {
|
||||
"stop" => self.instances.get_mut(&id).unwrap().playing = false,
|
||||
"play" => self.instances.get_mut(&id).unwrap().playing = true,
|
||||
"gotoAndPlay" | "gotoAndStop" => {
|
||||
let c = &self.characters[&self.instances[&id].character];
|
||||
let frame = match args.first().ok_or("APT goto requires frame")? {
|
||||
Value::Text(label) => c
|
||||
.frames
|
||||
.iter()
|
||||
.position(|f| {
|
||||
f.controls.iter().any(|control| {
|
||||
control.type_name == "frame_label"
|
||||
&& control.label.as_deref() == Some(label)
|
||||
})
|
||||
})
|
||||
.ok_or_else(|| format!("APT character {} lacks label {label}", c.id))?,
|
||||
value => {
|
||||
let frame = value.number();
|
||||
if !frame.is_finite() || frame < 0.0 {
|
||||
return Err(format!(
|
||||
"Invalid APT frame {frame} in {} on character {}",
|
||||
method, c.id
|
||||
));
|
||||
}
|
||||
// The shipped line timer deliberately requests zero at
|
||||
// full capacity. Frame zero addresses the first frame.
|
||||
(frame as usize).saturating_sub(1)
|
||||
}
|
||||
};
|
||||
self.seek(vm, id, frame, 0)?;
|
||||
self.instances.get_mut(&id).unwrap().playing = method == "gotoAndPlay";
|
||||
}
|
||||
_ => return Ok(false),
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
//! Retained movie traversal. All geometry and glyphs come from the owned APT.
|
||||
use crate::{apt_movie::Movie, apt_vm::Vm};
|
||||
use serde::Deserialize;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
#[derive(Clone, Deserialize)]
|
||||
pub struct Vertex {
|
||||
pub position: [f32; 2],
|
||||
pub uv: [f32; 2],
|
||||
}
|
||||
#[derive(Clone, Deserialize)]
|
||||
pub struct Texture {
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub rgba: String,
|
||||
}
|
||||
#[derive(Clone, Deserialize)]
|
||||
pub struct Shape {
|
||||
pub texture: Texture,
|
||||
pub triangles: Vec<[Vertex; 3]>,
|
||||
pub color: [f32; 4],
|
||||
}
|
||||
pub type Shapes = BTreeMap<i32, Vec<Shape>>;
|
||||
pub struct Draw {
|
||||
pub texture: String,
|
||||
pub size: [u32; 2],
|
||||
pub vertices: Vec<Vertex>,
|
||||
pub multiply: [f32; 4],
|
||||
pub add: [f32; 4],
|
||||
}
|
||||
fn compose(a: [f32; 6], b: [f32; 6]) -> [f32; 6] {
|
||||
[
|
||||
a[0] * b[0] + a[2] * b[1],
|
||||
a[1] * b[0] + a[3] * b[1],
|
||||
a[0] * b[2] + a[2] * b[3],
|
||||
a[1] * b[2] + a[3] * b[3],
|
||||
a[0] * b[4] + a[2] * b[5] + a[4],
|
||||
a[1] * b[4] + a[3] * b[5] + a[5],
|
||||
]
|
||||
}
|
||||
fn transform(m: [f32; 6], v: &Vertex) -> Vertex {
|
||||
Vertex {
|
||||
position: [
|
||||
m[0] * v.position[0] + m[2] * v.position[1] + m[4],
|
||||
m[1] * v.position[0] + m[3] * v.position[1] + m[5],
|
||||
],
|
||||
uv: v.uv,
|
||||
}
|
||||
}
|
||||
pub fn draw(movie: &Movie, vm: &Vm, shapes: &Shapes) -> Result<Vec<Draw>, String> {
|
||||
let mut draws = Vec::new();
|
||||
visit(
|
||||
movie,
|
||||
vm,
|
||||
shapes,
|
||||
movie.root,
|
||||
[1., 0., 0., 1., 0., 0.],
|
||||
[1.; 4],
|
||||
[0.; 4],
|
||||
&mut draws,
|
||||
)?;
|
||||
Ok(draws)
|
||||
}
|
||||
fn visit(
|
||||
movie: &Movie,
|
||||
vm: &Vm,
|
||||
shapes: &Shapes,
|
||||
id: usize,
|
||||
parent: [f32; 6],
|
||||
pm: [f32; 4],
|
||||
pa: [f32; 4],
|
||||
out: &mut Vec<Draw>,
|
||||
) -> Result<(), String> {
|
||||
if !vm.get(id, "_visible").truth() {
|
||||
return Ok(());
|
||||
}
|
||||
let instance = &movie.instances[&id];
|
||||
let mut matrix = instance
|
||||
.placement
|
||||
.as_ref()
|
||||
.map_or([1., 0., 0., 1., 0., 0.], |p| p.matrix);
|
||||
matrix[4] = vm.get(id, "_x").number() as f32;
|
||||
matrix[5] = vm.get(id, "_y").number() as f32;
|
||||
let matrix = compose(parent, matrix);
|
||||
if matrix.iter().any(|x| !x.is_finite()) {
|
||||
return Err(format!(
|
||||
"Nonfinite HUD transform on character {}",
|
||||
instance.character
|
||||
));
|
||||
}
|
||||
let color = instance
|
||||
.placement
|
||||
.as_ref()
|
||||
.map_or([255, 255, 255, 255, 0, 0, 0, 0], |p| p.color);
|
||||
let rgba = [1, 2, 3, 0];
|
||||
let mut multiply = std::array::from_fn(|i| pm[i] * color[rgba[i]] as f32 / 255.);
|
||||
let mut add = std::array::from_fn(|i| pa[i] + pm[i] * color[4 + rgba[i]] as f32 / 255.);
|
||||
let alpha = (vm.get(id, "_alpha").number() as f32 / 100.).clamp(0., 1.);
|
||||
multiply[3] *= alpha;
|
||||
add[3] *= alpha;
|
||||
if multiply[3] <= 0. && add[3] <= 0. {
|
||||
return Ok(());
|
||||
}
|
||||
let character = &movie.characters[&instance.character];
|
||||
if character.type_name == "shape" {
|
||||
for shape in shapes
|
||||
.get(&character.id)
|
||||
.ok_or("Missing original shape geometry")?
|
||||
{
|
||||
out.push(Draw {
|
||||
texture: shape.texture.rgba.clone(),
|
||||
size: [shape.texture.width, shape.texture.height],
|
||||
vertices: shape
|
||||
.triangles
|
||||
.iter()
|
||||
.flatten()
|
||||
.map(|v| transform(matrix, v))
|
||||
.collect(),
|
||||
multiply: std::array::from_fn(|i| multiply[i] * shape.color[i]),
|
||||
add,
|
||||
});
|
||||
}
|
||||
} else if let Some(text) = &character.text {
|
||||
let font = &movie.text_assets.fonts
|
||||
[&(text["font_id"].as_i64().ok_or("Invalid text font")? as i32)];
|
||||
let height = text["font_height"].as_f64().ok_or("Invalid text height")? as f32;
|
||||
let sx = font.scale[0] * height;
|
||||
let sy = font.scale[1] * height;
|
||||
let value = vm.get(id, "_displayText").text();
|
||||
let bounds = character.bounds.ok_or("Missing text bounds")?;
|
||||
let width = font.width(&value, height);
|
||||
let alignment = text["alignment"].as_u64().unwrap_or(0);
|
||||
let autosize = vm.get(id, "autoSize").text();
|
||||
let alignment = if autosize == "left" { 0 } else { alignment };
|
||||
let mut x = bounds[0]
|
||||
+ font.offset[0] * sx
|
||||
+ match alignment {
|
||||
1 => bounds[2] - bounds[0] - width,
|
||||
2 => (bounds[2] - bounds[0] - width) * 0.5,
|
||||
_ => 0.,
|
||||
};
|
||||
let y = bounds[1] + font.offset[1] * sy;
|
||||
let mut vertices = Vec::new();
|
||||
for c in value.chars() {
|
||||
if let Some(g) = font.glyph(c) {
|
||||
let x0 = x + g.x_offset * sx;
|
||||
let y0 = y + (font.ascent - g.y_offset) * sy;
|
||||
let x1 = x0 + g.width * sx;
|
||||
let y1 = y0 + g.height * sy;
|
||||
let [u0, v0, u1, v1] = g.atlas_bounds;
|
||||
let points = [[x0, y0], [x1, y0], [x1, y1], [x0, y1]];
|
||||
let uvs = [[u0, v0], [u1, v0], [u1, v1], [u0, v1]];
|
||||
for i in [0, 1, 2, 0, 2, 3] {
|
||||
vertices.push(transform(
|
||||
matrix,
|
||||
&Vertex {
|
||||
position: points[i],
|
||||
uv: [
|
||||
uvs[i][0] / font.size[0] as f32,
|
||||
uvs[i][1] / font.size[1] as f32,
|
||||
],
|
||||
},
|
||||
));
|
||||
}
|
||||
x = g.x_advance.mul_add(sx, x);
|
||||
}
|
||||
}
|
||||
let argb = u32::from_str_radix(
|
||||
text["color_argb"]
|
||||
.as_str()
|
||||
.ok_or("Missing text color")?
|
||||
.trim_start_matches('#'),
|
||||
16,
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
let color = [
|
||||
((argb >> 16) & 255) as f32 / 255.,
|
||||
((argb >> 8) & 255) as f32 / 255.,
|
||||
(argb & 255) as f32 / 255.,
|
||||
(argb >> 24) as f32 / 255.,
|
||||
];
|
||||
if !vertices.is_empty() {
|
||||
out.push(Draw {
|
||||
texture: font.texture.clone(),
|
||||
size: font.size,
|
||||
vertices,
|
||||
multiply: std::array::from_fn(|i| multiply[i] * color[i]),
|
||||
add,
|
||||
});
|
||||
}
|
||||
}
|
||||
for child in instance.children.values() {
|
||||
visit(movie, vm, shapes, *child, matrix, multiply, add, out)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
//! SkateAptString82CA14D0 and cFont828076D0/82808708 layout.
|
||||
use serde::Deserialize;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
#[derive(Clone, Deserialize)]
|
||||
pub struct Glyph {
|
||||
pub glyph_index: usize,
|
||||
pub width: f32,
|
||||
pub height: f32,
|
||||
pub x_offset: f32,
|
||||
pub y_offset: f32,
|
||||
pub x_advance: f32,
|
||||
pub atlas_bounds: [f32; 4],
|
||||
}
|
||||
#[derive(Clone)]
|
||||
pub struct Font {
|
||||
pub texture: String,
|
||||
pub size: [u32; 2],
|
||||
pub scale: [f32; 2],
|
||||
pub offset: [f32; 2],
|
||||
pub ascent: f32,
|
||||
pub glyphs: BTreeMap<u32, Glyph>,
|
||||
}
|
||||
#[derive(Clone, Default)]
|
||||
pub struct TextAssets {
|
||||
pub fonts: BTreeMap<i32, Font>,
|
||||
pub language: BTreeMap<String, String>,
|
||||
}
|
||||
impl TextAssets {
|
||||
pub fn load(json: &serde_json::Value) -> Result<Self, String> {
|
||||
let mut out = Self::default();
|
||||
out.language =
|
||||
serde_json::from_value(json["language"].clone()).map_err(|e| e.to_string())?;
|
||||
for c in json["characters"]
|
||||
.as_array()
|
||||
.ok_or("HUD characters missing")?
|
||||
{
|
||||
if c["type_name"] != "font" {
|
||||
continue;
|
||||
}
|
||||
let name = c["font"]["name"].as_str().ok_or("HUD font name missing")?;
|
||||
let asset = &json["fonts"][name];
|
||||
let d = &asset["definition"];
|
||||
let glyphs: Vec<Glyph> =
|
||||
serde_json::from_value(d["glyphs"].clone()).map_err(|e| e.to_string())?;
|
||||
let layout = &json["font_mappings"][name]["native_layout"];
|
||||
let f = |key: &str| {
|
||||
layout[key]
|
||||
.as_f64()
|
||||
.map(|v| v as f32)
|
||||
.ok_or_else(|| format!("Missing native font {name}.{key}"))
|
||||
};
|
||||
let mut font = Font {
|
||||
texture: asset["texture"]
|
||||
.as_str()
|
||||
.ok_or("Font texture missing")?
|
||||
.into(),
|
||||
size: [
|
||||
d["textures"][0]["width"]
|
||||
.as_u64()
|
||||
.ok_or("Font width missing")? as u32,
|
||||
d["textures"][0]["height"]
|
||||
.as_u64()
|
||||
.ok_or("Font height missing")? as u32,
|
||||
],
|
||||
scale: [f("ScaleX")?, f("ScaleY")?],
|
||||
offset: [f("OffsetX")?, f("OffsetY")?],
|
||||
ascent: d["metrics"]["Ascent"]
|
||||
.as_f64()
|
||||
.ok_or("Font ascent missing")? as f32,
|
||||
glyphs: BTreeMap::new(),
|
||||
};
|
||||
for mapping in d["characters"]
|
||||
.as_array()
|
||||
.ok_or("Font character map missing")?
|
||||
{
|
||||
let index = mapping["glyph_index"]
|
||||
.as_u64()
|
||||
.ok_or("Invalid font glyph index")? as usize;
|
||||
let glyph = glyphs
|
||||
.iter()
|
||||
.find(|g| g.glyph_index == index)
|
||||
.ok_or("Absent mapped glyph")?;
|
||||
font.glyphs.insert(
|
||||
mapping["codepoint"]
|
||||
.as_u64()
|
||||
.ok_or("Invalid font codepoint")? as u32,
|
||||
glyph.clone(),
|
||||
);
|
||||
}
|
||||
out.fonts.insert(
|
||||
c["id"].as_i64().ok_or("Invalid font character id")? as i32,
|
||||
font,
|
||||
);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
pub fn localize(&self, text: &str) -> String {
|
||||
if let Some(literal) = text.strip_prefix('#') {
|
||||
return literal.into();
|
||||
}
|
||||
self.language
|
||||
.get(text)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| text.into())
|
||||
}
|
||||
}
|
||||
impl Font {
|
||||
pub fn glyph(&self, c: char) -> Option<&Glyph> {
|
||||
self.glyphs
|
||||
.get(&(c as u32))
|
||||
.or_else(|| self.glyphs.get(&65535))
|
||||
}
|
||||
pub fn width(&self, text: &str, height: f32) -> f32 {
|
||||
text.chars()
|
||||
.filter_map(|c| self.glyph(c))
|
||||
.fold(0.0, |x, g| g.x_advance.mul_add(self.scale[0] * height, x))
|
||||
}
|
||||
}
|
||||
@@ -114,10 +114,14 @@ struct Function {
|
||||
|
||||
struct Scope {
|
||||
this: usize,
|
||||
local_definitions: bool,
|
||||
locals: BTreeMap<String, Value>,
|
||||
}
|
||||
|
||||
pub trait Host {
|
||||
fn property_changed(&mut self, _vm: &mut Vm, _object: usize, _key: &str) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
fn call(
|
||||
&mut self,
|
||||
vm: &mut Vm,
|
||||
@@ -129,6 +133,7 @@ pub trait Host {
|
||||
#[derive(Default)]
|
||||
pub struct Vm {
|
||||
pub objects: Vec<Object>,
|
||||
free_objects: Vec<usize>,
|
||||
functions: Vec<Function>,
|
||||
pub global: usize,
|
||||
remaining: usize,
|
||||
@@ -151,6 +156,14 @@ impl Vm {
|
||||
vm
|
||||
}
|
||||
pub fn object(&mut self, kind: ObjectKind) -> usize {
|
||||
if let Some(id) = self.free_objects.pop() {
|
||||
self.objects[id] = Object {
|
||||
kind,
|
||||
fields: BTreeMap::new(),
|
||||
prototype: None,
|
||||
};
|
||||
return id;
|
||||
}
|
||||
let id = self.objects.len();
|
||||
self.objects.push(Object {
|
||||
kind,
|
||||
@@ -159,6 +172,45 @@ impl Vm {
|
||||
});
|
||||
id
|
||||
}
|
||||
/// Collect only between updates. Host-owned movie handles are explicit
|
||||
/// roots; script references and prototypes preserve retired clips when
|
||||
/// they remain reachable. Stable handles never move during collection.
|
||||
pub fn collect(&mut self, host_roots: impl IntoIterator<Item = usize>) -> Result<(), String> {
|
||||
if self.depth != 0 {
|
||||
return Err("Cannot collect during an APT call".into());
|
||||
}
|
||||
let mut marked = vec![false; self.objects.len()];
|
||||
let mut pending: Vec<_> = host_roots.into_iter().collect();
|
||||
pending.push(self.global);
|
||||
while let Some(id) = pending.pop() {
|
||||
let seen = marked.get_mut(id).ok_or("Invalid APT collection root")?;
|
||||
if *seen {
|
||||
continue;
|
||||
}
|
||||
*seen = true;
|
||||
let object = &self.objects[id];
|
||||
if let Some(id) = object.prototype {
|
||||
pending.push(id);
|
||||
}
|
||||
for value in object.fields.values() {
|
||||
if let Value::Object(id) = value {
|
||||
pending.push(*id);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.free_objects.clear();
|
||||
for (id, live) in marked.into_iter().enumerate() {
|
||||
if !live {
|
||||
self.objects[id] = Object {
|
||||
kind: ObjectKind::Plain,
|
||||
fields: BTreeMap::new(),
|
||||
prototype: None,
|
||||
};
|
||||
self.free_objects.push(id);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
pub fn set(
|
||||
&mut self,
|
||||
object: usize,
|
||||
@@ -219,6 +271,7 @@ impl Vm {
|
||||
&mut Vec::new(),
|
||||
&mut Scope {
|
||||
this: object,
|
||||
local_definitions: false,
|
||||
locals: BTreeMap::new(),
|
||||
},
|
||||
host,
|
||||
@@ -261,6 +314,7 @@ impl Vm {
|
||||
let mut reg = 1;
|
||||
let mut scope = Scope {
|
||||
this,
|
||||
local_definitions: true,
|
||||
locals: BTreeMap::new(),
|
||||
};
|
||||
if f.code.flags & 2 == 0 {
|
||||
@@ -351,6 +405,17 @@ impl Vm {
|
||||
};
|
||||
match op {
|
||||
0 => break,
|
||||
0x70 => stack.push(Value::Object(scope.this)),
|
||||
0xa1 => stack.push(Value::Text(
|
||||
i.operand
|
||||
.as_str()
|
||||
.ok_or("APT string operand missing")?
|
||||
.into(),
|
||||
)),
|
||||
0xa4 => stack.push(self.variable(
|
||||
scope,
|
||||
i.operand.as_str().ok_or("APT variable operand missing")?,
|
||||
)),
|
||||
0x06 | 0x07 => {
|
||||
host.call(
|
||||
self,
|
||||
@@ -426,7 +491,8 @@ impl Vm {
|
||||
let k = pop(&mut stack)?.text();
|
||||
let o = pop(&mut stack)?;
|
||||
if let Value::Object(id) = o {
|
||||
self.set(id, k, v)?;
|
||||
self.set(id, &k, v)?;
|
||||
host.property_changed(self, id, &k)?;
|
||||
}
|
||||
}
|
||||
0x1c => {
|
||||
@@ -436,10 +502,11 @@ impl Vm {
|
||||
0x1d | 0x3c => {
|
||||
let v = pop(&mut stack)?;
|
||||
let k = pop(&mut stack)?.text();
|
||||
if op == 0x3c || scope.locals.contains_key(&k) {
|
||||
if (op == 0x3c && scope.local_definitions) || scope.locals.contains_key(&k) {
|
||||
scope.locals.insert(k, v);
|
||||
} else {
|
||||
self.set(scope.this, k, v)?;
|
||||
self.set(scope.this, &k, v)?;
|
||||
host.property_changed(self, scope.this, &k)?;
|
||||
}
|
||||
}
|
||||
0x3a => {
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
#import bevy_sprite::mesh2d_vertex_output::VertexOutput
|
||||
struct ColorTransform { multiply: vec4<f32>, add: vec4<f32> }
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(0) var<uniform> color: ColorTransform;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(1) var atlas: texture_2d<f32>;
|
||||
@group(#{MATERIAL_BIND_GROUP}) @binding(2) var atlas_sampler: sampler;
|
||||
@fragment
|
||||
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||
return clamp(textureSample(atlas, atlas_sampler, in.uv) * color.multiply + color.add, vec4(0.0), vec4(1.0));
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
//! Native HUD bindings and the original ActionScript-driven movie lifecycle.
|
||||
use crate::{
|
||||
apt_movie::Movie,
|
||||
apt_vm::{Host, ObjectKind, Value, Vm},
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Input {
|
||||
pub sequence_score: i32,
|
||||
pub line_score: i32,
|
||||
pub sequence_timer: i32,
|
||||
pub line_time: f32,
|
||||
pub line_capacity: f32,
|
||||
pub multiplier: f32,
|
||||
pub clean: bool,
|
||||
pub sketchy: bool,
|
||||
pub stance: [bool; 4],
|
||||
pub trick_name: String,
|
||||
pub trick_metrics: [Value; 5],
|
||||
pub context_tricks: Vec<Value>,
|
||||
}
|
||||
pub struct Bindings {
|
||||
pub movie: Movie,
|
||||
pub input: Input,
|
||||
random: u32,
|
||||
}
|
||||
pub struct Runtime {
|
||||
pub vm: Vm,
|
||||
pub bindings: Bindings,
|
||||
pub controller: usize,
|
||||
}
|
||||
|
||||
fn array(vm: &mut Vm, values: impl IntoIterator<Item = Value>) -> Result<Value, String> {
|
||||
let object = vm.object(ObjectKind::Plain);
|
||||
let mut n = 0;
|
||||
for value in values {
|
||||
vm.set(object, n.to_string(), value)?;
|
||||
n += 1;
|
||||
}
|
||||
vm.set(object, "length", Value::Number(n as f64))?;
|
||||
Ok(Value::Object(object))
|
||||
}
|
||||
fn score(value: i32) -> String {
|
||||
let digits = value.unsigned_abs().to_string();
|
||||
let mut out = String::new();
|
||||
if value < 0 {
|
||||
out.push('-');
|
||||
}
|
||||
for (i, c) in digits.chars().enumerate() {
|
||||
if i > 0 && (digits.len() - i) % 3 == 0 {
|
||||
out.push(',');
|
||||
}
|
||||
out.push(c);
|
||||
}
|
||||
out
|
||||
}
|
||||
impl Host for Bindings {
|
||||
fn property_changed(&mut self, vm: &mut Vm, object: usize, key: &str) -> Result<(), String> {
|
||||
if key == "text" || key == "autoSize" {
|
||||
self.movie.text_changed(vm, object)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn call(
|
||||
&mut self,
|
||||
vm: &mut Vm,
|
||||
object: usize,
|
||||
method: &str,
|
||||
args: Vec<Value>,
|
||||
) -> Result<Value, String> {
|
||||
if self.movie.method(vm, object, method, &args)? {
|
||||
return Ok(Value::Undefined);
|
||||
}
|
||||
let native = match vm.objects.get(object).map(|o| &o.kind) {
|
||||
Some(ObjectKind::Native(name)) => name.as_str(),
|
||||
_ => "global",
|
||||
};
|
||||
match (native, method) {
|
||||
("Math", "floor") => Ok(Value::Number(
|
||||
args.first().ok_or("Math.floor argument")?.number().floor(),
|
||||
)),
|
||||
("Math", "random") => {
|
||||
// Presentation-only random source; never influences scoring.
|
||||
self.random ^= self.random << 13;
|
||||
self.random ^= self.random >> 17;
|
||||
self.random ^= self.random << 5;
|
||||
Ok(Value::Number(self.random as f64 / 4294967296.0))
|
||||
}
|
||||
// The HUD never enumerates prototype properties. Marking their
|
||||
// enumeration flags therefore leaves its observable fields intact.
|
||||
("global", "ASSetPropFlags") => Ok(Value::Undefined),
|
||||
("FELanguage", "GetLanguage") => Ok(Value::Text("english".into())),
|
||||
("Tricks", "GetLineTimerMaxPoints") => {
|
||||
Ok(Value::Number(self.input.line_capacity as f64))
|
||||
}
|
||||
//825C2910 truncates manager+32 to integer;825C28F8 returns+192.
|
||||
("Tricks", "GetLineTimeRemaining") => {
|
||||
Ok(Value::Number(self.input.line_time.trunc() as f64))
|
||||
}
|
||||
("Tricks", "GetLineScore") => Ok(Value::Number(self.input.line_score as f64)),
|
||||
("HUDComponents", "TrickDisplay_GetSequenceMultiplier") => {
|
||||
Ok(Value::Number(self.input.multiplier as f64))
|
||||
}
|
||||
("Tricks", "GetGeneralInfo") => array(
|
||||
vm,
|
||||
[
|
||||
Value::Number(self.input.sequence_score as f64),
|
||||
Value::Text(score(self.input.sequence_score)),
|
||||
Value::Bool(self.input.clean),
|
||||
Value::Bool(self.input.sketchy),
|
||||
Value::Number(self.input.sequence_timer as f64),
|
||||
Value::Number(self.input.line_score as f64),
|
||||
Value::Text(score(self.input.line_score)),
|
||||
],
|
||||
),
|
||||
("Tricks", "GetCurrentTrickStance") => array(vm, self.input.stance.map(Value::Bool)),
|
||||
("Tricks", "GetCurrentTrickName") => Ok(Value::Text(self.input.trick_name.clone())),
|
||||
("Tricks", "GetCurrentTrickMetrics") => array(vm, self.input.trick_metrics.clone()),
|
||||
("Tricks", "TrickDisplay_GetAllTrickData") => {
|
||||
array(vm, self.input.context_tricks.clone())
|
||||
}
|
||||
_ => Err(format!(
|
||||
"Unimplemented original HUD binding {native}.{method}"
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Runtime {
|
||||
pub fn load(json: &serde_json::Value, input: Input) -> Result<Self, String> {
|
||||
let mut vm = Vm::new();
|
||||
for name in ["MovieClip", "Tricks", "FELanguage", "HUDComponents", "Math"] {
|
||||
let object = vm.object(ObjectKind::Native(name.into()));
|
||||
let prototype = vm.object(ObjectKind::Plain);
|
||||
vm.set(object, "prototype", Value::Object(prototype))?;
|
||||
vm.set(vm.global, name, Value::Object(object))?;
|
||||
}
|
||||
vm.set(vm.global, "Screen_EdgeOffset", Value::Number(0.0))?;
|
||||
let mut bindings = Bindings {
|
||||
movie: Movie::load(json)?,
|
||||
input,
|
||||
random: 0x9e3779b9,
|
||||
};
|
||||
let initial: Vec<_> = bindings
|
||||
.movie
|
||||
.characters
|
||||
.values()
|
||||
.flat_map(|c| &c.frames)
|
||||
.flat_map(|f| &f.controls)
|
||||
.filter(|c| c.type_name == "do_init_action")
|
||||
.map(|c| c.actions_offset)
|
||||
.collect();
|
||||
for offset in initial {
|
||||
let code = bindings.movie.actions[&offset.to_string()].clone();
|
||||
vm.run(&code, &mut bindings)?;
|
||||
}
|
||||
vm.begin_update();
|
||||
bindings.movie.initialize(&mut vm)?;
|
||||
let mut runtime = Self {
|
||||
vm,
|
||||
bindings,
|
||||
controller: usize::MAX,
|
||||
};
|
||||
runtime.drain()?;
|
||||
let Value::Object(controller) = runtime.vm.get(runtime.bindings.movie.root, "screen")
|
||||
else {
|
||||
return Err("Original HUD controller was not constructed".into());
|
||||
};
|
||||
runtime.controller = controller;
|
||||
runtime.vm.call_method(
|
||||
controller,
|
||||
"ScreenShow",
|
||||
vec![Value::Bool(true)],
|
||||
&mut runtime.bindings,
|
||||
)?;
|
||||
runtime.drain()?;
|
||||
Ok(runtime)
|
||||
}
|
||||
fn drain(&mut self) -> Result<(), String> {
|
||||
let mut calls = 0;
|
||||
while let Some((object, offset)) = self.bindings.movie.pending.pop_front() {
|
||||
calls += 1;
|
||||
if calls > 4096 {
|
||||
return Err("HUD frame action limit".into());
|
||||
}
|
||||
if !self.bindings.movie.instances.contains_key(&object) {
|
||||
continue;
|
||||
}
|
||||
let code = self
|
||||
.bindings
|
||||
.movie
|
||||
.actions
|
||||
.get(&offset.to_string())
|
||||
.ok_or("Missing HUD action block")?
|
||||
.clone();
|
||||
self.vm.run_on(object, &code, &mut self.bindings)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
pub fn update(
|
||||
&mut self,
|
||||
input: Input,
|
||||
new_trick: bool,
|
||||
modified_trick: bool,
|
||||
close_tricks: bool,
|
||||
) -> Result<(), String> {
|
||||
self.vm.begin_update();
|
||||
let previous = self.bindings.input.clone();
|
||||
self.bindings.input = input;
|
||||
let mut methods = Vec::new();
|
||||
if previous.multiplier != self.bindings.input.multiplier {
|
||||
methods.push("UpdateMultiplier");
|
||||
}
|
||||
if previous.sequence_score != self.bindings.input.sequence_score
|
||||
|| previous.clean != self.bindings.input.clean
|
||||
|| previous.sketchy != self.bindings.input.sketchy
|
||||
|| previous.line_score != self.bindings.input.line_score
|
||||
{
|
||||
methods.push("UpdateTrickScoring");
|
||||
}
|
||||
if previous.stance != self.bindings.input.stance {
|
||||
methods.push("UpdateStance");
|
||||
}
|
||||
if new_trick {
|
||||
methods.push("RefreshTrickName");
|
||||
} else if modified_trick {
|
||||
methods.push("CurrentTrickModified");
|
||||
}
|
||||
if close_tricks {
|
||||
methods.push("CloseTrickDisplay");
|
||||
}
|
||||
for method in methods {
|
||||
self.vm
|
||||
.call_method(self.controller, method, vec![], &mut self.bindings)?;
|
||||
self.drain()?;
|
||||
}
|
||||
self.vm.call_method(
|
||||
self.controller,
|
||||
"UpdateLineDisplay",
|
||||
vec![],
|
||||
&mut self.bindings,
|
||||
)?;
|
||||
self.drain()?;
|
||||
self.bindings.movie.advance(&mut self.vm)?;
|
||||
self.drain()?;
|
||||
self.vm
|
||||
.collect(self.bindings.movie.instances.keys().copied())?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,12 @@
|
||||
mod animation;
|
||||
mod apt_vm;
|
||||
mod apt_display;
|
||||
mod apt_movie;
|
||||
mod apt_text;
|
||||
mod apt_scene;
|
||||
mod hud_runtime;
|
||||
mod scoring_runtime;
|
||||
mod scoring_hud;
|
||||
mod animation_pose;
|
||||
mod app;
|
||||
mod assets;
|
||||
|
||||
@@ -27,6 +27,7 @@ pub(crate) fn advance(
|
||||
graphs: &StockGraphs,
|
||||
profile: &AnimationProfile,
|
||||
) -> Result<AnimationPhaseOutput, String> {
|
||||
skater.animation.motion.score_packet = Default::default();
|
||||
let p = &skater.player_input.processed;
|
||||
let physical = &skater.player_input.physical;
|
||||
let deck = physics.board.part_transforms()[BodyId::Deck.index()];
|
||||
|
||||
@@ -222,6 +222,23 @@ pub(super) fn advance(
|
||||
events,
|
||||
});
|
||||
camera_output::advance(physics, skater, &feedback, camera)?;
|
||||
let score = &skater.animation.motion.score_packet;
|
||||
let deck_frame = physics.board.part_transforms()[BodyId::Deck.index()];
|
||||
let position = deck.rates.position;
|
||||
let velocity = deck.rates.linear_velocity;
|
||||
let filtered = skater.player_state.filtered_output;
|
||||
skater.scoring.advance(crate::scoring_runtime::Frame {
|
||||
tick: tick as u32, dt: simulation.time_step,
|
||||
category: filtered.map_or(Default::default(), |f|f.category),
|
||||
state: skater.player_state.current() as u32,
|
||||
descriptor: score.trick.or_else(||score.grab.map(|g|g.0)),
|
||||
grind_id: filtered.map_or(-1, |f|f.grind.scorable_id), flags:score.flags,
|
||||
position:[position.x,position.y,position.z], velocity:[velocity.x,velocity.y,velocity.z],
|
||||
forward:deck_frame.basis.columns[2],
|
||||
switch:skater.animation.packet.riding_switch,fakie:skater.animation.packet.riding_fakie,
|
||||
nollie:skater.animation.packet.weight_forwards,body_flip:skater.player_input.physical.air.flag_441!=0,
|
||||
landing:skater.landing_quality,teleported,reverting:skater.player_state.state_flags[70-52],
|
||||
})?;
|
||||
super::climbing::approach::advance(physics, skater, controls);
|
||||
skater.animation_input.finish_output_publication();
|
||||
skater.player_input.player.update_count_1316 =
|
||||
|
||||
@@ -27,6 +27,7 @@ use std::path::Path;
|
||||
|
||||
#[derive(Resource)]
|
||||
pub(crate) struct SkaterRuntime {
|
||||
pub scoring: crate::scoring_runtime::Runtime,
|
||||
pub climbing: super::climbing::Runtime,
|
||||
/// Completed physical pose in native animation space, read by rendering.
|
||||
pub render_pose: Vec<skate_core::animation::output::NativeMatrix>,
|
||||
@@ -191,6 +192,7 @@ impl SkaterRuntime {
|
||||
&data, asset_root, &animation.evaluator.frames.source_sha256,
|
||||
)?;
|
||||
Ok(Self {
|
||||
scoring: crate::scoring_runtime::Runtime::load(&data)?,
|
||||
climbing: super::climbing::Runtime::load(asset_root, &animation.evaluator.frames.bone_names)?,
|
||||
offboard,
|
||||
render_pose: initial_hierarchy,
|
||||
|
||||
@@ -0,0 +1,293 @@
|
||||
//! Original APT HUD rendered independently of the world's resolution scale.
|
||||
use crate::{apt_scene, config::Config, hud_runtime, physics::SkaterRuntime};
|
||||
use bevy::{
|
||||
asset::{RenderAssetUsages, embedded_asset},
|
||||
camera::{RenderTarget, visibility::RenderLayers},
|
||||
prelude::*,
|
||||
render::render_resource::{
|
||||
AsBindGroup, Extent3d, PrimitiveTopology, ShaderType, TextureDimension, TextureFormat,
|
||||
},
|
||||
shader::ShaderRef,
|
||||
sprite_render::{AlphaMode2d, Material2d, Material2dPlugin, MeshMaterial2d},
|
||||
};
|
||||
use std::{collections::BTreeMap, path::PathBuf};
|
||||
|
||||
#[derive(Clone, Copy, Debug, ShaderType)]
|
||||
struct ColorTransform {
|
||||
multiply: Vec4,
|
||||
add: Vec4,
|
||||
}
|
||||
#[derive(Asset, TypePath, AsBindGroup, Debug, Clone)]
|
||||
struct HudMaterial {
|
||||
#[uniform(0)]
|
||||
color: ColorTransform,
|
||||
#[texture(1)]
|
||||
#[sampler(2)]
|
||||
atlas: Handle<Image>,
|
||||
}
|
||||
impl Material2d for HudMaterial {
|
||||
fn fragment_shader() -> ShaderRef {
|
||||
"embedded://skate3rust/hud_render.wgsl".into()
|
||||
}
|
||||
fn alpha_mode(&self) -> AlphaMode2d {
|
||||
AlphaMode2d::Blend
|
||||
}
|
||||
}
|
||||
struct Slot {
|
||||
entity: Entity,
|
||||
mesh: Handle<Mesh>,
|
||||
material: Handle<HudMaterial>,
|
||||
}
|
||||
#[derive(Resource)]
|
||||
struct Hud {
|
||||
runtime: hud_runtime::Runtime,
|
||||
source: serde_json::Value,
|
||||
shapes: apt_scene::Shapes,
|
||||
textures: BTreeMap<String, Handle<Image>>,
|
||||
slots: Vec<Slot>,
|
||||
generation: u64,
|
||||
failed: bool,
|
||||
}
|
||||
pub(crate) struct ScoringHudPlugin;
|
||||
impl Plugin for ScoringHudPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
embedded_asset!(app, "hud_render.wgsl");
|
||||
app.add_plugins(Material2dPlugin::<HudMaterial>::default())
|
||||
.add_systems(PostStartup, setup)
|
||||
.add_systems(
|
||||
FixedUpdate,
|
||||
advance
|
||||
.after(crate::app::SimulationSet::Physics)
|
||||
.run_if(crate::graphics_menu::gameplay_active),
|
||||
)
|
||||
.add_systems(
|
||||
Update,
|
||||
(reset, render).chain().after(crate::app::FrameSet::Physics),
|
||||
);
|
||||
}
|
||||
}
|
||||
fn setup(
|
||||
mut commands: Commands,
|
||||
config: Res<Config>,
|
||||
skater: Res<SkaterRuntime>,
|
||||
cameras: Query<Entity, With<IsDefaultUiCamera>>,
|
||||
mut images: ResMut<Assets<Image>>,
|
||||
) {
|
||||
let root = std::env::var_os("SKATE_SCORING_HUD_ROOT")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|| config.asset_root.join("private/hud"));
|
||||
let result = (|| -> Result<Hud, String> {
|
||||
let source: serde_json::Value = serde_json::from_slice(
|
||||
&std::fs::read(root.join("runtime/trickdisplay.json"))
|
||||
.map_err(|e| format!("{}: {e}", root.display()))?,
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
let runtime = hud_runtime::Runtime::load(&source, skater.scoring.hud_input())?;
|
||||
let shapes: apt_scene::Shapes =
|
||||
serde_json::from_value(source["shapes"].clone()).map_err(|e| e.to_string())?;
|
||||
let mut files = BTreeMap::new();
|
||||
for shape in shapes.values().flatten() {
|
||||
files.insert(
|
||||
shape.texture.rgba.clone(),
|
||||
[shape.texture.width, shape.texture.height],
|
||||
);
|
||||
}
|
||||
for font in runtime.bindings.movie.text_assets.fonts.values() {
|
||||
files.insert(font.texture.clone(), font.size);
|
||||
}
|
||||
let mut textures = BTreeMap::new();
|
||||
for (path, size) in files {
|
||||
let bytes = std::fs::read(root.join(&path)).map_err(|e| format!("HUD {path}: {e}"))?;
|
||||
if bytes.len() != size[0] as usize * size[1] as usize * 4 {
|
||||
return Err(format!("Invalid HUD texture size {path}"));
|
||||
}
|
||||
let image = Image::new(
|
||||
Extent3d {
|
||||
width: size[0],
|
||||
height: size[1],
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
TextureDimension::D2,
|
||||
bytes,
|
||||
TextureFormat::Rgba8UnormSrgb,
|
||||
RenderAssetUsages::RENDER_WORLD,
|
||||
);
|
||||
textures.insert(path, images.add(image));
|
||||
}
|
||||
Ok(Hud {
|
||||
runtime,
|
||||
source,
|
||||
shapes,
|
||||
textures,
|
||||
slots: Vec::new(),
|
||||
generation: 0,
|
||||
failed: false,
|
||||
})
|
||||
})();
|
||||
match result {
|
||||
Ok(hud) => {
|
||||
let target = images.add(Image::new_target_texture(
|
||||
1280,
|
||||
720,
|
||||
TextureFormat::Rgba8UnormSrgb,
|
||||
None,
|
||||
));
|
||||
commands.spawn((
|
||||
Camera2d,
|
||||
Camera {
|
||||
order: -1,
|
||||
clear_color: ClearColorConfig::Custom(Color::NONE),
|
||||
..default()
|
||||
},
|
||||
RenderTarget::Image(target.clone().into()),
|
||||
RenderLayers::layer(31),
|
||||
Msaa::Off,
|
||||
));
|
||||
let Ok(output) = cameras.single() else {
|
||||
error!("Original HUD requires the presentation camera");
|
||||
return;
|
||||
};
|
||||
commands.spawn((
|
||||
ImageNode::new(target),
|
||||
UiTargetCamera(output),
|
||||
GlobalZIndex(1),
|
||||
Pickable::IGNORE,
|
||||
Node {
|
||||
position_type: PositionType::Absolute,
|
||||
width: percent(100),
|
||||
height: percent(100),
|
||||
..default()
|
||||
},
|
||||
));
|
||||
commands.insert_resource(hud);
|
||||
info!("Original scoring HUD loaded");
|
||||
}
|
||||
Err(error) => error!("Original scoring HUD could not load: {error}"),
|
||||
}
|
||||
}
|
||||
fn reset(
|
||||
hud: Option<ResMut<Hud>>,
|
||||
skater: Res<SkaterRuntime>,
|
||||
map: Res<crate::map_transition::CurrentMap>,
|
||||
) {
|
||||
let Some(mut hud) = hud else {
|
||||
return;
|
||||
};
|
||||
if hud.generation == map.generation {
|
||||
return;
|
||||
}
|
||||
match hud_runtime::Runtime::load(&hud.source, skater.scoring.hud_input()) {
|
||||
Ok(runtime) => {
|
||||
hud.runtime = runtime;
|
||||
hud.generation = map.generation;
|
||||
hud.failed = false;
|
||||
}
|
||||
Err(error) => {
|
||||
error!("Original scoring HUD reset: {error}");
|
||||
hud.failed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
fn advance(
|
||||
hud: Option<ResMut<Hud>>,
|
||||
skater: Res<SkaterRuntime>,
|
||||
map: Res<crate::map_transition::CurrentMap>,
|
||||
) {
|
||||
let Some(mut hud) = hud else {
|
||||
return;
|
||||
};
|
||||
if hud.failed || hud.generation != map.generation {
|
||||
return;
|
||||
}
|
||||
if let Err(error) = hud.runtime.update(
|
||||
skater.scoring.hud_input(),
|
||||
skater.scoring.new_trick,
|
||||
skater.scoring.modified_trick,
|
||||
skater.scoring.close_tricks,
|
||||
) {
|
||||
error!("Original scoring HUD stopped: {error}");
|
||||
hud.failed = true;
|
||||
}
|
||||
}
|
||||
fn render(
|
||||
mut commands: Commands,
|
||||
hud: Option<ResMut<Hud>>,
|
||||
mut meshes: ResMut<Assets<Mesh>>,
|
||||
mut materials: ResMut<Assets<HudMaterial>>,
|
||||
) {
|
||||
let Some(mut hud) = hud else {
|
||||
return;
|
||||
};
|
||||
let draws = if hud.failed {
|
||||
Vec::new()
|
||||
} else {
|
||||
match apt_scene::draw(&hud.runtime.bindings.movie, &hud.runtime.vm, &hud.shapes) {
|
||||
Ok(draws) => draws,
|
||||
Err(error) => {
|
||||
error!("Original HUD geometry: {error}");
|
||||
hud.failed = true;
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
};
|
||||
for (index, draw) in draws.iter().enumerate() {
|
||||
let Some(texture) = hud.textures.get(&draw.texture).cloned() else {
|
||||
continue;
|
||||
};
|
||||
let mut mesh = Mesh::new(
|
||||
PrimitiveTopology::TriangleList,
|
||||
RenderAssetUsages::MAIN_WORLD | RenderAssetUsages::RENDER_WORLD,
|
||||
);
|
||||
mesh.insert_attribute(
|
||||
Mesh::ATTRIBUTE_POSITION,
|
||||
draw.vertices
|
||||
.iter()
|
||||
.map(|v| [v.position[0] - 640., 360. - v.position[1], 0.])
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
mesh.insert_attribute(
|
||||
Mesh::ATTRIBUTE_UV_0,
|
||||
draw.vertices.iter().map(|v| v.uv).collect::<Vec<_>>(),
|
||||
);
|
||||
mesh.insert_attribute(
|
||||
Mesh::ATTRIBUTE_NORMAL,
|
||||
vec![[0., 0., 1.]; draw.vertices.len()],
|
||||
);
|
||||
let material = HudMaterial {
|
||||
color: ColorTransform {
|
||||
multiply: draw.multiply.into(),
|
||||
add: draw.add.into(),
|
||||
},
|
||||
atlas: texture,
|
||||
};
|
||||
if index == hud.slots.len() {
|
||||
let mesh = meshes.add(mesh);
|
||||
let material = materials.add(material);
|
||||
let entity = commands
|
||||
.spawn((
|
||||
Mesh2d(mesh.clone()),
|
||||
MeshMaterial2d(material.clone()),
|
||||
Transform::from_xyz(0., 0., index as f32 * 0.01),
|
||||
RenderLayers::layer(31),
|
||||
))
|
||||
.id();
|
||||
hud.slots.push(Slot {
|
||||
entity,
|
||||
mesh,
|
||||
material,
|
||||
});
|
||||
} else {
|
||||
let slot = &hud.slots[index];
|
||||
if let Some(old) = meshes.get_mut(&slot.mesh) {
|
||||
*old = mesh;
|
||||
}
|
||||
if let Some(old) = materials.get_mut(&slot.material) {
|
||||
*old = material;
|
||||
}
|
||||
commands.entity(slot.entity).insert(Visibility::Visible);
|
||||
}
|
||||
}
|
||||
for slot in &hud.slots[draws.len()..] {
|
||||
commands.entity(slot.entity).insert(Visibility::Hidden);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,597 @@
|
||||
//! ScoreModule integration. The simulation owns recognition and accounting;
|
||||
//! the APT movie only consumes the resulting publication.
|
||||
use crate::{apt_vm::Value, hud_runtime};
|
||||
use skate_core::{
|
||||
animation::output::attributes::AttributeName,
|
||||
physics::filtered_state::FilteredCategory,
|
||||
scoring::{
|
||||
carrier::{Carrier, delay_ticks},
|
||||
conversions::LINKS,
|
||||
session::Session,
|
||||
},
|
||||
};
|
||||
use skate_data::{collections::Collections, scoring::ScoringData};
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum Collector {
|
||||
None,
|
||||
Ground,
|
||||
Air,
|
||||
Grind,
|
||||
Offboard,
|
||||
Special,
|
||||
}
|
||||
pub(crate) struct Frame {
|
||||
pub tick: u32,
|
||||
pub dt: f32,
|
||||
pub category: FilteredCategory,
|
||||
pub state: u32,
|
||||
pub descriptor: Option<AttributeName>,
|
||||
pub grind_id: i32,
|
||||
pub flags: u32,
|
||||
pub position: [f32; 3],
|
||||
pub velocity: [f32; 3],
|
||||
pub forward: [f32; 3],
|
||||
pub switch: bool,
|
||||
pub fakie: bool,
|
||||
pub nollie: bool,
|
||||
pub body_flip: bool,
|
||||
pub landing: skate_core::animation::landing_quality::Output,
|
||||
pub teleported: bool,
|
||||
pub reverting: bool,
|
||||
}
|
||||
pub(crate) struct Runtime {
|
||||
pub data: ScoringData,
|
||||
pub session: Session,
|
||||
collector: Collector,
|
||||
carriers: [Option<Carrier>; 4],
|
||||
held: [f32; 4],
|
||||
distance: [f32; 4],
|
||||
metric_rewards: [f32; 4],
|
||||
start: [f32; 3],
|
||||
previous: [f32; 3],
|
||||
previous_heading: f32,
|
||||
spin: f32,
|
||||
peak: f32,
|
||||
air_factor: f32,
|
||||
air_repetition: f32,
|
||||
air_repetition_set: bool,
|
||||
grab_chain: u32,
|
||||
air_metrics: [f32; 5],
|
||||
landing_countdown: u32,
|
||||
idle_ticks: u32,
|
||||
collector_ticks: u32,
|
||||
manual_revert_ticks: u32,
|
||||
revert_id: Option<usize>,
|
||||
sequence_active: bool,
|
||||
sequence_score: f32,
|
||||
trick_name: String,
|
||||
stance: [bool; 4],
|
||||
clean: bool,
|
||||
sketchy: bool,
|
||||
pub new_trick: bool,
|
||||
pub modified_trick: bool,
|
||||
pub close_tricks: bool,
|
||||
}
|
||||
impl Runtime {
|
||||
pub fn load(data: &Collections) -> Result<Self, String> {
|
||||
Ok(Self {
|
||||
data: ScoringData::load(data)?,
|
||||
session: Session::default(),
|
||||
collector: Collector::None,
|
||||
carriers: std::array::from_fn(|_| None),
|
||||
held: [0.; 4],
|
||||
distance: [0.; 4],
|
||||
metric_rewards: [0.; 4],
|
||||
start: [0.; 3],
|
||||
previous: [0.; 3],
|
||||
previous_heading: 0.,
|
||||
spin: 0.,
|
||||
peak: 0.,
|
||||
air_factor: 1.,
|
||||
air_repetition: 1.,
|
||||
air_repetition_set: false,
|
||||
grab_chain: 0,
|
||||
air_metrics: [0.; 5],
|
||||
landing_countdown: 0,
|
||||
idle_ticks: 0,
|
||||
collector_ticks: 0,
|
||||
manual_revert_ticks: 0,
|
||||
revert_id: None,
|
||||
sequence_active: false,
|
||||
sequence_score: 0.,
|
||||
trick_name: String::new(),
|
||||
stance: [false; 4],
|
||||
clean: false,
|
||||
sketchy: false,
|
||||
new_trick: false,
|
||||
modified_trick: false,
|
||||
close_tricks: false,
|
||||
})
|
||||
}
|
||||
pub fn hud_input(&self) -> hud_runtime::Input {
|
||||
hud_runtime::Input {
|
||||
sequence_score: self.sequence_score as i32,
|
||||
line_score: self.session.holder.snapshot.line as i32,
|
||||
sequence_timer: self.session.line.points as i32,
|
||||
line_time: self.session.line.points,
|
||||
line_capacity: self.data.line_capacity,
|
||||
multiplier: self.session.combo.multiplier,
|
||||
clean: self.clean,
|
||||
sketchy: self.sketchy,
|
||||
stance: self.stance,
|
||||
trick_name: self.trick_name.clone(),
|
||||
trick_metrics: [
|
||||
Value::Text(self.trick_name.clone()),
|
||||
Value::Bool(false),
|
||||
Value::Bool(false),
|
||||
Value::Bool(false),
|
||||
Value::Bool(false),
|
||||
],
|
||||
context_tricks: Vec::new(),
|
||||
}
|
||||
}
|
||||
fn penalty(&self, id: usize) -> f32 {
|
||||
let Some(d) = self.data.by_id(id) else {
|
||||
return 1.;
|
||||
};
|
||||
if d.metadata.repetition_applies() {
|
||||
self.data.repetition.evaluate(
|
||||
self.session
|
||||
.holder
|
||||
.repetition_count(d.metadata)
|
||||
.unwrap_or(0) as f32,
|
||||
)
|
||||
} else {
|
||||
1.
|
||||
}
|
||||
}
|
||||
fn finish(&mut self, slot: usize, complete: bool, keep_metric: bool) {
|
||||
if let Some(mut carrier) = self.carriers[slot].take() {
|
||||
if complete {
|
||||
carrier.complete(self.data.unannounced_factor);
|
||||
if self.collector == Collector::Air {
|
||||
self.session
|
||||
.holder
|
||||
.end_trick(carrier.scorable, carrier.reward);
|
||||
} else {
|
||||
let metric = self.collector == Collector::Grind
|
||||
|| self.collector == Collector::Ground && slot < 3;
|
||||
if !(keep_metric && self.collector == Collector::Ground) {
|
||||
let reward = if keep_metric {
|
||||
0.
|
||||
} else if metric {
|
||||
if carrier.scorable.score_type == 9 {
|
||||
carrier.reward.max(self.metric_rewards[slot])
|
||||
} else {
|
||||
self.metric_rewards[slot].max(0.)
|
||||
}
|
||||
} else {
|
||||
carrier.reward
|
||||
};
|
||||
self.session.holder.credit_trick(carrier.scorable, reward);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !keep_metric {
|
||||
self.held[slot] = 0.;
|
||||
self.distance[slot] = 0.;
|
||||
self.metric_rewards[slot] = 0.;
|
||||
}
|
||||
}
|
||||
fn carrier(&mut self, slot: usize, id: Option<usize>, f: &Frame) -> Result<(), String> {
|
||||
if self.carriers[slot].as_ref().map(|c| c.scorable.id) != id {
|
||||
let conversion = id.filter(|new| {
|
||||
self.collector == Collector::Air
|
||||
&& self.carriers[slot].is_some()
|
||||
&& LINKS[*new].0 >= 0
|
||||
});
|
||||
let chained_grab = self.collector == Collector::Air
|
||||
&& id
|
||||
.and_then(|id| self.data.by_id(id))
|
||||
.is_some_and(|d| d.metadata.class == 2)
|
||||
&& self.carriers[slot]
|
||||
.as_ref()
|
||||
.is_some_and(|c| c.scorable.class == 2 && !c.announced);
|
||||
let previous_tick = self.carriers[slot].as_ref().map(|c| c.start_tick);
|
||||
let keep_metric = id.is_some()
|
||||
&& (self.collector == Collector::Grind
|
||||
|| self.collector == Collector::Ground && slot == 0);
|
||||
if conversion.is_none() {
|
||||
if chained_grab {
|
||||
self.carriers[slot] = None;
|
||||
self.grab_chain += 1;
|
||||
} else {
|
||||
self.finish(slot, true, keep_metric);
|
||||
self.grab_chain = 0;
|
||||
}
|
||||
}
|
||||
if let Some(id) = id {
|
||||
let d = self
|
||||
.data
|
||||
.by_id(id)
|
||||
.ok_or_else(|| format!("Missing native scorable {id}"))?;
|
||||
let factor = self.penalty(id)
|
||||
* if self.collector == Collector::Air {
|
||||
self.air_factor
|
||||
} else {
|
||||
1.
|
||||
};
|
||||
let mut carrier = Carrier::new(
|
||||
d.metadata,
|
||||
d.points,
|
||||
factor,
|
||||
self.data.announcement.evaluate(d.points as f32),
|
||||
if chained_grab {
|
||||
previous_tick.unwrap_or(f.tick)
|
||||
} else {
|
||||
f.tick
|
||||
},
|
||||
delay_ticks(
|
||||
d.completion_delay,
|
||||
if self.grab_chain > 1 {
|
||||
self.data.collector.scalar(0x698)
|
||||
} else {
|
||||
0.
|
||||
},
|
||||
),
|
||||
f.switch,
|
||||
f.fakie,
|
||||
);
|
||||
if let Some(old) = self.carriers[slot].as_mut() {
|
||||
old.convert_to(&mut carrier, self.data.unannounced_factor);
|
||||
self.modified_trick = true;
|
||||
}
|
||||
if conversion.is_some() {
|
||||
self.trick_name = d.label.clone();
|
||||
}
|
||||
self.carriers[slot] = Some(carrier);
|
||||
self.sequence_active = true;
|
||||
self.idle_ticks = 0;
|
||||
}
|
||||
}
|
||||
let penalty = self.carriers[slot]
|
||||
.as_ref()
|
||||
.map(|c| self.penalty(c.scorable.id))
|
||||
.unwrap_or(1.);
|
||||
if let Some(c) = self.carriers[slot].as_mut() {
|
||||
if c.announce(f.tick, self.data.unannounced_factor) {
|
||||
let d = self
|
||||
.data
|
||||
.by_id(c.scorable.id)
|
||||
.ok_or("Missing announced scorable")?;
|
||||
self.trick_name = d.label.clone();
|
||||
self.stance = [f.switch, f.fakie, f.nollie, false];
|
||||
self.new_trick = true;
|
||||
if self.collector == Collector::Air && !self.air_repetition_set {
|
||||
self.air_repetition = penalty;
|
||||
self.air_repetition_set = true;
|
||||
}
|
||||
}
|
||||
if c.announced {
|
||||
self.held[slot] += f.dt;
|
||||
let previous_distance = self.distance[slot];
|
||||
let displacement =
|
||||
std::array::from_fn::<_, 3, _>(|i| f.position[i] - self.previous[i]);
|
||||
let delta = if self.collector == Collector::Ground && slot == 1 {
|
||||
displacement
|
||||
.iter()
|
||||
.zip(f.forward)
|
||||
.map(|(x, y)| x * y)
|
||||
.sum::<f32>()
|
||||
} else {
|
||||
(displacement.iter().map(|x| x * x).sum::<f32>()).sqrt()
|
||||
};
|
||||
if delta > f32::from_bits(0x3ba3d70a) {
|
||||
self.distance[slot] += delta;
|
||||
}
|
||||
let curve = match self.collector {
|
||||
Collector::Air if c.scorable.class == 2 => Some(0x460),
|
||||
Collector::Offboard => Some(0x140),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(curve) = curve {
|
||||
c.reward += (self.data.collector.curve(curve, self.held[slot]) * f.dt)
|
||||
* c.announcement_threshold;
|
||||
}
|
||||
let curve = match (self.collector, c.scorable.score_type) {
|
||||
(Collector::Ground, 9) => Some((0x000, 0x050)),
|
||||
(Collector::Ground, 8) => Some((0x0f0, 0x0a0)),
|
||||
(Collector::Ground, _) if slot == 2 => Some((0x230, 0x1e0)),
|
||||
(Collector::Grind, _) => Some((0x2d0, 0x280)),
|
||||
_ => None,
|
||||
};
|
||||
if let Some((distance, time)) = curve {
|
||||
let delta = (self.data.collector.curve(distance, self.distance[slot])
|
||||
- self.data.collector.curve(distance, previous_distance))
|
||||
.max(0.);
|
||||
self.metric_rewards[slot] = (self
|
||||
.data
|
||||
.collector
|
||||
.curve(time, self.held[slot])
|
||||
.mul_add(f.dt, delta))
|
||||
.mul_add(c.announcement_threshold, self.metric_rewards[slot]);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
pub fn advance(&mut self, f: Frame) -> Result<(), String> {
|
||||
self.new_trick = false;
|
||||
self.modified_trick = false;
|
||||
self.close_tricks = false;
|
||||
let descriptor = f
|
||||
.descriptor
|
||||
.and_then(|name| self.data.by_name(name))
|
||||
.filter(|d| !(f.flags & 0x02000000 != 0 && [67, 68, 69].contains(&d.metadata.id)))
|
||||
.map(|d| (d.metadata.id, d.metadata.class, d.metadata.score_type));
|
||||
let mut next = match f.category {
|
||||
FilteredCategory::Ground => Collector::Ground,
|
||||
FilteredCategory::Air => Collector::Air,
|
||||
FilteredCategory::Grind => Collector::Grind,
|
||||
FilteredCategory::Offboard | FilteredCategory::OffboardAir => Collector::Offboard,
|
||||
_ => Collector::None,
|
||||
};
|
||||
if f.state == 600 && matches!(next, Collector::Ground | Collector::Air) {
|
||||
next = Collector::Special;
|
||||
}
|
||||
if next == Collector::Air
|
||||
&& self.collector == Collector::Ground
|
||||
&& descriptor.is_some_and(|d| d.1 == 0)
|
||||
{
|
||||
next = Collector::Ground;
|
||||
}
|
||||
if next == Collector::Ground
|
||||
&& self.collector == Collector::Air
|
||||
&& descriptor.is_some_and(|d| d.1 == 3)
|
||||
{
|
||||
next = Collector::Air;
|
||||
}
|
||||
if f.teleported {
|
||||
next = Collector::None;
|
||||
}
|
||||
if next != self.collector {
|
||||
let complete = next != Collector::None;
|
||||
let previous_type = self
|
||||
.carriers
|
||||
.iter()
|
||||
.flatten()
|
||||
.last()
|
||||
.map(|c| c.scorable.score_type);
|
||||
for slot in 0..4 {
|
||||
self.finish(slot, complete, false);
|
||||
}
|
||||
if self.collector == Collector::Air {
|
||||
if complete {
|
||||
for (i, reward) in self.air_metrics.into_iter().enumerate() {
|
||||
if let Some(d) = self.data.by_id(129 + i) {
|
||||
self.session.holder.end_trick(d.metadata, reward);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.session.holder.finish_collector();
|
||||
self.landing_countdown = 2;
|
||||
}
|
||||
self.collector = next;
|
||||
self.start = f.position;
|
||||
self.peak = f.position[1];
|
||||
self.spin = 0.;
|
||||
self.previous_heading = f.forward[0].atan2(f.forward[2]);
|
||||
self.air_metrics = [0.; 5];
|
||||
self.air_repetition = 1.;
|
||||
self.air_repetition_set = false;
|
||||
self.air_factor = 1.;
|
||||
self.grab_chain = 0;
|
||||
self.collector_ticks = 0;
|
||||
self.manual_revert_ticks = 0;
|
||||
self.revert_id = None;
|
||||
if next == Collector::Air {
|
||||
self.session.holder.reward_sequence(1.);
|
||||
self.landing_countdown = 0;
|
||||
if previous_type == Some(8) {
|
||||
self.air_factor *= self.data.collector.scalar(0x68c);
|
||||
}
|
||||
if previous_type == Some(5) {
|
||||
self.air_factor *= self.data.collector.scalar(0x690);
|
||||
}
|
||||
if f.velocity[0] * f.velocity[0] + f.velocity[2] * f.velocity[2]
|
||||
< self.data.collector.scalar(0x66c).powi(2)
|
||||
{
|
||||
self.air_factor *= self.data.collector.scalar(0x688);
|
||||
}
|
||||
if f.switch && !f.fakie {
|
||||
self.air_factor *= self.data.collector.scalar(0x684);
|
||||
}
|
||||
if f.fakie && !f.switch {
|
||||
self.air_factor *= self.data.collector.scalar(0x694);
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut ids = [None; 4];
|
||||
self.collector_ticks = self.collector_ticks.saturating_add(1);
|
||||
match self.collector {
|
||||
Collector::Ground => {
|
||||
ids[0] = if f.flags & 0x80000000 != 0 {
|
||||
Some(0)
|
||||
} else if f.flags & 0x40000000 != 0 {
|
||||
Some(1)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
ids[1] = if f.flags & 0x08000000 != 0 {
|
||||
Some(2)
|
||||
} else if f.flags & 0x04000000 != 0 {
|
||||
Some(3)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if ids[1].is_some() && f.reverting {
|
||||
self.manual_revert_ticks += 1;
|
||||
ids[1] = None;
|
||||
} else {
|
||||
self.manual_revert_ticks = 0;
|
||||
}
|
||||
if self.manual_revert_ticks > 6 {
|
||||
ids[1] = None;
|
||||
}
|
||||
if ids[1].is_some() {
|
||||
if let Some(c) = &self.carriers[1] {
|
||||
ids[1] = Some(c.scorable.id);
|
||||
}
|
||||
}
|
||||
ids[2] = descriptor
|
||||
.filter(|d| d.0 == 60 && f.flags & 0x02000000 != 0)
|
||||
.map(|d| d.0);
|
||||
if f.flags & 0x20000000 != 0 {
|
||||
self.revert_id = Some(4);
|
||||
} else if f.flags & 0x10000000 != 0 {
|
||||
self.revert_id = Some(5);
|
||||
}
|
||||
ids[3] = if f.reverting { self.revert_id } else { None };
|
||||
}
|
||||
Collector::Air => ids[0] = descriptor.filter(|d| d.2 != 5).map(|d| d.0),
|
||||
Collector::Grind => ids[0] = (f.grind_id >= 0).then_some(f.grind_id as usize),
|
||||
Collector::Offboard => ids[0] = descriptor.filter(|d| d.2 == 6).map(|d| d.0),
|
||||
Collector::Special => ids[0] = descriptor.filter(|d| d.0 == 234).map(|d| d.0),
|
||||
Collector::None => {}
|
||||
}
|
||||
for (slot, id) in ids.into_iter().enumerate() {
|
||||
self.carrier(slot, id, &f)?;
|
||||
}
|
||||
if self.collector == Collector::Air {
|
||||
if self.collector_ticks > 5 || f.flags & 0x01000000 != 0 {
|
||||
self.sequence_active = true;
|
||||
}
|
||||
let heading = f.forward[0].atan2(f.forward[2]);
|
||||
let delta = (heading - self.previous_heading + std::f32::consts::PI)
|
||||
.rem_euclid(std::f32::consts::TAU)
|
||||
- std::f32::consts::PI;
|
||||
self.spin += delta;
|
||||
self.previous_heading = heading;
|
||||
self.peak = self.peak.max(f.position[1]);
|
||||
let dx = f.position[0] - self.start[0];
|
||||
let dz = f.position[2] - self.start[2];
|
||||
let scale = self.air_factor * self.air_repetition;
|
||||
self.air_metrics[0] =
|
||||
self.data.collector.curve(0x3c0, (dx * dx + dz * dz).sqrt()) * scale;
|
||||
self.air_metrics[1] =
|
||||
self.data.collector.curve(0x410, self.peak - self.start[1]) * scale;
|
||||
self.air_metrics[2] = self
|
||||
.data
|
||||
.collector
|
||||
.curve(0x320, f.position[1] - self.start[1])
|
||||
* scale;
|
||||
let turns =
|
||||
((self.spin.to_degrees().abs() + self.data.collector.scalar(0x63c)) / 180.) as i32;
|
||||
self.air_metrics[3] = self.data.collector.curve(0x370, (turns * 180) as f32) * scale;
|
||||
if f.body_flip
|
||||
&& self.carriers[0]
|
||||
.as_ref()
|
||||
.is_some_and(|c| c.announced && c.scorable.class == 2)
|
||||
{
|
||||
self.air_metrics[4] = self.data.collector.scalar(0x640) * scale;
|
||||
}
|
||||
}
|
||||
let active = self.carriers.iter().any(Option::is_some) || self.collector == Collector::Air;
|
||||
self.idle_ticks = if active {
|
||||
0
|
||||
} else {
|
||||
self.idle_ticks.saturating_add(1)
|
||||
};
|
||||
if self.session.holder.has_pending_sequence() {
|
||||
if f.landing.landing_data_167 {
|
||||
self.clean = f.landing.landing_type_96 == 0;
|
||||
self.sketchy = f.landing.landing_type_96 == 1
|
||||
&& f.landing.sideways_speed_84 > self.data.sketchy_side_speed
|
||||
&& f.landing.spin_92.abs() > 0.5;
|
||||
}
|
||||
if self.landing_countdown == 0 {
|
||||
let mut factor = 1.;
|
||||
if f.switch && !f.fakie {
|
||||
factor *= self.data.collector.scalar(0x61c);
|
||||
}
|
||||
if f.fakie && !f.switch {
|
||||
factor *= self.data.collector.scalar(0x62c);
|
||||
}
|
||||
if self.collector == Collector::Grind {
|
||||
factor *= self.data.collector.scalar(0x628);
|
||||
}
|
||||
if self.clean {
|
||||
factor *= self.data.collector.scalar(0x630);
|
||||
}
|
||||
if self.sketchy {
|
||||
factor *= self.data.collector.scalar(0x620);
|
||||
}
|
||||
self.session.holder.reward_sequence(factor);
|
||||
} else {
|
||||
self.landing_countdown -= 1;
|
||||
}
|
||||
}
|
||||
let scales = match self.collector {
|
||||
Collector::Air => (0x668, 0x664),
|
||||
Collector::Grind => (0x618, 0x614),
|
||||
Collector::Offboard => (0x608, 0x604),
|
||||
_ => (0x610, 0x60c),
|
||||
};
|
||||
let line_scale = if active {
|
||||
self.data.collector.scalar(scales.0)
|
||||
} else {
|
||||
1.
|
||||
};
|
||||
let combo_scale = if active {
|
||||
self.data.collector.scalar(scales.1)
|
||||
} else {
|
||||
1.
|
||||
};
|
||||
self.session
|
||||
.line
|
||||
.advance(f.dt, self.data.line_drain, line_scale, active);
|
||||
self.session
|
||||
.combo
|
||||
.timer
|
||||
.advance(f.dt, self.data.combo_drain, combo_scale, active);
|
||||
let bailout = self.collector == Collector::None && self.sequence_active;
|
||||
if self.sequence_active && (bailout || self.idle_ticks >= 3 && self.landing_countdown == 0)
|
||||
{
|
||||
if bailout {
|
||||
self.session.holder.cancel_pending();
|
||||
}
|
||||
self.sequence_score =
|
||||
self.session
|
||||
.publish_sequence(&self.data.session_rules(), 1., bailout, true);
|
||||
self.sequence_active = false;
|
||||
self.close_tricks = true;
|
||||
} else if self.sequence_active {
|
||||
let s = &self.session.holder.snapshot;
|
||||
self.sequence_score = (s.accumulated
|
||||
+ s.general_pending
|
||||
+ s.fingerflip_pending
|
||||
+ self
|
||||
.carriers
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(i, c)| {
|
||||
c.as_ref().map(|c| {
|
||||
if self.collector == Collector::Grind
|
||||
|| self.collector == Collector::Ground && i < 3
|
||||
{
|
||||
if c.scorable.score_type == 9 {
|
||||
c.reward.max(self.metric_rewards[i])
|
||||
} else {
|
||||
self.metric_rewards[i]
|
||||
}
|
||||
} else {
|
||||
c.reward
|
||||
}
|
||||
})
|
||||
})
|
||||
.sum::<f32>()
|
||||
+ self.air_metrics.iter().sum::<f32>())
|
||||
* self.session.combo.multiplier;
|
||||
}
|
||||
self.session.settle_line(f.teleported || bailout, active);
|
||||
self.previous = f.position;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
+56
-31
@@ -59,40 +59,65 @@ 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
|
||||
## Production integration and validation
|
||||
|
||||
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.
|
||||
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 production fixed tick now supplies animation descriptors, conditioned category,
|
||||
grind IDs, landing output and physical motion to `scoring_runtime`. The runtime
|
||||
loads native VLT points, repetition, announcement and collector curves, maintains
|
||||
carriers and continuous distance rewards, and publishes sequence/line accounting.
|
||||
It is a usable integration for testing, **not a finished native-parity port**.
|
||||
|
||||
The HUD still needs a complete movie object hierarchy, native bindings,
|
||||
timeline execution and rendering with multiplicative and additive colors.
|
||||
Superclass preloads and supported action coverage also remain incomplete.
|
||||
The audit's dummy host intentionally does not establish native binding parity.
|
||||
The HUD executes the owned trickdisplay ActionScript and timelines, including
|
||||
score, multiplier, line meter, stance and trick-name updates. It renders original
|
||||
shape atlases and font glyphs through a separate 1280x720 camera with inherited
|
||||
multiplicative/additive colors. Movie objects are collected and mesh/material
|
||||
slots reused. Fixed updates pause with gameplay; map-generation changes recreate
|
||||
the movie even while paused. Missing assets or unsupported actions are logged.
|
||||
|
||||
Font resolution is recovered from FontManager initialization `82808AE8`,
|
||||
APT-name lookup `82809208` and loading `82809308`. VLT class
|
||||
`FECFBCAF356518C4` maps the APT name (field `340832CCFD9FDEB4`, layout +8) to
|
||||
`FileName` (layout +4). In particular, `FuturaOuterGlow` maps to `futurashadow`,
|
||||
not the prior extractor's inferred `Futura Glow`. The prepared movie has no
|
||||
unresolved font references. The manifest retains VLT record provenance and
|
||||
native scale/offset fields. The community symbol calling `82809168`
|
||||
GetFontByAptName is misleading: that function searches the filename column;
|
||||
`82809208` searches the APT-name column.
|
||||
Font resolution follows FontManager initialization `82808AE8`, APT-name lookup
|
||||
`82809208` and loading `82809308`. VLT class `FECFBCAF356518C4` maps AptName to
|
||||
FileName; `FuturaOuterGlow` resolves to `futurashadow`. Glyph advance and placement
|
||||
use native scale/ascent metrics. English text resolves through the owned language
|
||||
asset. No replacement interface or bundled game assets are introduced.
|
||||
|
||||
Pause, map-change generation reset and resource reuse remain to be integrated
|
||||
and checked. A release compilation succeeds; its executable is copied into
|
||||
the ignored task-local `logs/scoring/build` directory, with an untracked
|
||||
`Launch scoring worktree.bat`. This is a compilation checkpoint, not a working
|
||||
scoring/HUD feature build. No game/recomp,
|
||||
controller harness, gameplay automation, `--check-assets`, or screenshot
|
||||
capture has been run for this work.
|
||||
Validation on 2026-09-08:
|
||||
|
||||
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.
|
||||
- All nine scoring unit tests pass, covering clock wrap, early completion,
|
||||
conversion, cancellation, repetition, timer thresholds and one-time banking.
|
||||
- The data-only scoring-flow example credits the authored 100-point stationary
|
||||
kickflip once and verifies idle stability and teleport cancellation.
|
||||
- The original HUD data-only audit passes 1,800 frames, with 172 VM slots,
|
||||
116 display instances, at most 31 draw batches and finite geometry.
|
||||
- The release target builds successfully with static MSVC CRT, without default
|
||||
features. Compiler warnings remain in the existing game and audit-only code.
|
||||
- No game, recomp, controller harness, gameplay automation, `--check-assets`
|
||||
or screenshot capture was run. GPU rendering and interactive behavior remain
|
||||
unverified until the user launches the build.
|
||||
|
||||
The copied build and launcher are in ignored `logs/scoring/build`. The launcher
|
||||
uses this worktree's HUD cache and the existing owned asset installation and
|
||||
starts paused. It checks required paths before running and retains failures on
|
||||
screen. The executable SHA256 is
|
||||
`775264c02f456665052355d466b99cb2e3ead1e359af3123c9a53afd68e1e182`.
|
||||
|
||||
## Native parity gaps
|
||||
|
||||
These are implementation gaps, not merely missing gameplay validation:
|
||||
|
||||
- Collector activation gates and exact publication timing need further porting;
|
||||
the current runtime uses conditioned categories and an idle countdown.
|
||||
- Air spin uses accumulated board heading rather than the complete native
|
||||
transform accumulator. Body-flip direction and some landing modifiers remain
|
||||
incomplete.
|
||||
- Gap/context collectors and their native ground-query inputs, contextual
|
||||
bonuses and off-board height rewards are not wired.
|
||||
- Revert recognition depends on an unpublished physical state flag in the
|
||||
current host. Full native revert scoring is not yet available.
|
||||
- Trick-name spin/direction modifiers and the HUD manager's stance transitions
|
||||
are incomplete. The bindings expose five metric slots but currently only
|
||||
supply the base trick label and default modifier flags.
|
||||
- The compact VM supports the exercised movie paths, not arbitrary APT programs.
|
||||
Superclass/native constructor behavior is limited, and Math.random uses a
|
||||
local presentation RNG rather than the original engine RNG stream.
|
||||
|
||||
Do not describe this build as fully finished or an exact native recreation.
|
||||
@@ -86,6 +86,8 @@ def prepare(game: Path, output: Path, collections: Path):
|
||||
'characters': list(bundle['characters'].values()),
|
||||
'shapes': shapes, 'fonts': fonts, 'actions': blocks,
|
||||
'font_mappings':mappings,
|
||||
'language': {row['label'].strip(): row['value'] for row in
|
||||
json.loads((output / 'metadata/languages/english_global.json').read_text(encoding='utf-8'))['entries']},
|
||||
'unresolved_fonts': [family for family, asset in fonts.items() if asset is None],
|
||||
}
|
||||
target = output / 'runtime' / 'trickdisplay.json'
|
||||
|
||||
Reference in new issue
Block a user