Compare commits
26 Commits
auto/re-ca
...
auto/re-fr
| Author | SHA1 | Date | |
|---|---|---|---|
| 6c9380ff13 | |||
| 4f6fcf36dc | |||
| 8b03a52aa6 | |||
| 92d9683f3a | |||
| a3f14710a4 | |||
| 0a84c1358e | |||
| 6c7025851e | |||
| dc46339b63 | |||
| dfa769420d | |||
| 5f3c618b51 | |||
| 850b04c606 | |||
| 3d3d6726fa | |||
| 0b2f004ad0 | |||
| faa7b3d611 | |||
| a79a1da183 | |||
| 4cbce6bd21 | |||
| aac017dd0d | |||
| 3ef2c438ae | |||
| 0bc790de52 | |||
| 023bb71cfd | |||
| f490fecefb | |||
| 20299c05a1 | |||
| 90e0e66c7b | |||
| 33ae20896e | |||
| 8ecd70f1bc | |||
| 10a96844ba |
55
crates/sylpheed-formats/examples/achievements_map.rs
Normal file
@@ -0,0 +1,55 @@
|
||||
//! RE probe: the `ACHIEVEMENTS_REQUIREMENTS` config list.
|
||||
//!
|
||||
//! `GamePart_Debriefing` (`0x8218CF38`..`0x82191B18`) walks this list after a
|
||||
//! mission (`sub_8218F9A8`): for each entry it takes the entry's **index** `n`,
|
||||
//! tests bit `n` of an awarded-mask, and if the bit is clear it evaluates the
|
||||
//! entry (`0x8218FAB0`) and sets the bit when satisfied. `GamePart_ChallengeMission`
|
||||
//! then gates each challenge mission on a bit of the same space via its own
|
||||
//! `REQUIREMENT` key. So this list *is* the achievement/bit numbering.
|
||||
//!
|
||||
//! Run: cargo run --release -p sylpheed-formats --example achievements_map -- <disc-root>
|
||||
|
||||
use sylpheed_formats::{idxd::IdxdObject, pak::PakArchive};
|
||||
|
||||
fn find(h: &[u8], n: &[u8]) -> bool {
|
||||
h.windows(n.len()).any(|w| w == n)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let disc = std::env::args().nth(1).unwrap_or_else(|| {
|
||||
std::env::var("SYLPHEED_DISC").expect("pass disc root or set SYLPHEED_DISC")
|
||||
});
|
||||
let dat = format!("{disc}/dat");
|
||||
let mut paks: Vec<_> = std::fs::read_dir(&dat)
|
||||
.expect("dat dir")
|
||||
.filter_map(|e| e.ok())
|
||||
.map(|e| e.path())
|
||||
.filter(|p| p.extension().is_some_and(|x| x == "pak"))
|
||||
.collect();
|
||||
paks.sort();
|
||||
|
||||
for p in &paks {
|
||||
let Ok(arc) = PakArchive::open(p) else { continue };
|
||||
for (i, e) in arc.entries().iter().enumerate() {
|
||||
let Ok(b) = arc.read(e) else { continue };
|
||||
if !find(&b, b"ACHIEVEMENTS_REQUIREMENTS") {
|
||||
continue;
|
||||
}
|
||||
let name = p.file_name().unwrap().to_string_lossy().to_string();
|
||||
match IdxdObject::parse(&b) {
|
||||
Ok(o) => {
|
||||
let t = o.tokens();
|
||||
println!(
|
||||
"\n===== {name} entry #{i} schema {:08x} {} tokens =====",
|
||||
o.schema_hash,
|
||||
t.len()
|
||||
);
|
||||
for (j, tok) in t.iter().enumerate() {
|
||||
println!(" {j:3} {tok}");
|
||||
}
|
||||
}
|
||||
Err(err) => println!("\n===== {name} entry #{i}: not IDXD ({err}) ====="),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
81
crates/sylpheed-formats/examples/challenge_map.rs
Normal file
@@ -0,0 +1,81 @@
|
||||
//! RE probe: what does the disc say about CHALLENGE / EXTRA missions?
|
||||
//!
|
||||
//! The title's stage-config reader (`0x82184b98`..`0x82184e94`) selects one of three
|
||||
//! config sections by a mode field at `obj+144`:
|
||||
//! mode == 3 -> "EXTRA"
|
||||
//! mode == 5 or 6 -> "CHALLENGE"
|
||||
//! otherwise -> "FILE"
|
||||
//! so challenge missions are a *mode*, not a separate stage numbering. This probe
|
||||
//! asks the disc which stage records exist and what GP_CHALLENGE.pak carries.
|
||||
//!
|
||||
//! Run: cargo run --release -p sylpheed-formats --example challenge_map -- <disc-root>
|
||||
|
||||
use sylpheed_formats::{idxd::IdxdObject, pak::PakArchive};
|
||||
|
||||
fn main() {
|
||||
let disc = std::env::args().nth(1).unwrap_or_else(|| {
|
||||
std::env::var("SYLPHEED_DISC").expect("pass disc root or set SYLPHEED_DISC")
|
||||
});
|
||||
|
||||
println!("=== 1. StageResource records in GP_MAIN_GAME_E.pak ===");
|
||||
let arc = PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")).unwrap();
|
||||
let mut rows = vec![];
|
||||
for e in arc.entries() {
|
||||
let Ok(b) = arc.read(e) else { continue };
|
||||
let Ok(o) = IdxdObject::parse(&b) else { continue };
|
||||
if o.schema_hash != 0x3c9ae32e {
|
||||
continue;
|
||||
}
|
||||
let t = o.tokens();
|
||||
let stage = t
|
||||
.iter()
|
||||
.find_map(|s| s.strip_prefix("EnumUnit_").map(|x| x.trim_end_matches(".tbl").to_string()))
|
||||
.unwrap_or("?".into());
|
||||
let bg = o.get_raw("BackGroundID").unwrap_or("?").to_string();
|
||||
rows.push((stage, bg, t.len()));
|
||||
}
|
||||
rows.sort();
|
||||
println!(" {} stage records", rows.len());
|
||||
for (s, bg, n) in &rows {
|
||||
println!(" {s:8} bg={bg:16} tokens={n}");
|
||||
}
|
||||
|
||||
println!("\n=== 2. GP_CHALLENGE.pak contents ===");
|
||||
match PakArchive::open(format!("{disc}/dat/GP_CHALLENGE.pak")) {
|
||||
Ok(ch) => {
|
||||
let mut by_schema: std::collections::BTreeMap<u32, usize> = Default::default();
|
||||
let mut all_tokens: Vec<(u32, Vec<String>)> = vec![];
|
||||
for e in ch.entries() {
|
||||
let Ok(b) = ch.read(e) else { continue };
|
||||
let Ok(o) = IdxdObject::parse(&b) else { continue };
|
||||
*by_schema.entry(o.schema_hash).or_default() += 1;
|
||||
all_tokens.push((o.schema_hash, o.tokens().iter().map(|s| s.to_string()).collect()));
|
||||
}
|
||||
println!(" {} entries, {} IDXD objects", ch.entries().len(), all_tokens.len());
|
||||
for (h, n) in &by_schema {
|
||||
println!(" schema {h:08x} x{n}");
|
||||
}
|
||||
for (h, t) in all_tokens.iter().take(12) {
|
||||
println!(" -- {h:08x}: {:?}", &t[..t.len().min(60)]);
|
||||
}
|
||||
}
|
||||
Err(e) => println!(" open failed: {e}"),
|
||||
}
|
||||
|
||||
println!("\n=== 3. tokens mentioning Challenge / EX across the main pak ===");
|
||||
let mut hits: std::collections::BTreeSet<String> = Default::default();
|
||||
for e in arc.entries() {
|
||||
let Ok(b) = arc.read(e) else { continue };
|
||||
let Ok(o) = IdxdObject::parse(&b) else { continue };
|
||||
for t in o.tokens() {
|
||||
let l = t.to_ascii_lowercase();
|
||||
if l.contains("challenge") || t.ends_with("_EX") || t.contains("_EX4") || t.contains("_EX5") {
|
||||
hits.insert(format!("{:08x} {t}", o.schema_hash));
|
||||
}
|
||||
}
|
||||
}
|
||||
for h in hits.iter().take(120) {
|
||||
println!(" {h}");
|
||||
}
|
||||
println!(" ({} distinct)", hits.len());
|
||||
}
|
||||
78
crates/sylpheed-formats/examples/challenge_screen.rs
Normal file
@@ -0,0 +1,78 @@
|
||||
//! RE probe: the GamePart_ChallengeMission screen config.
|
||||
//!
|
||||
//! The class's code range (`0x82187E60`..`0x8218CF10`, bounded by the factory
|
||||
//! creator thunks either side) references these config keys:
|
||||
//! MISSIONS, MISSION_ID, NEW_STAGE, REQUIREMENT, REQUIREMENT_DESC, "Always",
|
||||
//! GRAY_BUTTON, NORMAL_BUTTON, THUMBNAIL, STAGE_DESC, TEXT_STAGE,
|
||||
//! RECORD_TYPE, "Time", TEXT_RECORD, BASE_INFO
|
||||
//! i.e. the challenge list is a *config record* with a per-mission REQUIREMENT.
|
||||
//! It lives in `tables.pak` (one copy per language), not in GP_CHALLENGE.pak.
|
||||
//!
|
||||
//! Run: cargo run --release -p sylpheed-formats --example challenge_screen -- <disc-root>
|
||||
|
||||
use sylpheed_formats::{idxd::IdxdObject, pak::PakArchive};
|
||||
|
||||
const KEYS: &[&str] = &[
|
||||
"MISSIONS",
|
||||
"MISSION_ID",
|
||||
"REQUIREMENT",
|
||||
"REQUIREMENT_DESC",
|
||||
"NEW_STAGE",
|
||||
"GRAY_BUTTON",
|
||||
"NORMAL_BUTTON",
|
||||
"RECORD_TYPE",
|
||||
"THUMBNAIL",
|
||||
"STAGE_DESC",
|
||||
];
|
||||
|
||||
fn find(h: &[u8], n: &[u8]) -> bool {
|
||||
h.windows(n.len()).any(|w| w == n)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let disc = std::env::args().nth(1).unwrap_or_else(|| {
|
||||
std::env::var("SYLPHEED_DISC").expect("pass disc root or set SYLPHEED_DISC")
|
||||
});
|
||||
|
||||
let dat = format!("{disc}/dat");
|
||||
let mut paks: Vec<_> = std::fs::read_dir(&dat)
|
||||
.expect("dat dir")
|
||||
.filter_map(|e| e.ok())
|
||||
.map(|e| e.path())
|
||||
.filter(|p| p.extension().is_some_and(|x| x == "pak"))
|
||||
.collect();
|
||||
paks.sort();
|
||||
|
||||
for p in &paks {
|
||||
let Ok(arc) = PakArchive::open(p) else { continue };
|
||||
for (i, e) in arc.entries().iter().enumerate() {
|
||||
let Ok(b) = arc.read(e) else { continue };
|
||||
if KEYS.iter().filter(|k| find(&b, k.as_bytes())).count() < KEYS.len() {
|
||||
continue;
|
||||
}
|
||||
let name = p.file_name().unwrap().to_string_lossy().to_string();
|
||||
match IdxdObject::parse(&b) {
|
||||
Ok(o) => {
|
||||
let toks = o.tokens();
|
||||
// language tag: the PATH value, e.g. "dat\GP_CHALLENGE.pak+eng\"
|
||||
let lang = toks
|
||||
.iter()
|
||||
.find(|t| t.contains("GP_CHALLENGE.pak+"))
|
||||
.map(|t| t.to_string())
|
||||
.unwrap_or_default();
|
||||
println!(
|
||||
"\n===== {name} entry #{i} schema {:08x} {} tokens [{lang}] =====",
|
||||
o.schema_hash,
|
||||
toks.len()
|
||||
);
|
||||
// Print the pool in order; the record is key/value interleaved and
|
||||
// IDXD dedupes repeats, so pairing is read by eye, not asserted.
|
||||
for (j, t) in toks.iter().enumerate() {
|
||||
println!(" {j:3} {t}");
|
||||
}
|
||||
}
|
||||
Err(e) => println!("\n===== {name} entry #{i}: IDXD parse failed: {e} ====="),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
18
crates/sylpheed-formats/examples/screen_configs.rs
Normal file
@@ -0,0 +1,18 @@
|
||||
//! RE probe: dump tables.pak screen-config records that mention a given token.
|
||||
//! Usage: screen_configs <disc-root> <substring>
|
||||
use sylpheed_formats::{idxd::IdxdObject, pak::PakArchive};
|
||||
fn main(){
|
||||
let a:Vec<String>=std::env::args().collect();
|
||||
let needle=a.get(2).cloned().unwrap_or_else(||"CHALLENGE".into());
|
||||
let arc=PakArchive::open(format!("{}/dat/tables.pak",a[1])).unwrap();
|
||||
for (i,e) in arc.entries().iter().enumerate(){
|
||||
let Ok(b)=arc.read(e) else{continue};
|
||||
let Ok(o)=IdxdObject::parse(&b) else{continue};
|
||||
let t=o.tokens();
|
||||
if !t.iter().any(|s|s.to_lowercase().contains(&needle.to_lowercase())){continue}
|
||||
let path=t.iter().find(|s|s.contains(".pak+")).cloned().unwrap_or_default();
|
||||
if !path.is_empty() && !path.contains("+eng"){continue}
|
||||
println!("\n=== entry #{i} schema {:08x} [{path}] {} tokens ===",o.schema_hash,t.len());
|
||||
for (j,tok) in t.iter().enumerate(){println!(" {j:3} {tok}");}
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,8 @@ Promote to a prose `structures/…md` file when a format needs behavioural notes
|
||||
|
||||
| Technique | Conf. | Spec | Notes |
|
||||
|-----------|-------|------|-------|
|
||||
| Scripted input without a virtual controller | ✅ | [`tools/re-capture/pad.py`](../../tools/re-capture/pad.py) + Canary `--hid=file` | The old `vgamepad` path created its device through `/dev/uinput`, which is **not namespaced** — a pad made inside the container registers with the HOST's input stack, so every scripted press leaked to the desktop. Canary now carries a header-only driver that reads pad state from a **text file** (`--hid=file --pad_file=…`): no kernel device, nothing leaves the container, and analogue values are exact. ⚠️ The trap: 360 menus poll **`XamInputGetKeystrokeEx`**, not `GetState` — with `GetKeystroke` stubbed the pad looks completely dead on a title screen while its own log shows the press arriving. Implemented edge-triggered, no auto-repeat (scripted input wants one event per press). Proven end to end: boot → title → main menu → EXTRAS from the file alone |
|
||||
| Live guest-memory write | ✅ | [`tools/re-capture/gpoke.py`](../../tools/re-capture/gpoke.py) | Write companion to `gmem.py`, same guest-VA → `/dev/shm` map; prints before/after per word. Used to confirm the challenge gate's cleared-stage mask on the running game |
|
||||
| Live guest-memory read | ✅ | [`tools/re-capture/gmem.py`](../../tools/re-capture/gmem.py) | Canary backs the guest address space with `/dev/shm/xenia_memory_*`; guest VAs map in through Xenia's fixed table. Full-RAM search ~0.2 s (sparse, `SEEK_DATA`). No debugger, no emulator patch, game keeps running |
|
||||
| IDXD object layout solver | ✅ | [`tools/re-capture/weapon_runtime.py`](../../tools/re-capture/weapon_runtime.py) | Scan RAM for a class's vtable → enumerate its objects → brute-force `(field, offset, encoding)` against the disc records. Accepts a binding only on **zero** contradictions. Generalizes to any IDXD-backed definition |
|
||||
| Live entity state, anchored on the definition | ✅ | [`tools/re-capture/own_state.py`](../../tools/re-capture/own_state.py) · [autopilot](autopilot-memory-driven.md) | An undamaged craft holds its definition's own numbers, so a *solved definition field* locates the matching live field without a value scan: definition `HP` (1500) → **hull at `position+0x154`**, confirmed by a trace across a death (30/60/90 per hit, negative at 0). Reusable for any live counter whose maximum the definition carries |
|
||||
@@ -41,8 +43,7 @@ Promote to a prose `structures/…md` file when a format needs behavioural notes
|
||||
|
||||
## Functions / code paths
|
||||
|
||||
_None documented yet — populated during the dynamic-RE phase._
|
||||
|
||||
| Function | Conf. | Reimpl. | Summary |
|
||||
|----------|-------|---------|---------|
|
||||
| — | — | — | — |
|
||||
| Achievement table + XAM read path | ✅/🟡 | [achievements](structures/achievements.md) | The XEX's `XACH` resource (`.pe` `0x8FBCBC`, 36-byte records) defines **24 achievements summing to 1000G** — the retail total, which self-checks the stride and field offsets. `GamePart_Debriefing` (`0x8218CF38`–`0x82191B18`) does two things: it walks the on-disc `ACHIEVEMENTS_REQUIREMENTS` list (`tables.pak` #16, entries `ACHIEVEMENT01…24`, in achievement-id order — its `ShootDownAircrafts`/`ShootDownShips`/`ShootDownWeight`/`GetAllWeapons`/`GetAllAchievements` types line up with ids 19–24 exactly as `XACH` names them), evaluating each and setting a bit; **and it enumerates `XACHIEVEMENT_DETAILS` from XAM** — 36-byte records confirmed by the `0x38E38E39`+`srawi 3` divide-by-36, `dwId` at `+0`, `dwFlags & 0x00020000` (`…_ACHIEVED`) at `+32`, behind a waited-then-closed async handle. **So earned state comes from the console profile, not the 545-byte save** — and the masks it builds are `1 << dwId`, i.e. **bit = the 1-based id**. **The challenge missions do NOT gate on this** — an earlier claim here, refuted by finding `+80`'s sole writer: it is `GamePart_StageClear` setting `1 << stage`, so that word is a *cleared-stage* mask and the shared "24" was a coincidence. `GetAllAchievements`/`GetAllWeapons` are requirement **types**, not debug cheats |
|
||||
| Challenge-mission gate + stage-config switch | ✅ | [challenge-mission-gate](challenge-mission-gate.md) | **`GamePart_ChallengeMission` gates each of the six challenge missions on a CLEARED-STAGE bit**: `REQUIREMENT` absent or `"Always"` → available, else `n = atoi(v)` and it tests bit `n` of singleton `+80` (`n < 24`) or bit `n-24` of `+1956` (`n ≥ 24`) — which is the disc's own stage numbering (story 1–16 and tutorial 18–23 below 24, challenge 24–29 above). Word A's **sole writer** is `0x821C1820` in `GamePart_StageClear`, doing `1 << (this+84)` where `this+84` is the stage number (it also indexes a 20-byte per-stage record array and the debriefing `STAGE` sprite list). So `TimeAttack` needs **stage 16** — the last story mission — and the rest chain off challenge stages 25–29. The six-mission table is on disc in `tables.pak` (schema `54a10697`). Separately, the stage loader picks its config section from a **mission-kind field at `object+144`**: `3` → `EXTRA`, `5`/`6` → `CHALLENGE`, anything else → `FILE`; two further sites treat `{3,5,6}` as one class. Constructed as `0` (`sub_821783D8`) and only ever *cleared* inside the class, so the kind comes from the launching GamePart, **not** from the stage number — which is a mechanism (🟡, unproven) for why patching the save's stage field to a challenge stage kills the load. Same note carries the **GamePart id table** (`0x820A1630`, 29 ids, `GP_CHALLENGE` = 26, cross-checked against the image's own `RegisterToFactory<26, …>` text) and the disc's **three stage families** — `S01`–`S16` story, `S18`–`S23` tutorial, `S24`–`S29` challenge, plus `Test`, matching `weapon.tbl`'s 16 + 6 + 6 key set exactly. `GP_CHALLENGE.pak` holds **0 IDXD objects** — it is the menu screen; challenge missions reuse `GP_MAIN_GAME_E.pak`'s stage records |
|
||||
|
||||
76
docs/re/captures/achievements-xach.txt
Normal file
@@ -0,0 +1,76 @@
|
||||
XACH @0x8FBCBC 24 achievements (string table #5 of 7)
|
||||
|
||||
id 1 | bit 0 | 20G | Space Combat Award
|
||||
unlocked: Received after your first space battle in the Glasner Training Area.
|
||||
locked : Awarded for participating in fighter combat in outer space.
|
||||
id 2 | bit 1 | 20G | Schlos Base Defense Award
|
||||
unlocked: Received for stopping the missile attack on Schlos Base.
|
||||
locked : Awarded for stopping enemy attacks on Schlos Base.
|
||||
id 3 | bit 2 | 20G | Aegis of the People Medal
|
||||
unlocked: Received for escorting all 7 refugee ships to safety.
|
||||
locked : Awarded for escorting the 7 refugee ships to safety.
|
||||
id 4 | bit 3 | 20G | TCAF Luna Medal
|
||||
unlocked: Received for bravely helping the fleet escape during the invasion of the Matisse System.
|
||||
locked : Awarded for bravery beyond the call of duty to escort allied vessels to safety.
|
||||
id 5 | bit 4 | 40G | TCAF Mars Medal
|
||||
unlocked: Received for bravery beyond the call of duty during the escape from the Matisse System.
|
||||
locked : Awarded for bravery beyond the call of duty under fierce enemy attacks.
|
||||
id 6 | bit 5 | 50G | Soldier's Charm Amulet
|
||||
unlocked: Given to you by Raymond as a token of his trust.
|
||||
locked : Given to you by Raymond as a token of his trust.
|
||||
id 7 | bit 6 | 20G | White Griffons Patch
|
||||
unlocked: Received by the commander and pilots of the White Griffon Squadron when it is formed.
|
||||
locked : Awarded to the commander and pilots of the White Griffon Squadron when it is formed.
|
||||
id 8 | bit 7 | 30G | TCAF Jupiter Medal
|
||||
unlocked: Received for bravery during the enemy's attack on the Alberti System.
|
||||
locked : Awarded for bravery beyond the call of duty during withdrawal of the allied fleet.
|
||||
id 9 | bit 8 | 40G | Furious Pursuit Badge
|
||||
unlocked: Received for continuing attacks on the enemy and shooting down a large number of ships.
|
||||
locked : Awarded for diligently shooting down large numbers of enemy ships.
|
||||
id 10 | bit 9 | 30G | Solo Aerospace Combat Award
|
||||
unlocked: Received for descending into Acheron's atmosphere and engaging in combat alone.
|
||||
locked : Awarded for descending into the atmosphere and engaging in combat alone.
|
||||
id 11 | bit 10 | 30G | Operation Nebula Blaze Award
|
||||
unlocked: Received for completing the extremely hazardous Operation Nebula Blaze.
|
||||
locked : Awarded for fulfilling duty and fighting bravely in this difficult operation.
|
||||
id 12 | bit 11 | 40G | Guilty Roses Patch
|
||||
unlocked: Received for repelling the Guilty Roses Squadron.
|
||||
locked : Awarded for repelling the enemy Guilty Roses Squadron.
|
||||
id 13 | bit 12 | 30G | Super Battleship Slayer Patch
|
||||
unlocked: Received for shooting down the second S battleship.
|
||||
locked : Awarded for shooting down the second S battleship supporting the enemy fleet.
|
||||
id 14 | bit 13 | 40G | TCAF Terra Medal
|
||||
unlocked: Received for shielding the fleet and seeing that all ships safely fled the Ingres System.
|
||||
locked : Awarded for shielding the fleet to allow its safe escape.
|
||||
id 15 | bit 14 | 40G | Hellfires Patch
|
||||
unlocked: Received for repelling the enemy Hellfire Squadron.
|
||||
locked : Awarded for repelling the enemy Hellfire Squadron.
|
||||
id 16 | bit 15 | 50G | Night Ravens Patch
|
||||
unlocked: Received for challenging and eradicating the Night Raven Squadron.
|
||||
locked : Awarded for challenging and eradicating the enemy Night Raven Squadron.
|
||||
id 17 | bit 16 | 40G | Solar System Defense Award
|
||||
unlocked: Received for great achievements during the campaign to defend the Solar System.
|
||||
locked : Awarded for great achievement during the campaign to defend the Solar System.
|
||||
id 18 | bit 17 | 40G | Special Operations Medal
|
||||
unlocked: Received for heroically destroying the Prometheus Driver.
|
||||
locked : Awarded for heroism in destroying the enemy's main weapon.
|
||||
id 19 | bit 18 | 30G | 1,000 Units Destroyed Medal
|
||||
unlocked: Received for shooting down 1,000 enemy fighters and attackers in combat.
|
||||
locked : Awarded for shooting down 1,000 enemy fighters and attackers in space combat.
|
||||
id 20 | bit 19 | 70G | 10,000 Units Destroyed Medal
|
||||
unlocked: Received for shooting down 10,000 enemy fighters and attackers in combat.
|
||||
locked : Awarded for shooting down 10,000 enemy fighters and attackers in space combat.
|
||||
id 21 | bit 20 | 50G | Ship Hunter Award
|
||||
unlocked: Received for shooting down 100 enemy warships in combat.
|
||||
locked : Awarded for shooting down 100 warships in space combat.
|
||||
id 22 | bit 21 | 70G | Gigaton Club Patch
|
||||
unlocked: Received for shooting down enemy vessels with a total weight of one gigaton.
|
||||
locked : Awarded for downing several enemy vessels with a combined weight of one gigaton.
|
||||
id 23 | bit 22 | 80G | Weapon Lord Patch
|
||||
unlocked: Received after you collect all usable equipment for the Delta Saber.
|
||||
locked : Awarded for collecting all usable equipment for the Delta Saber.
|
||||
id 24 | bit 23 | 100G | TCAF Pilot's Commendation
|
||||
unlocked: Commemmorates you as one of the greatest pilots in history.
|
||||
locked : Awarded to the greatest pilots in the TCAF.
|
||||
|
||||
total gamerscore = 1000 [OK: retail total]
|
||||
5
docs/re/captures/axis-probe-rows-pinned.csv
Normal file
@@ -0,0 +1,5 @@
|
||||
input,roll_deg_s,yaw_deg_s,pitch_deg_s
|
||||
rx+,0.0,0.0,0.0
|
||||
ry+,0.0,0.0,0.0
|
||||
LB,0.0,0.0,0.0
|
||||
RB,0.0,0.0,0.0
|
||||
|
7
docs/re/captures/axis-probe-stage02.csv
Normal file
@@ -0,0 +1,7 @@
|
||||
input,roll_deg_s,yaw_deg_s,pitch_deg_s
|
||||
lx+,209.8,0.87,10.07
|
||||
ly+,0.0,154.12,0.0
|
||||
rx+,0.0,0.0,0.0
|
||||
ry+,161.07,86.98,37.06
|
||||
LB,0.0,0.0,0.0
|
||||
RB,0.0,0.0,0.0
|
||||
|
BIN
docs/re/captures/challenge-extras-after-poke.png
Normal file
|
After Width: | Height: | Size: 921 KiB |
96
docs/re/captures/challenge-map.txt
Normal file
@@ -0,0 +1,96 @@
|
||||
=== 1. StageResource records in GP_MAIN_GAME_E.pak ===
|
||||
29 stage records
|
||||
S01 bg=Lebendorf tokens=52
|
||||
S02 bg=Lebendorf tokens=51
|
||||
S03 bg=Planet_Lebendorf tokens=51
|
||||
S04 bg=Lebendorf_far tokens=54
|
||||
S05 bg=Lebendorf_far tokens=54
|
||||
S06 bg=Hargenteen tokens=51
|
||||
S07 bg=Hargenteen tokens=53
|
||||
S08 bg=Barch tokens=59
|
||||
S09 bg=Acheron tokens=53
|
||||
S10 bg=Acheron tokens=53
|
||||
S11 bg=Anastasis tokens=53
|
||||
S12 bg=Hargenteen tokens=53
|
||||
S13 bg=Hargenteen tokens=56
|
||||
S14 bg=Earth tokens=59
|
||||
S15 bg=Earth tokens=53
|
||||
S16 bg=PD tokens=53
|
||||
S18 bg=Original tokens=43
|
||||
S19 bg=Original tokens=43
|
||||
S20 bg=Original tokens=43
|
||||
S21 bg=Original tokens=43
|
||||
S22 bg=Original tokens=50
|
||||
S23 bg=Original tokens=43
|
||||
S24 bg=Anastasis tokens=53
|
||||
S25 bg=Hargenteen tokens=53
|
||||
S26 bg=Hargenteen tokens=53
|
||||
S27 bg=Planet_Lebendorf tokens=53
|
||||
S28 bg=Lebendorf tokens=54
|
||||
S29 bg=Earth tokens=53
|
||||
Test bg=Earth tokens=43
|
||||
|
||||
=== 2. GP_CHALLENGE.pak contents ===
|
||||
151 entries, 0 IDXD objects
|
||||
|
||||
=== 3. tokens mentioning Challenge / EX across the main pak ===
|
||||
033b5b7e pgmsg_challenge1.t32
|
||||
033b5b7e pgmsg_challenge2.t32
|
||||
033b5b7e pgmsg_challenge3.t32
|
||||
033b5b7e pgmsg_challenge4.t32
|
||||
033b5b7e pgmsg_challenge5.t32
|
||||
033b5b7e pgmsg_challenge6.t32
|
||||
35b8dc67 UN_e001_ADAN_Elan_EX4
|
||||
35b8dc67 UN_e007_ADAN_Turret_EX4
|
||||
35b8dc67 UN_e008_ADAN_TurretPlus_EX4
|
||||
35b8dc67 UN_e010_ADAN_Attacker_S_EX4
|
||||
35b8dc67 UN_e011_ADAN_Attacker_B_EX4
|
||||
35b8dc67 UN_e107_ADAN_AAFrigate_EX4
|
||||
35b8dc67 UN_f001_TCAF_DeltaSaber_T_EX5
|
||||
35b8dc67 UN_f001_TCAF_DeltaSaber_T_EX5_el
|
||||
35b8dc67 UN_f003_TCAF_ArrowHead_EX4
|
||||
35b8dc67 UN_f003_TCAF_ArrowHead_EX5
|
||||
35b8dc67 UN_f104_TCAF_Battleship_EX5
|
||||
35b8dc67 UN_f105_TCAF_Cruiser_EX5
|
||||
3c5b0549 UN_e107_ADAN_AAFrigate_EX4
|
||||
3c5b0549 UN_f104_TCAF_Battleship_EX5
|
||||
3c5b0549 UN_f105_TCAF_Cruiser_EX5
|
||||
3c5b0549 UnitName_UN_e107_ADAN_AAFrigate_EX4
|
||||
3c5b0549 UnitName_UN_f104_TCAF_Battleship_EX5
|
||||
3c5b0549 UnitName_UN_f105_TCAF_Cruiser_EX5
|
||||
3c5b0549 Weapon_TCAF_Ship_AAGun_EX5
|
||||
3c9ae32e EnumWeapon_EX5.tbl
|
||||
43faa517 UN_e001_ADAN_Elan_EX4
|
||||
43faa517 UN_e007_ADAN_Turret_EX4
|
||||
43faa517 UN_e008_ADAN_TurretPlus_EX4
|
||||
43faa517 UN_e010_ADAN_Attacker_S_EX4
|
||||
43faa517 UN_e011_ADAN_Attacker_B_EX4
|
||||
43faa517 UN_f001_TCAF_DeltaSaber_T_EX5
|
||||
43faa517 UN_f001_TCAF_DeltaSaber_T_EX5_el
|
||||
43faa517 UN_f003_TCAF_ArrowHead_EX4
|
||||
43faa517 UN_f003_TCAF_ArrowHead_EX5
|
||||
43faa517 UnitName_UN_e001_ADAN_Elan_EX4
|
||||
43faa517 UnitName_UN_e007_ADAN_Turret_EX4
|
||||
43faa517 UnitName_UN_e008_ADAN_TurretPlus_EX4
|
||||
43faa517 UnitName_UN_e010_ADAN_Attacker_S_EX4
|
||||
43faa517 UnitName_UN_e011_ADAN_Attacker_B_EX4
|
||||
43faa517 UnitName_UN_f001_TCAF_DeltaSaber_T_EX5
|
||||
43faa517 UnitName_UN_f001_TCAF_DeltaSaber_T_EX5_el
|
||||
43faa517 UnitName_UN_f003_TCAF_ArrowHead_EX4
|
||||
43faa517 UnitName_UN_f003_TCAF_ArrowHead_EX5
|
||||
659aff47 UN_e001_ADAN_Elan_EX4
|
||||
659aff47 UN_e007_ADAN_Turret_EX4
|
||||
659aff47 UN_e008_ADAN_TurretPlus_EX4
|
||||
659aff47 UN_e010_ADAN_Attacker_S_EX4
|
||||
659aff47 UN_e011_ADAN_Attacker_B_EX4
|
||||
659aff47 UN_e107_ADAN_AAFrigate_EX4
|
||||
659aff47 UN_f003_TCAF_ArrowHead_EX4
|
||||
6ab4825a WeaponCannonName_TCAF_Ship_AAGun_EX5
|
||||
6ab4825a Weapon_TCAF_Ship_AAGun_EX5
|
||||
6b9b000d UN_f001_TCAF_DeltaSaber_T_EX5
|
||||
6b9b000d UN_f001_TCAF_DeltaSaber_T_EX5_el
|
||||
6b9b000d UN_f003_TCAF_ArrowHead_EX5
|
||||
6b9b000d UN_f104_TCAF_Battleship_EX5
|
||||
6b9b000d UN_f105_TCAF_Cruiser_EX5
|
||||
ffbc19c2 Weapon_TCAF_Ship_AAGun_EX5
|
||||
(59 distinct)
|
||||
750
docs/re/captures/challenge-screen-config.txt
Normal file
@@ -0,0 +1,750 @@
|
||||
|
||||
===== tables.pak entry #60 schema 54a10697 123 tokens [dat\GP_CHALLENGE.pak+fra\] =====
|
||||
0 xBASE_INFO
|
||||
1 0x061031
|
||||
2 VERSION
|
||||
3 dat\GP_CHALLENGE.pak+fra\
|
||||
4 PATH
|
||||
5 strings.tbl
|
||||
6 STRINGS
|
||||
7 py_menu_scr.prt
|
||||
8 MENU
|
||||
9 CHIPS
|
||||
10 py_menu_n_btn1.prt
|
||||
11 Button_TimeAttack
|
||||
12 py_menu_n_btn2.prt
|
||||
13 Button_ScoreAttack
|
||||
14 py_menu_n_btn3.prt
|
||||
15 Button_Extra01
|
||||
16 py_menu_n_btn4.prt
|
||||
17 Button_Extra02
|
||||
18 py_menu_n_btn5.prt
|
||||
19 Button_Extra03
|
||||
20 py_menu_n_btn6.prt
|
||||
21 Button_Extra04
|
||||
22 py_menu_b_btn1.prt
|
||||
23 Button_TimeAttack_Gray
|
||||
24 py_menu_b_btn2.prt
|
||||
25 Button_ScoreAttack_Gray
|
||||
26 py_menu_b_btn3.prt
|
||||
27 Button_Extra01_Gray
|
||||
28 py_menu_b_btn4.prt
|
||||
29 Button_Extra02_Gray
|
||||
30 py_menu_b_btn5.prt
|
||||
31 Button_Extra03_Gray
|
||||
32 py_menu_b_btn6.prt
|
||||
33 Button_Extra04_Gray
|
||||
34 py_menu_challenge1.t32
|
||||
35 Thumbnail_TimeAttack
|
||||
36 py_menu_challenge2.t32
|
||||
37 Thumbnail_ScoreAttack
|
||||
38 py_menu_challenge3.t32
|
||||
39 Thumbnail_Extra01
|
||||
40 py_menu_challenge4.t32
|
||||
41 Thumbnail_Extra02
|
||||
42 py_menu_challenge5.t32
|
||||
43 Thumbnail_Extra03
|
||||
44 py_menu_challenge6.t32
|
||||
45 Thumbnail_Extra04
|
||||
46 py_menu_str_time.t32
|
||||
47 Record_Time
|
||||
48 py_menu_str_point.t32
|
||||
49 Record_Point
|
||||
50 py_menu_str_p.t32
|
||||
51 Unit_Point
|
||||
52 py_menu_new.prt
|
||||
53 New_Stage
|
||||
54 FONTS
|
||||
55 Font_Desc
|
||||
56 Font_Record
|
||||
57 CONTM___.TTF
|
||||
58 FONT
|
||||
59 24
|
||||
60 HEIGHT
|
||||
61 28
|
||||
62 Desc_Stage
|
||||
63 580,400
|
||||
64 LOCATE
|
||||
65 8
|
||||
66 LINE_SPACE
|
||||
67 Desc_Time
|
||||
68 1048,608,R
|
||||
69 0
|
||||
70 INDEX
|
||||
71 Desc_Points
|
||||
72 1032,608,R
|
||||
73 UNIT
|
||||
74 New_Stage01
|
||||
75 New_Stage02
|
||||
76 0,60
|
||||
77 INDEX_ADJUST
|
||||
78 New_Stage03
|
||||
79 0,120
|
||||
80 New_Stage04
|
||||
81 0,180
|
||||
82 New_Stage05
|
||||
83 0,240
|
||||
84 New_Stage06
|
||||
85 0,300
|
||||
86 MISSIONS
|
||||
87 TimeAttack
|
||||
88 ScoreAttack
|
||||
89 Extra01
|
||||
90 Extra02
|
||||
91 Extra03
|
||||
92 Extra04
|
||||
93 MISSION_ID
|
||||
94 Time
|
||||
95 RECORD_TYPE
|
||||
96 16
|
||||
97 REQUIREMENT
|
||||
98 NORMAL_BUTTON
|
||||
99 GRAY_BUTTON
|
||||
100 THUMBNAIL
|
||||
101 TimeAttackRequirement
|
||||
102 REQUIREMENT_DESC
|
||||
103 TimeAttackStageDesc
|
||||
104 STAGE_DESC
|
||||
105 TEXT_STAGE
|
||||
106 TEXT_RECORD
|
||||
107 NEW_STAGE
|
||||
108 25
|
||||
109 Points
|
||||
110 ScoreAttackRequirement
|
||||
111 ScoreAttackStageDesc
|
||||
112 26
|
||||
113 Extra01Requirement
|
||||
114 ExtraStage01Desc
|
||||
115 27
|
||||
116 Extra02Requirement
|
||||
117 ExtraStage02Desc
|
||||
118 Extra03Requirement
|
||||
119 ExtraStage03Desc
|
||||
120 29
|
||||
121 Extra04Requirement
|
||||
122 ExtraStage04Desc
|
||||
|
||||
===== tables.pak entry #64 schema 54a10697 123 tokens [dat\GP_CHALLENGE.pak+eng\] =====
|
||||
0 xBASE_INFO
|
||||
1 0x061031
|
||||
2 VERSION
|
||||
3 dat\GP_CHALLENGE.pak+eng\
|
||||
4 PATH
|
||||
5 strings.tbl
|
||||
6 STRINGS
|
||||
7 py_menu_scr.prt
|
||||
8 MENU
|
||||
9 CHIPS
|
||||
10 py_menu_n_btn1.prt
|
||||
11 Button_TimeAttack
|
||||
12 py_menu_n_btn2.prt
|
||||
13 Button_ScoreAttack
|
||||
14 py_menu_n_btn3.prt
|
||||
15 Button_Extra01
|
||||
16 py_menu_n_btn4.prt
|
||||
17 Button_Extra02
|
||||
18 py_menu_n_btn5.prt
|
||||
19 Button_Extra03
|
||||
20 py_menu_n_btn6.prt
|
||||
21 Button_Extra04
|
||||
22 py_menu_b_btn1.prt
|
||||
23 Button_TimeAttack_Gray
|
||||
24 py_menu_b_btn2.prt
|
||||
25 Button_ScoreAttack_Gray
|
||||
26 py_menu_b_btn3.prt
|
||||
27 Button_Extra01_Gray
|
||||
28 py_menu_b_btn4.prt
|
||||
29 Button_Extra02_Gray
|
||||
30 py_menu_b_btn5.prt
|
||||
31 Button_Extra03_Gray
|
||||
32 py_menu_b_btn6.prt
|
||||
33 Button_Extra04_Gray
|
||||
34 py_menu_challenge1.t32
|
||||
35 Thumbnail_TimeAttack
|
||||
36 py_menu_challenge2.t32
|
||||
37 Thumbnail_ScoreAttack
|
||||
38 py_menu_challenge3.t32
|
||||
39 Thumbnail_Extra01
|
||||
40 py_menu_challenge4.t32
|
||||
41 Thumbnail_Extra02
|
||||
42 py_menu_challenge5.t32
|
||||
43 Thumbnail_Extra03
|
||||
44 py_menu_challenge6.t32
|
||||
45 Thumbnail_Extra04
|
||||
46 py_menu_str_time.t32
|
||||
47 Record_Time
|
||||
48 py_menu_str_point.t32
|
||||
49 Record_Point
|
||||
50 py_menu_str_p.t32
|
||||
51 Unit_Point
|
||||
52 py_menu_new.prt
|
||||
53 New_Stage
|
||||
54 FONTS
|
||||
55 Font_Desc
|
||||
56 Font_Record
|
||||
57 CONTM___.TTF
|
||||
58 FONT
|
||||
59 24
|
||||
60 HEIGHT
|
||||
61 28
|
||||
62 Desc_Stage
|
||||
63 580,400
|
||||
64 LOCATE
|
||||
65 8
|
||||
66 LINE_SPACE
|
||||
67 Desc_Time
|
||||
68 1048,608,R
|
||||
69 0
|
||||
70 INDEX
|
||||
71 Desc_Points
|
||||
72 1032,608,R
|
||||
73 UNIT
|
||||
74 New_Stage01
|
||||
75 New_Stage02
|
||||
76 0,60
|
||||
77 INDEX_ADJUST
|
||||
78 New_Stage03
|
||||
79 0,120
|
||||
80 New_Stage04
|
||||
81 0,180
|
||||
82 New_Stage05
|
||||
83 0,240
|
||||
84 New_Stage06
|
||||
85 0,300
|
||||
86 MISSIONS
|
||||
87 TimeAttack
|
||||
88 ScoreAttack
|
||||
89 Extra01
|
||||
90 Extra02
|
||||
91 Extra03
|
||||
92 Extra04
|
||||
93 MISSION_ID
|
||||
94 Time
|
||||
95 RECORD_TYPE
|
||||
96 16
|
||||
97 REQUIREMENT
|
||||
98 NORMAL_BUTTON
|
||||
99 GRAY_BUTTON
|
||||
100 THUMBNAIL
|
||||
101 TimeAttackRequirement
|
||||
102 REQUIREMENT_DESC
|
||||
103 TimeAttackStageDesc
|
||||
104 STAGE_DESC
|
||||
105 TEXT_STAGE
|
||||
106 TEXT_RECORD
|
||||
107 NEW_STAGE
|
||||
108 25
|
||||
109 Points
|
||||
110 ScoreAttackRequirement
|
||||
111 ScoreAttackStageDesc
|
||||
112 26
|
||||
113 Extra01Requirement
|
||||
114 ExtraStage01Desc
|
||||
115 27
|
||||
116 Extra02Requirement
|
||||
117 ExtraStage02Desc
|
||||
118 Extra03Requirement
|
||||
119 ExtraStage03Desc
|
||||
120 29
|
||||
121 Extra04Requirement
|
||||
122 ExtraStage04Desc
|
||||
|
||||
===== tables.pak entry #67 schema 54a10697 123 tokens [dat\GP_CHALLENGE.pak+deu\] =====
|
||||
0 xBASE_INFO
|
||||
1 0x061031
|
||||
2 VERSION
|
||||
3 dat\GP_CHALLENGE.pak+deu\
|
||||
4 PATH
|
||||
5 strings.tbl
|
||||
6 STRINGS
|
||||
7 py_menu_scr.prt
|
||||
8 MENU
|
||||
9 CHIPS
|
||||
10 py_menu_n_btn1.prt
|
||||
11 Button_TimeAttack
|
||||
12 py_menu_n_btn2.prt
|
||||
13 Button_ScoreAttack
|
||||
14 py_menu_n_btn3.prt
|
||||
15 Button_Extra01
|
||||
16 py_menu_n_btn4.prt
|
||||
17 Button_Extra02
|
||||
18 py_menu_n_btn5.prt
|
||||
19 Button_Extra03
|
||||
20 py_menu_n_btn6.prt
|
||||
21 Button_Extra04
|
||||
22 py_menu_b_btn1.prt
|
||||
23 Button_TimeAttack_Gray
|
||||
24 py_menu_b_btn2.prt
|
||||
25 Button_ScoreAttack_Gray
|
||||
26 py_menu_b_btn3.prt
|
||||
27 Button_Extra01_Gray
|
||||
28 py_menu_b_btn4.prt
|
||||
29 Button_Extra02_Gray
|
||||
30 py_menu_b_btn5.prt
|
||||
31 Button_Extra03_Gray
|
||||
32 py_menu_b_btn6.prt
|
||||
33 Button_Extra04_Gray
|
||||
34 py_menu_challenge1.t32
|
||||
35 Thumbnail_TimeAttack
|
||||
36 py_menu_challenge2.t32
|
||||
37 Thumbnail_ScoreAttack
|
||||
38 py_menu_challenge3.t32
|
||||
39 Thumbnail_Extra01
|
||||
40 py_menu_challenge4.t32
|
||||
41 Thumbnail_Extra02
|
||||
42 py_menu_challenge5.t32
|
||||
43 Thumbnail_Extra03
|
||||
44 py_menu_challenge6.t32
|
||||
45 Thumbnail_Extra04
|
||||
46 py_menu_str_time.t32
|
||||
47 Record_Time
|
||||
48 py_menu_str_point.t32
|
||||
49 Record_Point
|
||||
50 py_menu_str_p.t32
|
||||
51 Unit_Point
|
||||
52 py_menu_new.prt
|
||||
53 New_Stage
|
||||
54 FONTS
|
||||
55 Font_Desc
|
||||
56 Font_Record
|
||||
57 CONTM___.TTF
|
||||
58 FONT
|
||||
59 24
|
||||
60 HEIGHT
|
||||
61 28
|
||||
62 Desc_Stage
|
||||
63 580,400
|
||||
64 LOCATE
|
||||
65 8
|
||||
66 LINE_SPACE
|
||||
67 Desc_Time
|
||||
68 1048,608,R
|
||||
69 0
|
||||
70 INDEX
|
||||
71 Desc_Points
|
||||
72 1032,608,R
|
||||
73 UNIT
|
||||
74 New_Stage01
|
||||
75 New_Stage02
|
||||
76 0,60
|
||||
77 INDEX_ADJUST
|
||||
78 New_Stage03
|
||||
79 0,120
|
||||
80 New_Stage04
|
||||
81 0,180
|
||||
82 New_Stage05
|
||||
83 0,240
|
||||
84 New_Stage06
|
||||
85 0,300
|
||||
86 MISSIONS
|
||||
87 TimeAttack
|
||||
88 ScoreAttack
|
||||
89 Extra01
|
||||
90 Extra02
|
||||
91 Extra03
|
||||
92 Extra04
|
||||
93 MISSION_ID
|
||||
94 Time
|
||||
95 RECORD_TYPE
|
||||
96 16
|
||||
97 REQUIREMENT
|
||||
98 NORMAL_BUTTON
|
||||
99 GRAY_BUTTON
|
||||
100 THUMBNAIL
|
||||
101 TimeAttackRequirement
|
||||
102 REQUIREMENT_DESC
|
||||
103 TimeAttackStageDesc
|
||||
104 STAGE_DESC
|
||||
105 TEXT_STAGE
|
||||
106 TEXT_RECORD
|
||||
107 NEW_STAGE
|
||||
108 25
|
||||
109 Points
|
||||
110 ScoreAttackRequirement
|
||||
111 ScoreAttackStageDesc
|
||||
112 26
|
||||
113 Extra01Requirement
|
||||
114 ExtraStage01Desc
|
||||
115 27
|
||||
116 Extra02Requirement
|
||||
117 ExtraStage02Desc
|
||||
118 Extra03Requirement
|
||||
119 ExtraStage03Desc
|
||||
120 29
|
||||
121 Extra04Requirement
|
||||
122 ExtraStage04Desc
|
||||
|
||||
===== tables.pak entry #68 schema 54a10697 123 tokens [dat\GP_CHALLENGE.pak+ita\] =====
|
||||
0 xBASE_INFO
|
||||
1 0x061031
|
||||
2 VERSION
|
||||
3 dat\GP_CHALLENGE.pak+ita\
|
||||
4 PATH
|
||||
5 strings.tbl
|
||||
6 STRINGS
|
||||
7 py_menu_scr.prt
|
||||
8 MENU
|
||||
9 CHIPS
|
||||
10 py_menu_n_btn1.prt
|
||||
11 Button_TimeAttack
|
||||
12 py_menu_n_btn2.prt
|
||||
13 Button_ScoreAttack
|
||||
14 py_menu_n_btn3.prt
|
||||
15 Button_Extra01
|
||||
16 py_menu_n_btn4.prt
|
||||
17 Button_Extra02
|
||||
18 py_menu_n_btn5.prt
|
||||
19 Button_Extra03
|
||||
20 py_menu_n_btn6.prt
|
||||
21 Button_Extra04
|
||||
22 py_menu_b_btn1.prt
|
||||
23 Button_TimeAttack_Gray
|
||||
24 py_menu_b_btn2.prt
|
||||
25 Button_ScoreAttack_Gray
|
||||
26 py_menu_b_btn3.prt
|
||||
27 Button_Extra01_Gray
|
||||
28 py_menu_b_btn4.prt
|
||||
29 Button_Extra02_Gray
|
||||
30 py_menu_b_btn5.prt
|
||||
31 Button_Extra03_Gray
|
||||
32 py_menu_b_btn6.prt
|
||||
33 Button_Extra04_Gray
|
||||
34 py_menu_challenge1.t32
|
||||
35 Thumbnail_TimeAttack
|
||||
36 py_menu_challenge2.t32
|
||||
37 Thumbnail_ScoreAttack
|
||||
38 py_menu_challenge3.t32
|
||||
39 Thumbnail_Extra01
|
||||
40 py_menu_challenge4.t32
|
||||
41 Thumbnail_Extra02
|
||||
42 py_menu_challenge5.t32
|
||||
43 Thumbnail_Extra03
|
||||
44 py_menu_challenge6.t32
|
||||
45 Thumbnail_Extra04
|
||||
46 py_menu_str_time.t32
|
||||
47 Record_Time
|
||||
48 py_menu_str_point.t32
|
||||
49 Record_Point
|
||||
50 py_menu_str_p.t32
|
||||
51 Unit_Point
|
||||
52 py_menu_new.prt
|
||||
53 New_Stage
|
||||
54 FONTS
|
||||
55 Font_Desc
|
||||
56 Font_Record
|
||||
57 CONTM___.TTF
|
||||
58 FONT
|
||||
59 24
|
||||
60 HEIGHT
|
||||
61 28
|
||||
62 Desc_Stage
|
||||
63 580,400
|
||||
64 LOCATE
|
||||
65 8
|
||||
66 LINE_SPACE
|
||||
67 Desc_Time
|
||||
68 1048,608,R
|
||||
69 0
|
||||
70 INDEX
|
||||
71 Desc_Points
|
||||
72 1032,608,R
|
||||
73 UNIT
|
||||
74 New_Stage01
|
||||
75 New_Stage02
|
||||
76 0,60
|
||||
77 INDEX_ADJUST
|
||||
78 New_Stage03
|
||||
79 0,120
|
||||
80 New_Stage04
|
||||
81 0,180
|
||||
82 New_Stage05
|
||||
83 0,240
|
||||
84 New_Stage06
|
||||
85 0,300
|
||||
86 MISSIONS
|
||||
87 TimeAttack
|
||||
88 ScoreAttack
|
||||
89 Extra01
|
||||
90 Extra02
|
||||
91 Extra03
|
||||
92 Extra04
|
||||
93 MISSION_ID
|
||||
94 Time
|
||||
95 RECORD_TYPE
|
||||
96 16
|
||||
97 REQUIREMENT
|
||||
98 NORMAL_BUTTON
|
||||
99 GRAY_BUTTON
|
||||
100 THUMBNAIL
|
||||
101 TimeAttackRequirement
|
||||
102 REQUIREMENT_DESC
|
||||
103 TimeAttackStageDesc
|
||||
104 STAGE_DESC
|
||||
105 TEXT_STAGE
|
||||
106 TEXT_RECORD
|
||||
107 NEW_STAGE
|
||||
108 25
|
||||
109 Points
|
||||
110 ScoreAttackRequirement
|
||||
111 ScoreAttackStageDesc
|
||||
112 26
|
||||
113 Extra01Requirement
|
||||
114 ExtraStage01Desc
|
||||
115 27
|
||||
116 Extra02Requirement
|
||||
117 ExtraStage02Desc
|
||||
118 Extra03Requirement
|
||||
119 ExtraStage03Desc
|
||||
120 29
|
||||
121 Extra04Requirement
|
||||
122 ExtraStage04Desc
|
||||
|
||||
===== tables.pak entry #74 schema 54a10697 123 tokens [dat\GP_CHALLENGE.pak+jpn\] =====
|
||||
0 BASE_INFO
|
||||
1 0x061031
|
||||
2 VERSION
|
||||
3 dat\GP_CHALLENGE.pak+jpn\
|
||||
4 PATH
|
||||
5 strings.tbl
|
||||
6 STRINGS
|
||||
7 py_menu_scr.prt
|
||||
8 MENU
|
||||
9 CHIPS
|
||||
10 py_menu_n_btn1.prt
|
||||
11 Button_TimeAttack
|
||||
12 py_menu_n_btn2.prt
|
||||
13 Button_ScoreAttack
|
||||
14 py_menu_n_btn3.prt
|
||||
15 Button_Extra01
|
||||
16 py_menu_n_btn4.prt
|
||||
17 Button_Extra02
|
||||
18 py_menu_n_btn5.prt
|
||||
19 Button_Extra03
|
||||
20 py_menu_n_btn6.prt
|
||||
21 Button_Extra04
|
||||
22 py_menu_b_btn1.prt
|
||||
23 Button_TimeAttack_Gray
|
||||
24 py_menu_b_btn2.prt
|
||||
25 Button_ScoreAttack_Gray
|
||||
26 py_menu_b_btn3.prt
|
||||
27 Button_Extra01_Gray
|
||||
28 py_menu_b_btn4.prt
|
||||
29 Button_Extra02_Gray
|
||||
30 py_menu_b_btn5.prt
|
||||
31 Button_Extra03_Gray
|
||||
32 py_menu_b_btn6.prt
|
||||
33 Button_Extra04_Gray
|
||||
34 py_menu_challenge1.t32
|
||||
35 Thumbnail_TimeAttack
|
||||
36 py_menu_challenge2.t32
|
||||
37 Thumbnail_ScoreAttack
|
||||
38 py_menu_challenge3.t32
|
||||
39 Thumbnail_Extra01
|
||||
40 py_menu_challenge4.t32
|
||||
41 Thumbnail_Extra02
|
||||
42 py_menu_challenge5.t32
|
||||
43 Thumbnail_Extra03
|
||||
44 py_menu_challenge6.t32
|
||||
45 Thumbnail_Extra04
|
||||
46 py_menu_str_time.t32
|
||||
47 Record_Time
|
||||
48 py_menu_str_point.t32
|
||||
49 Record_Point
|
||||
50 py_menu_str_p.t32
|
||||
51 Unit_Point
|
||||
52 py_menu_new.prt
|
||||
53 New_Stage
|
||||
54 FONTS
|
||||
55 Font_Desc
|
||||
56 Font_Record
|
||||
57 DFSOGE5.TTC
|
||||
58 FONT
|
||||
59 24
|
||||
60 HEIGHT
|
||||
61 CONTM___.TTF
|
||||
62 28
|
||||
63 Desc_Stage
|
||||
64 580,400
|
||||
65 LOCATE
|
||||
66 8
|
||||
67 LINE_SPACE
|
||||
68 Desc_Time
|
||||
69 1048,608,R
|
||||
70 INDEX
|
||||
71 Desc_Points
|
||||
72 1032,608,R
|
||||
73 UNIT
|
||||
74 New_Stage01
|
||||
75 New_Stage02
|
||||
76 0,60
|
||||
77 INDEX_ADJUST
|
||||
78 New_Stage03
|
||||
79 0,120
|
||||
80 New_Stage04
|
||||
81 0,180
|
||||
82 New_Stage05
|
||||
83 0,240
|
||||
84 New_Stage06
|
||||
85 0,300
|
||||
86 MISSIONS
|
||||
87 TimeAttack
|
||||
88 ScoreAttack
|
||||
89 Extra01
|
||||
90 Extra02
|
||||
91 Extra03
|
||||
92 Extra04
|
||||
93 MISSION_ID
|
||||
94 Time
|
||||
95 RECORD_TYPE
|
||||
96 16
|
||||
97 REQUIREMENT
|
||||
98 NORMAL_BUTTON
|
||||
99 GRAY_BUTTON
|
||||
100 THUMBNAIL
|
||||
101 TimeAttackRequirement
|
||||
102 REQUIREMENT_DESC
|
||||
103 TimeAttackStageDesc
|
||||
104 STAGE_DESC
|
||||
105 TEXT_STAGE
|
||||
106 TEXT_RECORD
|
||||
107 NEW_STAGE
|
||||
108 25
|
||||
109 Points
|
||||
110 ScoreAttackRequirement
|
||||
111 ScoreAttackStageDesc
|
||||
112 26
|
||||
113 Extra01Requirement
|
||||
114 ExtraStage01Desc
|
||||
115 27
|
||||
116 Extra02Requirement
|
||||
117 ExtraStage02Desc
|
||||
118 Extra03Requirement
|
||||
119 ExtraStage03Desc
|
||||
120 29
|
||||
121 Extra04Requirement
|
||||
122 ExtraStage04Desc
|
||||
|
||||
===== tables.pak entry #75 schema 54a10697 123 tokens [dat\GP_CHALLENGE.pak+esp\] =====
|
||||
0 xBASE_INFO
|
||||
1 0x061031
|
||||
2 VERSION
|
||||
3 dat\GP_CHALLENGE.pak+esp\
|
||||
4 PATH
|
||||
5 strings.tbl
|
||||
6 STRINGS
|
||||
7 py_menu_scr.prt
|
||||
8 MENU
|
||||
9 CHIPS
|
||||
10 py_menu_n_btn1.prt
|
||||
11 Button_TimeAttack
|
||||
12 py_menu_n_btn2.prt
|
||||
13 Button_ScoreAttack
|
||||
14 py_menu_n_btn3.prt
|
||||
15 Button_Extra01
|
||||
16 py_menu_n_btn4.prt
|
||||
17 Button_Extra02
|
||||
18 py_menu_n_btn5.prt
|
||||
19 Button_Extra03
|
||||
20 py_menu_n_btn6.prt
|
||||
21 Button_Extra04
|
||||
22 py_menu_b_btn1.prt
|
||||
23 Button_TimeAttack_Gray
|
||||
24 py_menu_b_btn2.prt
|
||||
25 Button_ScoreAttack_Gray
|
||||
26 py_menu_b_btn3.prt
|
||||
27 Button_Extra01_Gray
|
||||
28 py_menu_b_btn4.prt
|
||||
29 Button_Extra02_Gray
|
||||
30 py_menu_b_btn5.prt
|
||||
31 Button_Extra03_Gray
|
||||
32 py_menu_b_btn6.prt
|
||||
33 Button_Extra04_Gray
|
||||
34 py_menu_challenge1.t32
|
||||
35 Thumbnail_TimeAttack
|
||||
36 py_menu_challenge2.t32
|
||||
37 Thumbnail_ScoreAttack
|
||||
38 py_menu_challenge3.t32
|
||||
39 Thumbnail_Extra01
|
||||
40 py_menu_challenge4.t32
|
||||
41 Thumbnail_Extra02
|
||||
42 py_menu_challenge5.t32
|
||||
43 Thumbnail_Extra03
|
||||
44 py_menu_challenge6.t32
|
||||
45 Thumbnail_Extra04
|
||||
46 py_menu_str_time.t32
|
||||
47 Record_Time
|
||||
48 py_menu_str_point.t32
|
||||
49 Record_Point
|
||||
50 py_menu_str_p.t32
|
||||
51 Unit_Point
|
||||
52 py_menu_new.prt
|
||||
53 New_Stage
|
||||
54 FONTS
|
||||
55 Font_Desc
|
||||
56 Font_Record
|
||||
57 CONTM___.TTF
|
||||
58 FONT
|
||||
59 24
|
||||
60 HEIGHT
|
||||
61 28
|
||||
62 Desc_Stage
|
||||
63 580,400
|
||||
64 LOCATE
|
||||
65 8
|
||||
66 LINE_SPACE
|
||||
67 Desc_Time
|
||||
68 1048,608,R
|
||||
69 0
|
||||
70 INDEX
|
||||
71 Desc_Points
|
||||
72 1032,608,R
|
||||
73 UNIT
|
||||
74 New_Stage01
|
||||
75 New_Stage02
|
||||
76 0,60
|
||||
77 INDEX_ADJUST
|
||||
78 New_Stage03
|
||||
79 0,120
|
||||
80 New_Stage04
|
||||
81 0,180
|
||||
82 New_Stage05
|
||||
83 0,240
|
||||
84 New_Stage06
|
||||
85 0,300
|
||||
86 MISSIONS
|
||||
87 TimeAttack
|
||||
88 ScoreAttack
|
||||
89 Extra01
|
||||
90 Extra02
|
||||
91 Extra03
|
||||
92 Extra04
|
||||
93 MISSION_ID
|
||||
94 Time
|
||||
95 RECORD_TYPE
|
||||
96 16
|
||||
97 REQUIREMENT
|
||||
98 NORMAL_BUTTON
|
||||
99 GRAY_BUTTON
|
||||
100 THUMBNAIL
|
||||
101 TimeAttackRequirement
|
||||
102 REQUIREMENT_DESC
|
||||
103 TimeAttackStageDesc
|
||||
104 STAGE_DESC
|
||||
105 TEXT_STAGE
|
||||
106 TEXT_RECORD
|
||||
107 NEW_STAGE
|
||||
108 25
|
||||
109 Points
|
||||
110 ScoreAttackRequirement
|
||||
111 ScoreAttackStageDesc
|
||||
112 26
|
||||
113 Extra01Requirement
|
||||
114 ExtraStage01Desc
|
||||
115 27
|
||||
116 Extra02Requirement
|
||||
117 ExtraStage02Desc
|
||||
118 Extra03Requirement
|
||||
119 ExtraStage03Desc
|
||||
120 29
|
||||
121 Extra04Requirement
|
||||
122 ExtraStage04Desc
|
||||
BIN
docs/re/captures/filepad-flight-stage01.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
docs/re/captures/filepad-mainmenu.png
Normal file
|
After Width: | Height: | Size: 938 KiB |
BIN
docs/re/captures/mission-select-all-story-unlocked.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
docs/re/captures/mission-select-ends-at-stage16.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
docs/re/captures/mission-select-stage01-only.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
7
docs/re/captures/pitch-burst-final.csv
Normal file
@@ -0,0 +1,7 @@
|
||||
throttle,rep,settled_speed,burst_speed,rate_deg_per_wall_s
|
||||
min,0,126.5,98.1,100.93
|
||||
min,1,130.0,115.5,118.43
|
||||
cruise,0,414.3,395.5,101.48
|
||||
cruise,1,462.7,381.0,107.22
|
||||
max,0,1463.5,1330.5,56.83
|
||||
max,1,1496.4,1382.8,61.68
|
||||
|
7
docs/re/captures/pitch-burst-settled-speeds.csv
Normal file
@@ -0,0 +1,7 @@
|
||||
throttle,rep,settled_speed,burst_speed,rate_deg_per_wall_s
|
||||
min,0,121.3,102.6,113.64
|
||||
min,1,152.0,108.8,109.46
|
||||
cruise,0,428.5,374.7,100.19
|
||||
cruise,1,361.6,391.4,88.39
|
||||
max,0,1564.4,1352.0,52.22
|
||||
max,1,1249.3,1614.6,70.52
|
||||
|
40
docs/re/captures/pitch-rate-curve-clean.csv
Normal file
@@ -0,0 +1,40 @@
|
||||
t,speed_units_per_wall_s,rate_deg_per_wall_s
|
||||
0.52,1498.8,37.47
|
||||
1.03,1276.1,91.64
|
||||
1.53,1045.5,90.49
|
||||
2.05,1133.7,118.83
|
||||
2.55,885.4,129.88
|
||||
3.06,777.7,127.51
|
||||
3.57,788.1,174.38
|
||||
4.07,546.7,157.56
|
||||
4.57,536.2,124.41
|
||||
5.09,946.8,161.62
|
||||
5.59,821.9,138.76
|
||||
6.09,636.7,131.35
|
||||
6.61,529.3,133.56
|
||||
7.13,674.7,162.01
|
||||
7.63,669.2,125.78
|
||||
8.17,746.4,127.35
|
||||
8.67,821.1,154.31
|
||||
9.17,569.7,135.57
|
||||
9.67,645.2,167.18
|
||||
10.19,720.0,150.99
|
||||
10.69,698.6,123.63
|
||||
11.21,717.0,125.53
|
||||
11.72,826.9,165.47
|
||||
12.23,471.3,114.2
|
||||
12.77,594.0,146.56
|
||||
13.29,858.3,174.73
|
||||
13.79,734.8,130.83
|
||||
14.31,502.8,91.53
|
||||
14.81,822.9,168.67
|
||||
15.31,696.2,166.37
|
||||
15.83,462.2,109.81
|
||||
16.33,791.0,162.87
|
||||
16.85,854.4,155.88
|
||||
17.37,536.8,98.26
|
||||
17.89,873.1,182.24
|
||||
18.41,630.0,155.61
|
||||
18.93,584.1,129.64
|
||||
19.43,699.6,132.45
|
||||
19.97,805.1,142.68
|
||||
|
315
docs/re/captures/pitch-rate-speed-bleed.csv
Normal file
@@ -0,0 +1,315 @@
|
||||
phase,t,dpitch_deg
|
||||
slow,0.051,0.0953
|
||||
slow,0.101,1.6885
|
||||
slow,0.152,0.8717
|
||||
slow,0.202,1.6238
|
||||
slow,0.252,-0.0
|
||||
slow,0.302,-0.0
|
||||
slow,0.352,8.8412
|
||||
slow,0.403,5.679
|
||||
slow,0.453,5.3839
|
||||
slow,0.503,12.9544
|
||||
slow,0.553,7.5713
|
||||
slow,0.603,4.0593
|
||||
slow,0.653,0.0
|
||||
slow,0.703,0.0
|
||||
slow,0.753,23.7081
|
||||
slow,0.804,14.501
|
||||
slow,0.854,9.6502
|
||||
slow,0.905,9.64
|
||||
slow,0.955,2.4036
|
||||
slow,1.006,-0.0
|
||||
slow,1.056,-0.0
|
||||
slow,1.106,24.1631
|
||||
slow,1.156,9.7201
|
||||
slow,1.206,14.5817
|
||||
slow,1.26,7.2861
|
||||
slow,1.31,-0.0
|
||||
slow,1.36,-0.0
|
||||
slow,1.41,21.8962
|
||||
slow,1.461,12.1865
|
||||
slow,1.511,14.6395
|
||||
slow,1.561,4.8866
|
||||
slow,1.611,4.8766
|
||||
slow,1.661,0.0
|
||||
slow,1.712,0.0
|
||||
slow,1.762,24.4862
|
||||
slow,1.812,12.272
|
||||
slow,1.863,14.7764
|
||||
slow,1.913,14.8237
|
||||
slow,1.964,9.9081
|
||||
slow,2.014,-0.0
|
||||
slow,2.064,-0.0
|
||||
slow,2.116,22.3434
|
||||
slow,2.166,4.9684
|
||||
slow,2.217,14.8905
|
||||
slow,2.267,9.9066
|
||||
slow,2.317,2.4656
|
||||
slow,2.367,-0.0
|
||||
slow,2.417,19.7888
|
||||
slow,2.467,7.374
|
||||
slow,2.517,9.8264
|
||||
slow,2.569,14.7106
|
||||
slow,2.619,12.2195
|
||||
slow,2.671,0.0
|
||||
slow,2.721,0.0
|
||||
slow,2.771,0.0
|
||||
slow,2.821,24.4044
|
||||
slow,2.871,12.1781
|
||||
slow,2.951,14.5889
|
||||
slow,3.001,14.5786
|
||||
slow,3.052,0.0
|
||||
slow,3.102,0.0
|
||||
slow,3.153,19.432
|
||||
slow,3.203,14.5741
|
||||
slow,3.253,4.8605
|
||||
slow,3.303,19.4596
|
||||
slow,3.355,2.4281
|
||||
slow,3.405,0.0
|
||||
slow,3.455,0.0
|
||||
slow,3.506,31.6949
|
||||
slow,3.556,9.7829
|
||||
slow,3.606,14.7094
|
||||
slow,3.656,9.8314
|
||||
slow,3.706,7.3808
|
||||
slow,3.756,-0.0
|
||||
slow,3.807,-0.0
|
||||
slow,3.857,19.7355
|
||||
slow,3.907,14.8675
|
||||
slow,3.958,4.9655
|
||||
slow,4.008,17.3918
|
||||
slow,4.06,2.4777
|
||||
slow,4.11,-0.0
|
||||
slow,4.16,-0.0
|
||||
slow,4.211,32.264
|
||||
slow,4.261,9.866
|
||||
slow,4.311,19.6734
|
||||
slow,4.361,7.3673
|
||||
slow,4.412,7.3404
|
||||
slow,4.462,0.0
|
||||
slow,4.513,0.0
|
||||
slow,4.563,19.5894
|
||||
slow,4.613,12.1841
|
||||
slow,4.663,4.8719
|
||||
slow,4.713,17.0264
|
||||
slow,4.765,2.4241
|
||||
slow,4.815,0.0
|
||||
slow,4.866,0.0
|
||||
slow,4.918,26.7496
|
||||
slow,4.968,17.0367
|
||||
slow,5.019,14.6052
|
||||
slow,5.069,7.3143
|
||||
slow,5.119,2.4296
|
||||
slow,5.17,0.0
|
||||
slow,5.222,17.0719
|
||||
slow,5.272,12.2148
|
||||
slow,5.351,14.6765
|
||||
slow,5.401,14.7096
|
||||
slow,5.454,2.4482
|
||||
slow,5.504,0.0
|
||||
slow,5.555,0.0
|
||||
slow,5.605,27.0453
|
||||
slow,5.655,12.3214
|
||||
slow,5.705,14.8308
|
||||
slow,5.756,4.9518
|
||||
slow,5.806,19.8347
|
||||
slow,5.856,2.4736
|
||||
slow,5.906,-0.0
|
||||
slow,5.957,-0.0
|
||||
slow,6.008,24.7941
|
||||
slow,6.058,9.8906
|
||||
slow,6.108,14.805
|
||||
slow,6.159,4.9105
|
||||
slow,6.209,-0.0
|
||||
slow,6.26,-0.0
|
||||
slow,6.31,24.6254
|
||||
slow,6.36,7.3425
|
||||
slow,6.41,14.6791
|
||||
slow,6.461,9.7716
|
||||
slow,6.511,7.3144
|
||||
slow,6.561,0.0
|
||||
slow,6.612,0.0
|
||||
slow,6.662,21.9754
|
||||
slow,6.713,12.1647
|
||||
slow,6.763,9.7355
|
||||
slow,6.813,19.4718
|
||||
slow,6.864,2.4274
|
||||
slow,6.914,0.0
|
||||
slow,6.964,0.0
|
||||
slow,7.014,29.2378
|
||||
slow,7.064,9.7533
|
||||
slow,7.114,14.6513
|
||||
slow,7.165,4.8902
|
||||
slow,7.215,2.4395
|
||||
slow,7.266,0.0
|
||||
slow,7.316,0.0
|
||||
slow,7.368,26.9512
|
||||
slow,7.418,14.7524
|
||||
slow,7.468,9.8579
|
||||
slow,7.518,12.3414
|
||||
slow,7.569,2.4662
|
||||
slow,7.621,-0.0
|
||||
slow,7.671,-0.0
|
||||
slow,7.722,19.8018
|
||||
slow,7.772,9.92
|
||||
slow,7.851,14.8814
|
||||
slow,7.901,12.3787
|
||||
slow,7.951,2.4669
|
||||
slow,8.001,0.0
|
||||
fast,0.05,0.0
|
||||
fast,0.104,0.0
|
||||
fast,0.154,0.0
|
||||
fast,0.204,0.2374
|
||||
fast,0.254,0.206
|
||||
fast,0.306,1.8902
|
||||
fast,0.356,1.5885
|
||||
fast,0.406,1.4595
|
||||
fast,0.456,0.0
|
||||
fast,0.506,0.0
|
||||
fast,0.556,5.7624
|
||||
fast,0.606,2.347
|
||||
fast,0.657,3.5323
|
||||
fast,0.707,6.1276
|
||||
fast,0.757,1.1012
|
||||
fast,0.807,-0.0
|
||||
fast,0.857,-0.0
|
||||
fast,0.909,12.418
|
||||
fast,0.959,5.1346
|
||||
fast,1.009,7.8218
|
||||
fast,1.059,6.5806
|
||||
fast,1.109,1.3424
|
||||
fast,1.16,0.0
|
||||
fast,1.21,10.761
|
||||
fast,1.263,4.2102
|
||||
fast,1.313,8.2445
|
||||
fast,1.363,6.8621
|
||||
fast,1.413,6.9892
|
||||
fast,1.463,-0.0
|
||||
fast,1.542,-0.0
|
||||
fast,1.592,11.0635
|
||||
fast,1.642,5.9249
|
||||
fast,1.692,8.7078
|
||||
fast,1.742,2.8665
|
||||
fast,1.793,10.1297
|
||||
fast,1.843,0.0
|
||||
fast,1.893,0.0
|
||||
fast,1.943,0.0
|
||||
fast,1.993,20.6893
|
||||
fast,2.043,3.2503
|
||||
fast,2.094,11.2506
|
||||
fast,2.144,6.3385
|
||||
fast,2.194,7.8452
|
||||
fast,2.244,1.556
|
||||
fast,2.295,-0.0
|
||||
fast,2.345,-0.0
|
||||
fast,2.395,18.7454
|
||||
fast,2.445,10.3395
|
||||
fast,2.495,10.608
|
||||
fast,2.545,7.0808
|
||||
fast,2.596,7.0815
|
||||
fast,2.646,0.0
|
||||
fast,2.696,0.0
|
||||
fast,2.746,0.0
|
||||
fast,2.796,17.7627
|
||||
fast,2.847,10.6635
|
||||
fast,2.897,7.1571
|
||||
fast,2.947,7.1983
|
||||
fast,3.002,5.4229
|
||||
fast,3.052,0.0
|
||||
fast,3.102,0.0
|
||||
fast,3.152,0.0
|
||||
fast,3.202,25.5105
|
||||
fast,3.253,7.3926
|
||||
fast,3.303,11.2085
|
||||
fast,3.353,3.7679
|
||||
fast,3.404,3.7764
|
||||
fast,3.455,-0.0
|
||||
fast,3.505,-0.0
|
||||
fast,3.556,19.0406
|
||||
fast,3.642,11.5779
|
||||
fast,3.692,11.712
|
||||
fast,3.742,3.9222
|
||||
fast,3.792,0.0
|
||||
fast,3.842,0.0
|
||||
fast,3.892,17.7661
|
||||
fast,3.942,1.9679
|
||||
fast,3.993,17.8611
|
||||
fast,4.043,3.9876
|
||||
fast,4.093,15.9815
|
||||
fast,4.146,0.0
|
||||
fast,4.196,0.0
|
||||
fast,4.246,0.0
|
||||
fast,4.296,21.9362
|
||||
fast,4.346,7.908
|
||||
fast,4.396,11.8184
|
||||
fast,4.447,7.8388
|
||||
fast,4.497,1.9466
|
||||
fast,4.547,0.0
|
||||
fast,4.597,0.0
|
||||
fast,4.647,0.0
|
||||
fast,4.697,29.0258
|
||||
fast,4.747,11.3566
|
||||
fast,4.797,11.275
|
||||
fast,4.847,3.7393
|
||||
fast,4.898,7.4494
|
||||
fast,4.948,-0.0
|
||||
fast,4.998,-0.0
|
||||
fast,5.048,-0.0
|
||||
fast,5.098,24.0113
|
||||
fast,5.148,10.888
|
||||
fast,5.202,10.8585
|
||||
fast,5.252,7.225
|
||||
fast,5.302,5.4068
|
||||
fast,5.352,0.0
|
||||
fast,5.403,0.0
|
||||
fast,5.453,0.0
|
||||
fast,5.504,26.97
|
||||
fast,5.554,10.7617
|
||||
fast,5.604,10.8137
|
||||
fast,5.654,3.6172
|
||||
fast,5.709,5.4336
|
||||
fast,5.759,-0.0
|
||||
fast,5.809,-0.0
|
||||
fast,5.862,20.0109
|
||||
fast,5.942,10.985
|
||||
fast,5.992,11.0845
|
||||
fast,6.042,5.5755
|
||||
fast,6.092,0.0
|
||||
fast,6.142,0.0
|
||||
fast,6.192,18.6832
|
||||
fast,6.245,1.866
|
||||
fast,6.295,15.0963
|
||||
fast,6.345,3.8107
|
||||
fast,6.395,9.5834
|
||||
fast,6.446,1.9237
|
||||
fast,6.496,0.0
|
||||
fast,6.546,0.0
|
||||
fast,6.598,21.2979
|
||||
fast,6.651,7.8108
|
||||
fast,6.701,13.7645
|
||||
fast,6.751,3.9499
|
||||
fast,6.802,5.9406
|
||||
fast,6.852,0.0
|
||||
fast,6.902,15.8604
|
||||
fast,6.952,5.9146
|
||||
fast,7.002,11.8545
|
||||
fast,7.053,7.9034
|
||||
fast,7.103,9.8434
|
||||
fast,7.153,1.9608
|
||||
fast,7.203,-0.0
|
||||
fast,7.253,-0.0
|
||||
fast,7.303,19.6693
|
||||
fast,7.357,13.4968
|
||||
fast,7.407,11.5239
|
||||
fast,7.457,7.6491
|
||||
fast,7.507,11.4147
|
||||
fast,7.559,1.8882
|
||||
fast,7.642,-0.0
|
||||
fast,7.692,15.147
|
||||
fast,7.742,5.584
|
||||
fast,7.792,11.1374
|
||||
fast,7.842,3.6992
|
||||
fast,7.892,9.2126
|
||||
fast,7.946,1.8328
|
||||
fast,7.996,-0.0
|
||||
fast,8.046,-0.0
|
||||
|
353
docs/re/captures/rate-curve-aliased-BAD.csv
Normal file
@@ -0,0 +1,353 @@
|
||||
t,speed_units_per_wall_s,rate_deg_per_wall_s
|
||||
0.05,0.0,0.0
|
||||
0.101,4682.4,0.0
|
||||
0.154,1865.9,8.21
|
||||
0.205,1915.4,22.53
|
||||
0.273,1722.2,32.72
|
||||
0.323,2309.7,64.27
|
||||
0.374,0.0,0.0
|
||||
0.426,0.0,0.0
|
||||
0.476,0.0,0.0
|
||||
0.526,3781.4,166.09
|
||||
0.576,1133.2,56.22
|
||||
0.626,2228.8,125.61
|
||||
0.676,735.8,46.29
|
||||
0.726,2161.2,150.9
|
||||
0.777,0.0,0.0
|
||||
0.827,0.0,0.0
|
||||
0.877,0.0,0.0
|
||||
0.927,4795.2,363.58
|
||||
0.978,1400.3,105.58
|
||||
1.028,2080.9,159.73
|
||||
1.078,688.0,53.58
|
||||
1.129,1018.2,79.65
|
||||
1.179,0.0,0.0
|
||||
1.229,0.0,0.0
|
||||
1.279,3169.4,272.63
|
||||
1.332,1891.3,159.33
|
||||
1.382,656.8,55.51
|
||||
1.432,1958.3,167.87
|
||||
1.485,311.4,26.7
|
||||
1.535,0.0,0.0
|
||||
1.585,0.0,0.0
|
||||
1.637,2189.9,217.26
|
||||
1.687,1470.3,152.38
|
||||
1.737,1832.8,178.53
|
||||
1.788,611.6,57.44
|
||||
1.838,311.7,29.03
|
||||
1.889,0.0,0.0
|
||||
1.939,0.0,0.0
|
||||
1.989,2568.6,290.03
|
||||
2.039,1303.4,156.28
|
||||
2.09,1571.3,188.96
|
||||
2.173,987.0,114.66
|
||||
2.223,1389.3,157.19
|
||||
2.274,0.0,0.0
|
||||
2.324,0.0,0.0
|
||||
2.374,0.0,0.0
|
||||
2.425,3014.5,437.25
|
||||
2.476,851.8,137.78
|
||||
2.526,1720.0,282.99
|
||||
2.576,855.8,142.04
|
||||
2.626,1272.7,213.71
|
||||
2.676,420.4,71.25
|
||||
2.726,0.0,0.0
|
||||
2.776,0.0,0.0
|
||||
2.827,2337.7,393.95
|
||||
2.879,605.4,103.05
|
||||
2.929,1229.4,216.06
|
||||
2.979,798.9,145.37
|
||||
3.029,1356.7,257.13
|
||||
3.081,183.1,35.66
|
||||
3.131,0.0,0.0
|
||||
3.181,0.0,0.0
|
||||
3.231,1858.0,372.22
|
||||
3.281,541.0,112.82
|
||||
3.332,1372.1,301.88
|
||||
3.382,335.4,76.89
|
||||
3.432,658.3,154.84
|
||||
3.482,162.0,38.8
|
||||
3.536,0.0,0.0
|
||||
3.586,0.0,0.0
|
||||
3.636,1591.8,390.72
|
||||
3.686,311.3,78.69
|
||||
3.736,1215.5,316.75
|
||||
3.786,602.4,159.01
|
||||
3.839,429.8,113.96
|
||||
3.889,0.0,0.0
|
||||
3.939,0.0,0.0
|
||||
3.989,0.0,0.0
|
||||
4.04,1835.6,469.81
|
||||
4.09,311.3,78.92
|
||||
4.14,1251.2,315.27
|
||||
4.19,317.9,78.47
|
||||
4.24,320.5,78.42
|
||||
4.29,0.0,0.0
|
||||
4.341,0.0,0.0
|
||||
4.391,1352.5,313.43
|
||||
4.441,850.5,192.03
|
||||
4.492,1033.9,227.09
|
||||
4.542,1073.0,227.87
|
||||
4.592,551.1,113.43
|
||||
4.642,0.0,0.0
|
||||
4.692,0.0,0.0
|
||||
4.743,1735.5,337.79
|
||||
4.793,779.7,147.59
|
||||
4.843,1186.4,220.17
|
||||
4.893,808.4,146.17
|
||||
4.943,1018.0,181.41
|
||||
4.993,204.4,36.08
|
||||
5.043,0.0,0.0
|
||||
5.093,0.0,0.0
|
||||
5.144,0.0,0.0
|
||||
5.194,3438.2,575.96
|
||||
5.244,853.2,142.26
|
||||
5.294,1685.8,285.28
|
||||
5.344,1250.1,215.19
|
||||
5.394,825.0,144.01
|
||||
5.445,202.2,35.56
|
||||
5.495,0.0,0.0
|
||||
5.545,0.0,0.0
|
||||
5.596,2042.5,361.76
|
||||
5.674,762.2,138.77
|
||||
5.725,1171.7,219.95
|
||||
5.775,384.3,73.55
|
||||
5.826,0.0,0.0
|
||||
5.877,0.0,0.0
|
||||
5.927,0.0,0.0
|
||||
5.977,1528.0,296.2
|
||||
6.028,1281.9,256.26
|
||||
6.078,365.6,75.03
|
||||
6.128,1073.3,226.52
|
||||
6.178,176.0,37.88
|
||||
6.231,0.0,0.0
|
||||
6.281,0.0,0.0
|
||||
6.332,1720.1,372.76
|
||||
6.382,518.3,114.37
|
||||
6.432,1009.5,230.82
|
||||
6.484,480.0,112.96
|
||||
6.534,807.9,194.56
|
||||
6.584,159.3,38.98
|
||||
6.675,0.0,0.0
|
||||
6.725,1449.1,352.93
|
||||
6.775,477.0,117.26
|
||||
6.826,927.2,232.38
|
||||
6.877,306.8,77.6
|
||||
6.927,928.5,236.59
|
||||
6.977,154.5,39.37
|
||||
7.027,0.0,0.0
|
||||
7.077,0.0,0.0
|
||||
7.131,1353.2,333.31
|
||||
7.181,963.9,234.69
|
||||
7.231,1304.5,311.86
|
||||
7.281,332.6,77.62
|
||||
7.331,506.1,116.36
|
||||
7.381,0.0,0.0
|
||||
7.432,0.0,0.0
|
||||
7.482,0.0,0.0
|
||||
7.534,2615.1,556.47
|
||||
7.584,1101.8,225.72
|
||||
7.634,1108.3,222.17
|
||||
7.686,929.5,182.14
|
||||
7.736,576.3,111.11
|
||||
7.786,0.0,0.0
|
||||
7.836,0.0,0.0
|
||||
7.886,0.0,0.0
|
||||
7.936,2840.4,515.81
|
||||
7.987,807.7,144.56
|
||||
8.037,1214.6,217.75
|
||||
8.087,809.7,145.24
|
||||
8.137,606.3,108.89
|
||||
8.187,0.0,0.0
|
||||
8.237,0.0,0.0
|
||||
8.288,0.0,0.0
|
||||
8.339,2628.6,463.46
|
||||
8.389,811.4,144.9
|
||||
8.439,1198.5,218.35
|
||||
8.489,392.4,72.53
|
||||
8.539,784.5,146.63
|
||||
8.59,0.0,0.0
|
||||
8.64,0.0,0.0
|
||||
8.69,0.0,0.0
|
||||
8.741,2111.3,400.11
|
||||
8.791,946.6,185.46
|
||||
8.841,1110.6,224.19
|
||||
8.891,364.4,75.15
|
||||
8.941,720.3,150.86
|
||||
8.991,0.0,0.0
|
||||
9.042,0.0,0.0
|
||||
9.092,1792.7,378.77
|
||||
9.142,529.6,113.67
|
||||
9.193,1022.7,226.47
|
||||
9.273,624.6,143.36
|
||||
9.325,644.6,151.82
|
||||
9.375,0.0,0.0
|
||||
9.425,0.0,0.0
|
||||
9.476,1289.1,307.07
|
||||
9.526,1122.5,273.41
|
||||
9.577,311.7,76.8
|
||||
9.627,789.7,196.14
|
||||
9.677,156.4,38.99
|
||||
9.727,0.0,0.0
|
||||
9.778,0.0,0.0
|
||||
9.828,2131.2,510.02
|
||||
9.878,653.9,155.64
|
||||
9.928,983.1,233.48
|
||||
9.978,330.0,77.72
|
||||
10.028,496.6,116.32
|
||||
10.079,0.0,0.0
|
||||
10.129,0.0,0.0
|
||||
10.179,0.0,0.0
|
||||
10.23,2470.2,540.62
|
||||
10.282,672.7,144.51
|
||||
10.333,1067.2,227.85
|
||||
10.383,1076.8,227.17
|
||||
10.433,724.0,150.8
|
||||
10.483,0.0,0.0
|
||||
10.533,0.0,0.0
|
||||
10.583,0.0,0.0
|
||||
10.633,2272.0,451.3
|
||||
10.683,759.7,148.67
|
||||
10.734,762.5,148.52
|
||||
10.784,756.3,146.57
|
||||
10.834,770.1,148.16
|
||||
10.885,0.0,0.0
|
||||
10.935,0.0,0.0
|
||||
10.985,0.0,0.0
|
||||
11.035,2168.6,406.08
|
||||
11.085,984.6,183.61
|
||||
11.136,1178.3,220.07
|
||||
11.186,590.5,110.24
|
||||
11.236,393.0,73.44
|
||||
11.286,0.0,0.0
|
||||
11.336,0.0,0.0
|
||||
11.386,2007.6,367.45
|
||||
11.436,597.4,109.35
|
||||
11.486,1180.4,219.81
|
||||
11.537,1160.8,220.86
|
||||
11.587,575.3,111.02
|
||||
11.639,368.0,71.56
|
||||
11.689,0.0,0.0
|
||||
11.739,0.0,0.0
|
||||
11.789,2275.4,445.88
|
||||
11.879,415.2,83.5
|
||||
11.929,1448.9,300.69
|
||||
11.979,356.5,75.48
|
||||
12.031,0.0,0.0
|
||||
12.081,0.0,0.0
|
||||
12.131,1612.8,341.73
|
||||
12.184,504.1,108.0
|
||||
12.234,1042.0,228.85
|
||||
12.284,683.1,153.38
|
||||
12.334,674.3,153.83
|
||||
12.386,0.0,0.0
|
||||
12.473,0.0,0.0
|
||||
12.524,1701.7,386.01
|
||||
12.574,672.1,154.19
|
||||
12.625,974.9,227.26
|
||||
12.675,330.4,77.54
|
||||
12.725,986.1,232.99
|
||||
12.775,163.8,38.73
|
||||
12.825,0.0,0.0
|
||||
12.878,0.0,0.0
|
||||
12.928,0.0,0.0
|
||||
12.978,2405.7,542.85
|
||||
13.029,1022.4,230.5
|
||||
13.079,681.6,153.81
|
||||
13.129,1354.2,305.37
|
||||
13.179,343.9,76.77
|
||||
13.229,692.5,153.38
|
||||
13.28,0.0,0.0
|
||||
13.33,0.0,0.0
|
||||
13.38,0.0,0.0
|
||||
13.43,2362.5,496.01
|
||||
13.483,693.6,143.63
|
||||
13.533,1466.7,300.97
|
||||
13.583,555.0,112.79
|
||||
13.633,184.3,37.4
|
||||
13.683,0.0,0.0
|
||||
13.735,0.0,0.0
|
||||
13.785,1917.4,375.17
|
||||
13.838,542.8,105.29
|
||||
13.888,1144.2,222.78
|
||||
13.938,1513.8,297.05
|
||||
13.988,568.4,111.69
|
||||
14.038,378.0,74.42
|
||||
14.088,0.0,0.0
|
||||
14.139,0.0,0.0
|
||||
14.189,1933.6,372.51
|
||||
14.239,955.5,184.91
|
||||
14.289,756.9,148.61
|
||||
14.374,662.1,132.25
|
||||
14.424,554.7,112.16
|
||||
14.474,0.0,0.0
|
||||
14.524,0.0,0.0
|
||||
14.577,1603.7,323.84
|
||||
14.627,1276.0,263.01
|
||||
14.677,542.3,113.23
|
||||
14.727,714.5,150.87
|
||||
14.777,354.7,75.59
|
||||
14.83,0.0,0.0
|
||||
14.88,0.0,0.0
|
||||
14.93,1795.9,379.83
|
||||
14.98,355.3,75.74
|
||||
15.031,1386.0,301.97
|
||||
15.083,330.9,73.04
|
||||
15.133,688.3,153.24
|
||||
15.183,170.9,38.24
|
||||
15.234,0.0,0.0
|
||||
15.284,0.0,0.0
|
||||
15.334,0.0,0.0
|
||||
15.384,2304.1,499.06
|
||||
15.435,1027.8,225.69
|
||||
15.485,1024.9,230.47
|
||||
15.536,1000.9,229.21
|
||||
15.586,664.6,153.64
|
||||
15.636,665.2,154.54
|
||||
15.687,0.0,0.0
|
||||
15.737,0.0,0.0
|
||||
15.788,0.0,0.0
|
||||
15.84,2131.8,478.6
|
||||
15.89,856.1,192.06
|
||||
15.941,1025.4,230.39
|
||||
15.991,342.4,76.74
|
||||
16.043,663.2,148.34
|
||||
16.093,0.0,0.0
|
||||
16.143,0.0,0.0
|
||||
16.193,0.0,0.0
|
||||
16.246,2555.4,545.64
|
||||
16.296,895.5,189.56
|
||||
16.373,694.2,146.49
|
||||
16.424,1443.5,302.27
|
||||
16.476,0.0,0.0
|
||||
16.526,0.0,0.0
|
||||
16.577,0.0,0.0
|
||||
16.627,2074.2,414.87
|
||||
16.677,751.3,149.12
|
||||
16.727,1123.6,223.97
|
||||
16.777,374.5,74.66
|
||||
16.827,1122.8,223.93
|
||||
16.88,0.0,0.0
|
||||
16.93,0.0,0.0
|
||||
16.98,0.0,0.0
|
||||
17.03,2296.0,447.62
|
||||
17.08,379.9,74.23
|
||||
17.13,1503.9,297.32
|
||||
17.18,563.4,111.98
|
||||
17.233,538.0,107.34
|
||||
17.283,0.0,0.0
|
||||
17.333,0.0,0.0
|
||||
17.383,1890.3,373.65
|
||||
17.433,1119.5,223.93
|
||||
17.485,361.3,72.88
|
||||
17.535,1651.2,337.72
|
||||
17.585,365.1,75.24
|
||||
17.635,547.2,113.12
|
||||
17.686,0.0,0.0
|
||||
17.736,0.0,0.0
|
||||
17.786,0.0,0.0
|
||||
17.839,2490.6,503.11
|
||||
17.892,1033.4,213.33
|
||||
17.942,1073.2,226.83
|
||||
17.995,670.1,143.94
|
||||
18.045,1223.2,266.8
|
||||
|
40
docs/re/captures/rate-curve-windowed.csv
Normal file
@@ -0,0 +1,40 @@
|
||||
t,speed_units_per_wall_s,rate_deg_per_wall_s
|
||||
0.52,766.4,15.56
|
||||
1.06,875.3,81.57
|
||||
1.56,319.7,79.47
|
||||
2.08,589.5,124.0
|
||||
2.6,833.9,118.23
|
||||
3.1,846.4,144.01
|
||||
3.62,682.9,147.83
|
||||
4.12,442.3,114.32
|
||||
4.62,782.3,183.76
|
||||
5.13,869.9,160.12
|
||||
5.66,691.9,117.79
|
||||
6.18,770.1,146.5
|
||||
6.68,659.7,149.99
|
||||
7.19,546.7,135.85
|
||||
7.7,760.5,163.7
|
||||
8.2,645.7,121.72
|
||||
8.72,884.3,159.2
|
||||
9.26,546.3,107.3
|
||||
9.78,633.0,139.66
|
||||
10.28,716.1,169.56
|
||||
10.8,607.5,129.02
|
||||
11.32,866.0,156.23
|
||||
11.82,677.8,119.63
|
||||
12.32,537.0,103.57
|
||||
12.82,821.2,187.87
|
||||
13.34,545.6,133.44
|
||||
13.86,608.8,128.22
|
||||
14.38,778.1,141.08
|
||||
14.88,543.6,93.59
|
||||
15.38,935.1,176.36
|
||||
15.9,708.2,159.77
|
||||
16.4,439.7,105.71
|
||||
16.9,847.5,185.58
|
||||
17.42,584.5,112.34
|
||||
17.94,874.4,158.62
|
||||
18.44,733.0,149.31
|
||||
18.96,527.8,128.4
|
||||
19.46,642.2,156.88
|
||||
19.98,737.8,149.45
|
||||
|
BIN
docs/re/captures/ready-room-extra-mode.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
313
docs/re/captures/roll-about-forward-axis.csv
Normal file
@@ -0,0 +1,313 @@
|
||||
phase,t,droll_deg
|
||||
slow,0.05,0.0
|
||||
slow,0.101,-0.0909
|
||||
slow,0.155,-1.4615
|
||||
slow,0.206,-7.3152
|
||||
slow,0.256,-2.7442
|
||||
slow,0.307,-8.4132
|
||||
slow,0.357,0.0
|
||||
slow,0.407,0.0
|
||||
slow,0.457,0.0
|
||||
slow,0.507,-38.7821
|
||||
slow,0.557,-13.0283
|
||||
slow,0.609,-19.4952
|
||||
slow,0.659,-12.9643
|
||||
slow,0.711,-3.2265
|
||||
slow,0.764,-0.0
|
||||
slow,0.814,-0.0
|
||||
slow,0.865,-38.9513
|
||||
slow,0.915,-13.0541
|
||||
slow,0.965,-13.0281
|
||||
slow,1.015,-25.9821
|
||||
slow,1.066,-12.9527
|
||||
slow,1.117,0.0
|
||||
slow,1.167,0.0
|
||||
slow,1.217,-25.9507
|
||||
slow,1.268,-9.7894
|
||||
slow,1.355,-26.0652
|
||||
slow,1.405,-19.4799
|
||||
slow,1.456,-9.7073
|
||||
slow,1.506,0.0
|
||||
slow,1.557,0.0
|
||||
slow,1.607,-29.1521
|
||||
slow,1.661,-9.7987
|
||||
slow,1.711,-19.5758
|
||||
slow,1.761,-6.5117
|
||||
slow,1.811,-16.2414
|
||||
slow,1.861,-3.2347
|
||||
slow,1.911,0.0
|
||||
slow,1.961,0.0
|
||||
slow,2.012,-29.2256
|
||||
slow,2.062,-13.0531
|
||||
slow,2.112,-19.532
|
||||
slow,2.162,-12.9885
|
||||
slow,2.212,-9.7139
|
||||
slow,2.263,-0.0
|
||||
slow,2.313,-0.0
|
||||
slow,2.363,-0.0
|
||||
slow,2.413,-42.2278
|
||||
slow,2.464,-19.6123
|
||||
slow,2.514,-13.0415
|
||||
slow,2.564,-13.0156
|
||||
slow,2.614,-3.2392
|
||||
slow,2.664,0.0
|
||||
slow,2.714,0.0
|
||||
slow,2.766,-35.7889
|
||||
slow,2.816,-13.0315
|
||||
slow,2.867,-6.5058
|
||||
slow,2.917,-25.9631
|
||||
slow,2.967,-3.2272
|
||||
slow,3.017,0.0
|
||||
slow,3.067,0.0
|
||||
slow,3.117,-25.9064
|
||||
slow,3.168,-9.7872
|
||||
slow,3.218,-22.8201
|
||||
slow,3.268,-9.762
|
||||
slow,3.318,-19.4592
|
||||
slow,3.368,-3.2268
|
||||
slow,3.456,0.0
|
||||
slow,3.511,-29.1619
|
||||
slow,3.561,-6.5294
|
||||
slow,3.612,-19.5509
|
||||
slow,3.662,-6.5035
|
||||
slow,3.713,-22.7155
|
||||
slow,3.763,-0.0
|
||||
slow,3.813,-0.0
|
||||
slow,3.863,-0.0
|
||||
slow,3.913,-35.7343
|
||||
slow,3.963,-13.0477
|
||||
slow,4.013,-22.7862
|
||||
slow,4.064,-6.4918
|
||||
slow,4.116,-9.7153
|
||||
slow,4.167,-0.0
|
||||
slow,4.217,-0.0
|
||||
slow,4.267,-32.469
|
||||
slow,4.317,-19.576
|
||||
slow,4.368,-6.5118
|
||||
slow,4.418,-19.4983
|
||||
slow,4.469,-3.2333
|
||||
slow,4.519,0.0
|
||||
slow,4.57,0.0
|
||||
slow,4.62,-29.208
|
||||
slow,4.671,-9.8048
|
||||
slow,4.721,-13.0645
|
||||
slow,4.773,-6.5222
|
||||
slow,4.823,-26.0281
|
||||
slow,4.873,-3.2352
|
||||
slow,4.923,0.0
|
||||
slow,4.974,0.0
|
||||
slow,5.024,-29.2291
|
||||
slow,5.074,-13.0522
|
||||
slow,5.125,-13.0262
|
||||
slow,5.175,-16.259
|
||||
slow,5.225,-12.9694
|
||||
slow,5.275,-0.0
|
||||
slow,5.325,-0.0
|
||||
slow,5.376,-0.0
|
||||
slow,5.426,-38.9864
|
||||
slow,5.476,-9.8137
|
||||
slow,5.527,-19.5613
|
||||
slow,5.577,-13.0078
|
||||
slow,5.632,-6.4941
|
||||
slow,5.682,-0.0
|
||||
slow,5.755,-0.0
|
||||
slow,5.805,-39.0388
|
||||
slow,5.855,-13.0591
|
||||
slow,5.905,-26.0433
|
||||
slow,5.956,-6.4938
|
||||
slow,6.006,-12.9688
|
||||
slow,6.056,0.0
|
||||
slow,6.106,0.0
|
||||
slow,6.156,-29.1717
|
||||
slow,6.206,-16.3415
|
||||
slow,6.257,-13.0519
|
||||
slow,6.309,-19.5302
|
||||
slow,6.359,-12.9873
|
||||
slow,6.412,-6.4839
|
||||
slow,6.463,0.0
|
||||
slow,6.513,0.0
|
||||
slow,6.563,-32.4511
|
||||
slow,6.613,-22.8937
|
||||
slow,6.665,-13.0382
|
||||
slow,6.716,-22.7698
|
||||
slow,6.766,-12.9684
|
||||
slow,6.817,-6.4745
|
||||
slow,6.867,-0.0
|
||||
slow,6.918,-0.0
|
||||
slow,6.968,-32.4432
|
||||
slow,7.018,-16.2949
|
||||
slow,7.069,-13.0149
|
||||
slow,7.119,-22.7295
|
||||
slow,7.17,-16.1918
|
||||
slow,7.221,-3.2315
|
||||
slow,7.271,0.0
|
||||
slow,7.321,0.0
|
||||
slow,7.371,-38.9877
|
||||
slow,7.421,-9.8138
|
||||
slow,7.474,-22.8806
|
||||
slow,7.555,-19.5384
|
||||
slow,7.605,-16.2268
|
||||
slow,7.655,0.0
|
||||
slow,7.705,0.0
|
||||
slow,7.756,0.0
|
||||
slow,7.806,-39.0224
|
||||
slow,7.856,-13.064
|
||||
slow,7.907,-19.5481
|
||||
slow,7.957,-6.5026
|
||||
slow,8.007,-9.7314
|
||||
fast,0.05,-0.0596
|
||||
fast,0.1,-0.3593
|
||||
fast,0.15,-2.5234
|
||||
fast,0.202,-4.9514
|
||||
fast,0.252,-3.1468
|
||||
fast,0.302,-0.0
|
||||
fast,0.352,-0.0
|
||||
fast,0.403,-0.0
|
||||
fast,0.469,-20.1363
|
||||
fast,0.519,-8.0449
|
||||
fast,0.569,-8.6891
|
||||
fast,0.619,-10.948
|
||||
fast,0.67,-2.1745
|
||||
fast,0.72,-0.0
|
||||
fast,0.77,-0.0
|
||||
fast,0.821,-23.8257
|
||||
fast,0.872,-4.363
|
||||
fast,0.922,-15.4009
|
||||
fast,0.972,-4.4215
|
||||
fast,1.023,-17.711
|
||||
fast,1.073,0.0
|
||||
fast,1.123,0.0
|
||||
fast,1.173,0.0
|
||||
fast,1.223,-26.3637
|
||||
fast,1.275,-4.4318
|
||||
fast,1.325,-15.5068
|
||||
fast,1.375,-4.4229
|
||||
fast,1.426,-11.048
|
||||
fast,1.476,0.0
|
||||
fast,1.527,0.0
|
||||
fast,1.577,0.0
|
||||
fast,1.627,-24.1538
|
||||
fast,1.677,-8.8455
|
||||
fast,1.727,-15.5045
|
||||
fast,1.778,-8.8441
|
||||
fast,1.83,-8.8553
|
||||
fast,1.88,-0.0
|
||||
fast,1.93,-0.0
|
||||
fast,1.98,-0.0
|
||||
fast,2.034,-28.7329
|
||||
fast,2.084,-11.0611
|
||||
fast,2.134,-13.2881
|
||||
fast,2.184,-8.8429
|
||||
fast,2.236,-2.2041
|
||||
fast,2.286,-0.0
|
||||
fast,2.336,-0.0
|
||||
fast,2.387,-21.9271
|
||||
fast,2.437,-6.6459
|
||||
fast,2.488,-11.0875
|
||||
fast,2.569,-13.2631
|
||||
fast,2.619,-8.8432
|
||||
fast,2.669,-0.0
|
||||
fast,2.719,-0.0
|
||||
fast,2.769,-17.7007
|
||||
fast,2.82,-11.1022
|
||||
fast,2.87,-4.4224
|
||||
fast,2.92,-13.265
|
||||
fast,2.97,-4.3887
|
||||
fast,3.02,0.0
|
||||
fast,3.07,0.0
|
||||
fast,3.12,0.0
|
||||
fast,3.176,-19.5844
|
||||
fast,3.226,-17.5675
|
||||
fast,3.277,-4.4215
|
||||
fast,3.327,-19.9265
|
||||
fast,3.377,-2.2046
|
||||
fast,3.427,0.0
|
||||
fast,3.478,0.0
|
||||
fast,3.528,-19.7653
|
||||
fast,3.578,-4.4075
|
||||
fast,3.628,-15.5094
|
||||
fast,3.678,-8.8442
|
||||
fast,3.729,-11.0476
|
||||
fast,3.78,0.0
|
||||
fast,3.83,0.0
|
||||
fast,3.88,0.0
|
||||
fast,3.93,-26.3791
|
||||
fast,3.981,-8.8468
|
||||
fast,4.031,-13.2966
|
||||
fast,4.081,-4.4213
|
||||
fast,4.133,-4.3888
|
||||
fast,4.183,0.0
|
||||
fast,4.233,0.0
|
||||
fast,4.283,-19.5663
|
||||
fast,4.333,-13.2564
|
||||
fast,4.383,-4.4215
|
||||
fast,4.433,-17.6873
|
||||
fast,4.488,-2.2041
|
||||
fast,4.569,-0.0
|
||||
fast,4.619,-0.0
|
||||
fast,4.67,-17.544
|
||||
fast,4.72,-8.8326
|
||||
fast,4.77,-4.4305
|
||||
fast,4.82,-17.6862
|
||||
fast,4.872,-2.2041
|
||||
fast,4.922,-0.0
|
||||
fast,4.972,-0.0
|
||||
fast,5.023,-19.7606
|
||||
fast,5.073,-0.0
|
||||
fast,5.124,-17.6727
|
||||
fast,5.174,-4.4215
|
||||
fast,5.224,-11.0475
|
||||
fast,5.274,0.0
|
||||
fast,5.325,0.0
|
||||
fast,5.375,0.0
|
||||
fast,5.425,-19.7606
|
||||
fast,5.475,-8.8292
|
||||
fast,5.529,-19.9262
|
||||
fast,5.579,-4.4214
|
||||
fast,5.629,-4.4216
|
||||
fast,5.68,0.0
|
||||
fast,5.73,0.0
|
||||
fast,5.78,-19.937
|
||||
fast,5.87,-15.529
|
||||
fast,5.92,-13.2634
|
||||
fast,5.971,-6.6258
|
||||
fast,6.021,0.0
|
||||
fast,6.071,0.0
|
||||
fast,6.121,0.0
|
||||
fast,6.174,-19.7607
|
||||
fast,6.224,-17.6728
|
||||
fast,6.274,-4.4215
|
||||
fast,6.324,-13.2651
|
||||
fast,6.375,-4.3887
|
||||
fast,6.425,0.0
|
||||
fast,6.475,0.0
|
||||
fast,6.526,-23.9369
|
||||
fast,6.576,-4.383
|
||||
fast,6.626,-13.2366
|
||||
fast,6.676,-8.8431
|
||||
fast,6.727,-8.8552
|
||||
fast,6.777,-0.0
|
||||
fast,6.827,-0.0
|
||||
fast,6.877,-0.0
|
||||
fast,6.93,-22.0832
|
||||
fast,6.98,-8.8599
|
||||
fast,7.03,-19.929
|
||||
fast,7.08,-4.4214
|
||||
fast,7.132,-8.8553
|
||||
fast,7.182,-0.0
|
||||
fast,7.232,-0.0
|
||||
fast,7.285,-19.8506
|
||||
fast,7.336,-8.8927
|
||||
fast,7.386,-4.424
|
||||
fast,7.436,-17.687
|
||||
fast,7.486,-8.7844
|
||||
fast,7.537,-0.0
|
||||
fast,7.589,-0.0
|
||||
fast,7.639,-15.4299
|
||||
fast,7.689,-0.0
|
||||
fast,7.739,-13.3118
|
||||
fast,7.79,-8.8378
|
||||
fast,7.84,-15.4965
|
||||
fast,7.89,0.0
|
||||
fast,7.969,0.0
|
||||
fast,8.019,0.0
|
||||
|
7
docs/re/captures/roll-burst-final.csv
Normal file
@@ -0,0 +1,7 @@
|
||||
throttle,rep,settled_speed,burst_speed,rate_deg_per_wall_s
|
||||
min,0,103.5,99.3,165.34
|
||||
min,1,123.8,91.9,149.72
|
||||
cruise,0,434.7,352.0,149.9
|
||||
cruise,1,423.1,382.5,150.07
|
||||
max,0,1230.6,1258.2,102.76
|
||||
max,1,1624.8,1319.1,105.38
|
||||
|
23
docs/re/captures/roll-ramp-inconclusive.csv
Normal file
@@ -0,0 +1,23 @@
|
||||
rep,t,speed,rate_deg_per_wall_s
|
||||
0,0.27,390.1,15.5
|
||||
0,0.54,233.9,58.9
|
||||
0,0.81,573.5,325.0
|
||||
0,1.07,341.0,209.1
|
||||
0,1.34,222.8,130.3
|
||||
0,1.61,525.5,321.8
|
||||
0,1.87,386.6,237.6
|
||||
0,2.14,190.9,110.4
|
||||
0,2.41,511.3,310.2
|
||||
0,2.66,246.7,151.1
|
||||
0,2.94,312.3,183.0
|
||||
1,0.27,446.6,51.6
|
||||
1,0.54,372.5,181.1
|
||||
1,0.8,377.7,231.0
|
||||
1,1.07,417.7,246.3
|
||||
1,1.32,385.0,236.1
|
||||
1,1.59,232.9,169.4
|
||||
1,1.85,590.3,368.1
|
||||
1,2.11,307.0,194.7
|
||||
1,2.38,311.9,180.7
|
||||
1,2.64,418.6,255.2
|
||||
1,2.9,306.5,181.9
|
||||
|
@@ -3023,6 +3023,150 @@ UN_e004_ADAN_ElanPlus_NF,TurnAway_Time_Minimum,0x1b0,f32,0.5,defaulted-on-disc,l
|
||||
UN_e004_ADAN_ElanPlus_NF,Turn_AngularVelocity,0x1ac,f32,1.0471975803375244,disc,verified
|
||||
UN_e004_ADAN_ElanPlus_NF,UsingChaffRatio,0x234,f32,0.0,defaulted-on-disc,layout-derived
|
||||
UN_e004_ADAN_ElanPlus_NF,YawDragFactor,0x104,f32,3.0,disc,verified
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,AA_AxisMode_Max,0x134,f32,0.0,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,AA_AxisMode_Min,0x138,f32,0.0,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,AA_PitchMinus_Max,0x0c8,f32,0.1745329350233078,disc,verified
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,AA_PitchMinus_Min,0x0cc,f32,0.6981317400932312,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,AA_PitchPlus_Max,0x0b8,f32,0.5235987901687622,disc,verified
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,AA_PitchPlus_Min,0x0bc,f32,1.7453293800354004,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,AA_Roll_Max,0x0e8,f32,1.0471975803375244,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,AA_Roll_Min,0x0ec,f32,2.6179940700531006,disc,verified
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,AA_Yaw_Max,0x0d8,f32,0.8726646900177002,disc,verified
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,AA_Yaw_Min,0x0dc,f32,4.188790321350098,disc,verified
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,AB_AA_PitchMinus,0x158,f32,0.0,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,AB_AA_PitchPlus,0x150,f32,0.0,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,AB_AA_Roll,0x168,f32,0.0,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,AB_AA_Yaw,0x160,f32,0.0,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,AB_AV_PitchMinus,0x154,f32,0.0,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,AB_AV_PitchPlus,0x14c,f32,0.0,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,AB_AV_Roll,0x164,f32,0.0,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,AB_AV_Yaw,0x15c,f32,0.0,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,AB_ConsumeShield,0x148,f32,0.0,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,AB_ConsumeShield_Begin,0x144,f32,0.0,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,AV_AxisMode_Max,0x12c,f32,0.0,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,AV_AxisMode_Min,0x130,f32,0.0,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,AV_PitchMinus_Max,0x0c0,f32,0.6981317400932312,disc,verified
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,AV_PitchMinus_Min,0x0c4,f32,2.094395160675049,disc,verified
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,AV_PitchPlus_Max,0x0b0,f32,1.3962634801864624,disc,verified
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,AV_PitchPlus_Min,0x0b4,f32,3.490658760070801,disc,verified
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,AV_Roll_Max,0x0e0,f32,2.094395160675049,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,AV_Roll_Min,0x0e4,f32,2.792526960372925,disc,verified
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,AV_Yaw_Max,0x0d0,f32,0.4363323450088501,disc,verified
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,AV_Yaw_Min,0x0d4,f32,1.0471975803375244,disc,verified
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,AccPitchFactor,0x124,f32,30.0,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,Acceleration,0x0a8,f32,600.0,disc,verified
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,ArterBurner_Acc,0x11c,f32,2.0,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,ArterBurner_Vc,0x114,f32,2.0,disc,verified
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,AttackCraftPoint,0x2b8,f32,0.800000011920929,disc,verified
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,AttackVesselPoint,0x2b4,f32,0.0010000000474974513,disc,verified
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,BarrelRoll,0x178,word,1,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,BarrelRoll_CountMaximum,0x180,f32,5.0,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,BarrelRoll_CountMinimum,0x17c,f32,0.5,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,BarrelRoll_Radius,0x188,f32,250.0,disc,verified
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,BarrelRoll_Time,0x184,f32,1.2000000476837158,disc,verified
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,BoostAway,0x1b8,word,1,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,BoostAway_Time_Maximum,0x1c0,f32,4.0,disc,verified
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,BoostAway_Time_Minimum,0x1bc,f32,1.0,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,ChargeDelay,0x23c,f32,3.0,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,ChargeDelay_Break,0x240,f32,10.0,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,ChargeSpeed,0x244,f32,25.0,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,Color_B,0x048,f32,0.10000000149011612,disc,verified
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,Color_G,0x044,f32,0.5,disc,verified
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,Color_R,0x040,f32,1.0,disc,verified
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,CruisingVelocity,0x0a4,f32,700.0,disc,verified
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,CutoffTimeMax,0x19c,f32,5.0,disc,verified
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,CutoffTimeMin,0x198,f32,0.6000000238418579,disc,verified
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,DecPitchFactor,0x128,f32,30.0,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,Deceleration,0x0ac,f32,400.0,disc,verified
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,DefencePoint,0x2bc,f32,0.0002500000118743628,disc,verified
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,Delay,0x248,f32,0.0,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,DelayAdjustment,0x24c,f32,0.0,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,DestroyMotionTime,0x270,f32,5.0,disc,verified
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,DragFactorThreshold,0x110,f32,0.5,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,DryMass,0x274,f32,100.0,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,ExplosionSE,0x284,word,601,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,FCSRange,0x2a4,f32,2500.0,disc,verified
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,FiringRange,0x2a8,f32,0.0,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,GrossMass,0x278,f32,200.0,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,HP,0x054,f32,3200.0,disc,verified
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,HQRatio,0x058,f32,1.0,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,HoldPosition,0x1c4,word,1,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,HoldPosition_BackRatio,0x1dc,f32,0.5,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,HoldPosition_CancelTime,0x1e4,f32,20.0,disc,verified
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,HoldPosition_CutoffRatio,0x1e0,f32,0.5,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,HoldPosition_LengthMax,0x1cc,f32,400.0,disc,verified
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,HoldPosition_LengthMin,0x1c8,f32,150.0,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,HoldPosition_MaximumTime,0x1d4,f32,10.0,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,HoldPosition_MinimumTime,0x1d0,f32,5.0,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,HoldPosition_SideRatio,0x1d8,f32,0.30000001192092896,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,HomingResistAdjustment,0x230,f32,0.6000000238418579,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,IsDestructible,0x064,word,1,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,JumpIn,0x288,word,4294967295,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,JumpOut,0x28c,word,4294967295,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,LowerHPSE,0x294,word,4294967295,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,LowerHPThresholdRatio,0x298,f32,0.30000001192092896,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,MaxValue,0x238,f32,100.0,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,MaximumBank_Normal,0x100,f32,1.0471975803375244,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,MaximumVelocity,0x0a0,f32,1000.0,disc,verified
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,MinimumVelocity,0x09c,f32,100.0,disc,verified
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,MountedFCS,0x2ac,word,1,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,MountedShieldGenerator,0x070,word,0,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,PitchDragFactor,0x108,f32,3.0,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,PowerCutConsumeShield,0x13c,f32,0.0,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,PowerCutDeceleration,0x140,f32,0.0,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,RadarRange,0x2a0,f32,7000.0,disc,verified
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,ResistanceParalyze,0x084,f32,0.9800000190734863,disc,verified
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,ResistanceToExplosion,0x07c,f32,0.10000000149011612,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,ResistanceToOptics,0x074,f32,0.10000000149011612,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,ResistanceToPlayer,0x080,f32,1.0,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,ResistanceToShell,0x078,f32,0.10000000149011612,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,ReverseThrust_Acc,0x120,f32,2.0,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,ReverseThrust_Vc,0x118,f32,-0.5,disc,verified
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,RollDragFactor,0x10c,f32,3.0,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,SELength,0x29c,f32,2000.0,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,ShieldRatio,0x05c,f32,1.0,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,ShipEnvironmentSE,0x290,word,4294967295,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,SideRoll,0x16c,word,1,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,SideRoll_Length,0x174,f32,750.0,disc,verified
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,SideRoll_Time,0x170,f32,0.5,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,SideThrustAcceleration,0x0fc,f32,1000.0,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,SideThrustVelocity_Max,0x0f8,f32,1000.0,disc,verified
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,SideThruster,0x280,word,4294967295,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,Size_Radius,0x050,f32,19.0,disc,verified
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,Size_X,0x030,f32,24.0,disc,verified
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,Size_Y,0x034,f32,15.0,disc,verified
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,Size_Z,0x038,f32,38.0,disc,verified
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,Slalom,0x1e8,word,0,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,Slalom_CutoffRatio,0x1ec,f32,0.0,disc,verified
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,Slalom_TurnCount_Max,0x1f4,f32,0.0,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,Slalom_TurnCount_Min,0x1f0,f32,0.0,disc,verified
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,SolidCutoff,0x220,word,1,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,SolidCutoff_LengthMax,0x22c,f32,2500.0,disc,verified
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,SolidCutoff_LengthMin,0x228,f32,500.0,disc,verified
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,SolidCutoff_Ratio,0x224,f32,0.5,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,Through,0x1f8,word,1,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,Through_AngleMaximum,0x204,f32,1.0471975803375244,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,Through_AngleMinimum,0x200,f32,0.5235987901687622,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,Through_CutoffRatio,0x1fc,f32,0.5,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,Through_LengthMaximum,0x21c,f32,500.0,disc,verified
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,Through_LengthMinimum,0x218,f32,250.0,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,Through_Time1Max,0x208,f32,2.0,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,Through_Time1Min,0x20c,f32,5.0,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,Through_Time2Max,0x210,f32,1.5,disc,verified
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,Through_Time2Min,0x214,f32,5.0,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,Thruster,0x27c,word,512,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,ThrusterRatio,0x060,f32,1.0,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,TurnAttack,0x18c,word,1,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,TurnAttack_CutoffRatio,0x190,f32,0.4000000059604645,disc,verified
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,TurnAttack_DoubleRatio,0x194,f32,0.30000001192092896,disc,verified
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,TurnAttack_DoubleTimeMax,0x1a4,f32,2.5,disc,verified
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,TurnAttack_DoubleTimeMin,0x1a0,f32,0.5,disc,verified
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,TurnAway,0x1a8,word,1,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,TurnAway_Time_Maximum,0x1b4,f32,2.0,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,TurnAway_Time_Minimum,0x1b0,f32,0.5,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,Turn_AngularVelocity,0x1ac,f32,2.094395160675049,disc,verified
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,UsingChaffRatio,0x234,f32,0.4000000059604645,defaulted-on-disc,layout-derived
|
||||
UN_e005_ADAN_ElanTypeQ_Margras,YawDragFactor,0x104,f32,3.0,disc,verified
|
||||
UN_e006_ADAN_Vindicator_Margras,AA_AxisMode_Max,0x134,f32,0.0,defaulted-on-disc,layout-derived
|
||||
UN_e006_ADAN_Vindicator_Margras,AA_AxisMode_Min,0x138,f32,0.0,defaulted-on-disc,layout-derived
|
||||
UN_e006_ADAN_Vindicator_Margras,AA_PitchMinus_Max,0x0c8,f32,0.1745329350233078,disc,verified
|
||||
|
||||
|
Can't render this file because it is too large.
|
459
docs/re/challenge-mission-gate.md
Normal file
@@ -0,0 +1,459 @@
|
||||
# Challenge / EX missions — the stage set, the GamePart graph, and the kind field
|
||||
|
||||
**Status:** ✅ for the static structure (stage set, GamePart ids, the config-section
|
||||
switch) and for the **unlock mechanism** — a bit test against a *cleared-stage* mask,
|
||||
now **read live off the running game and matching the profile's progress exactly**
|
||||
(§5.6). 🟡 for the stage-field crash mechanism, for word B's writer, and for which
|
||||
mission consumes which bit; ❔ how the challenge menu is actually entered.
|
||||
**Method:** static analysis (`.pe` strings/pointers + DuckDB disassembly + disc
|
||||
records), then confirmed on the running title via the container-safe
|
||||
[`--hid=file` pad](../../../xenia-canary-native/src/xenia/hid/file/file_input_driver.h)
|
||||
and a live guest-memory read.
|
||||
**Evidence:** [`captures/challenge-map.txt`](captures/challenge-map.txt),
|
||||
[`captures/challenge-screen-config.txt`](captures/challenge-screen-config.txt),
|
||||
[`structures/achievements.md`](structures/achievements.md);
|
||||
`examples/challenge_map.rs`, `examples/challenge_screen.rs`.
|
||||
|
||||
## Why this was worth doing
|
||||
|
||||
The Route-B unit harvest is complete for the **story** campaign (68 of 110 units,
|
||||
7 115 defaulted-on-disc values) and stalled there: the remaining units are `*_EX4` /
|
||||
`*_EX5` / `*EX` variants that only the challenge missions field. The save's stage
|
||||
field addresses those stage records — but setting it to **27** boots to the title and
|
||||
then the emulator **exits during the load**, while 1–16 all load normally. This note
|
||||
answers *what the challenge missions are on disc* and *why the stage field alone is
|
||||
not enough*, so the next runtime attempt is targeted rather than another probe sweep.
|
||||
|
||||
## 1. The disc has exactly 29 stage records, in three families ✅
|
||||
|
||||
`StageResource` (schema `0x3c9ae32e`) in `dat/GP_MAIN_GAME_E.pak`:
|
||||
|
||||
| ids | count | `BackGroundID` | reading |
|
||||
|---|---|---|---|
|
||||
| `S01`–`S16` | 16 | Lebendorf … PD (real locations) | the **story** campaign |
|
||||
| `S18`–`S23` | 6 | `Original` (all six) | the **tutorial** missions |
|
||||
| `S24`–`S29` | 6 | Anastasis, Hargenteen ×2, Planet_Lebendorf, Lebendorf, Earth | the **challenge** missions |
|
||||
| `Test` | 1 | Earth | developer stage |
|
||||
|
||||
`S17` does not exist. This **matches `weapon.tbl`'s sprite-key set exactly** —
|
||||
`stage01…16` + `tutorial01…06` + `challenge01…06` (recorded in
|
||||
[static screen config](#)) — which is an independent confirmation of the three-way
|
||||
split: 16 + 6 + 6 + Test = 29.
|
||||
|
||||
The `Original` six carry ~43 tokens each while the story and challenge records carry
|
||||
51–59, consistent with tutorials having no squadron/route/formation tables.
|
||||
|
||||
## 2. `GP_CHALLENGE.pak` is a screen, not mission data ✅
|
||||
|
||||
151 entries, **0 IDXD objects** — sprites and layout only. So challenge missions are
|
||||
*not* a separate data set: they are ordinary `StageResource` records in
|
||||
`GP_MAIN_GAME_E.pak`, reached through a different menu. That is why the EX unit
|
||||
rosters show up in the same `EnumUnit_S<NN>` tables the story stages use.
|
||||
|
||||
## 3. The GamePart id table — the game's whole screen graph ✅
|
||||
|
||||
`.rdata` pointer array at **`0x820A1630`**, 29 entries, index = GamePart id:
|
||||
|
||||
```
|
||||
0 GP_TITLE 10 GP_BUNK 20 GP_STAGE_CLEAR
|
||||
1 GP_ADVERTISE_DEMO 11 GP_READY_ROOM 21 GP_MISSION_LOG
|
||||
2 GP_SELECT_STORAGE 12 GP_HANGAR 22 GP_GAMEOVER
|
||||
3 GP_LOAD 13 GP_ARSENAL 23 GP_DEBRIEFING
|
||||
4 GP_SAVE 14 GP_PILOT_LOG 24 GP_DIALOG
|
||||
5 GP_EXTRAS 15 GP_SYSTEM 25 GP_TUTORIAL
|
||||
6 GP_MOVIE_THEATER 16 GP_DEMO *26 GP_CHALLENGE
|
||||
7 GP_MISSION_SELECT 17 GP_MAIN_GAME 27 GP_LEADERBOARD
|
||||
8 GP_OPTIONS 18 GP_SELECTOR 28 GP_TEST
|
||||
9 GP_MOVIE 19 GP_PAUSE_MENU
|
||||
```
|
||||
|
||||
The indices are **not inferred from position** — the image carries the factory
|
||||
registration text, e.g.
|
||||
`silph::GamePartTask::RegisterToFactory<26, class silph::GamePart_ChallengeMission>::RegisterToFactory is failed!`
|
||||
and `<10, … GamePart_Bunk>`, both of which agree with this table. So `GP_CHALLENGE`
|
||||
is GamePart **26** and `GP_TUTORIAL` is **25**.
|
||||
|
||||
## 4. A mission-**kind** field selects the stage config section ✅
|
||||
|
||||
Immediately before the array above, `0x820A1600`–`0x820A162C` holds the stage-config
|
||||
key list: `BASE_INFO`, `StageResource`, `Background`, `PlayerUnit`,
|
||||
`EQUIIP_LIMITATION` *(sic)*, `MODEL_PATH`, `NEW_ITEM`, `LOADING`, `CHALLENGE`,
|
||||
`EXTRA`, `FILE`.
|
||||
|
||||
The stage loader picks **one of the last three** by a field at **`object + 144`**, and
|
||||
the same three-way switch appears at two independent sites (`0x82184df0` inside the
|
||||
all-keys config reader, and `0x82185ed0` in `sub_82185E80`):
|
||||
|
||||
```asm
|
||||
lwz r11, 144(r30)
|
||||
cmpi r11, 3 ; == 3 -> "EXTRA" (0x820A2160)
|
||||
beq extra
|
||||
addi r11, r11, -5
|
||||
cmpli r11, 1 ; == 5 or 6 -> "CHALLENGE" (0x820A2154)
|
||||
bgt file ; otherwise -> "FILE" (0x820A2168)
|
||||
```
|
||||
|
||||
Two further sites (`0x82186a60`, `0x82186cb8`) classify the same field as
|
||||
`{3, 5, 6}` versus everything else — i.e. **EXTRA and CHALLENGE together are "not an
|
||||
ordinary story load"**. Every write to `+144` inside this class (`0x82184b10`,
|
||||
`0x82185d48`, `0x82186ab4`) only *clears* it, and nothing in the image stores an
|
||||
immediate 3/5/6 into it, so the kind is **supplied from outside the class** — by
|
||||
whichever GamePart launches the mission — not derived from the stage number.
|
||||
|
||||
> ⚠️ **Withdrawn:** this section used to add "the field is initialised to 0 in the
|
||||
> constructor `sub_821783D8`, alongside `+148 = 0`, `+132 = 2`, `+136/+140 = -1`".
|
||||
> That linked two code regions on nothing more than both touching `+144`/`+148`, and
|
||||
> a snapshot **refutes it**: `sub_821783D8` initialises the **static** at
|
||||
> `0x828F3EC0` (its first act is `InitializeCriticalSection(obj, 256)`), and in an
|
||||
> in-flight snapshot that object's `+144` holds `0x000B1C8B` and `+592` a float —
|
||||
> not a kind and not flags. So **the object owning the kind field is unidentified**.
|
||||
> Scanning the snapshot for it (kind ∈ {0,3,5,6} at `+144`, stage at `+148`,
|
||||
> pointers at `+0`/`+52`/`+604`) returns only matches inside the executable's own
|
||||
> static data — `10` at `+148` is far too common to discriminate. The **switch
|
||||
> itself stands**: it is direct disassembly at two sites. Only the constructor
|
||||
> attribution was wrong.
|
||||
|
||||
**Confirmed on screen instead:** launching a story stage from MISSION SELECT reaches
|
||||
a READY ROOM carrying an **"EXTRA" watermark**
|
||||
([capture](captures/ready-room-extra-mode.png)) — the `EXTRA` config section, i.e.
|
||||
kind **3**, visible in the UI. That is independent evidence the kind field means what
|
||||
§4 says, even though its owning object is not pinned.
|
||||
|
||||
### 4.1 Why the stage-field probe crashes 🟡
|
||||
|
||||
That gives a mechanism for the observed failure: patching the save's stage field to
|
||||
27 selects the `S27` **record** while the kind stays **0**, so the loader reads the
|
||||
`FILE` section for a stage whose config lives under `CHALLENGE`. A missing section
|
||||
then propagates into the load, and the title exits. It fits the evidence (1–16 fine,
|
||||
every 24–29 the same failure, `/dev/shm` empty and disk free, so not the resource
|
||||
trap) but it is **not proven** — proving it needs a run.
|
||||
|
||||
**Cheap untried discriminator, no new tooling:** patch the stage field to **18–23**
|
||||
(the tutorials). If those also die, the failure tracks "record outside the story
|
||||
range", and the kind field is the likely gate. It has never been tested — only
|
||||
1–16 and 24–29 were.
|
||||
|
||||
## 5. The challenge screen, its six missions, and the unlock gate ✅
|
||||
|
||||
The factory registration sites (`0x8280C000`–`0x8280F800`) give **id → creator** for
|
||||
22 of the 24 registered parts, and the creators bound each class's code block. So
|
||||
`GamePart_ChallengeMission`'s methods are **`0x82187E60`–`0x8218CF10`** (between
|
||||
`GP_BUNK`'s creator `0x82187E38` and its own `0x8218CF10`). Resolving every string
|
||||
that range references gives the screen's whole config schema:
|
||||
|
||||
```
|
||||
MISSIONS · MISSION_ID · RECORD_TYPE · REQUIREMENT · REQUIREMENT_DESC
|
||||
NORMAL_BUTTON · GRAY_BUTTON · THUMBNAIL · STAGE_DESC · TEXT_STAGE
|
||||
TEXT_RECORD · NEW_STAGE · "Always" · "Time" · BASE_INFO
|
||||
```
|
||||
|
||||
**The record itself is on disc**, in `tables.pak` (schema `54a10697`, one copy per
|
||||
language — English is entry #64), *not* in `GP_CHALLENGE.pak`. It names six missions:
|
||||
|
||||
| slot | `MISSION_ID` | buttons / thumbnail | record |
|
||||
|---|---|---|---|
|
||||
| 1 | `TimeAttack` | `Button_TimeAttack{,_Gray}`, `Thumbnail_TimeAttack` | `Time` |
|
||||
| 2 | `ScoreAttack` | `Button_ScoreAttack{,_Gray}` | `Points` |
|
||||
| 3–6 | `Extra01`…`Extra04` | `Button_Extra0N{,_Gray}` | — |
|
||||
|
||||
`GRAY_BUTTON` is the locked art, `NORMAL_BUTTON` the unlocked art — so the screen
|
||||
always shows all six and greys out what you have not earned. Full dump:
|
||||
[`captures/challenge-screen-config.txt`](captures/challenge-screen-config.txt).
|
||||
|
||||
### 5.1 The gate is a bit test against a progress bitfield ✅
|
||||
|
||||
`0x82189870` builds the list, and `0x82189970`–`0x821899D8` is the availability
|
||||
decision for each mission:
|
||||
|
||||
```asm
|
||||
lookup REQUIREMENT in the mission record ; bl 0x82448C50
|
||||
absent -> AVAILABLE
|
||||
== "Always" -> AVAILABLE ; strcmp, bl 0x825EDD20
|
||||
else n = atoi(v) ; bl 0x825EDCD0
|
||||
n == 0 -> LOCKED
|
||||
n < 24 -> bit n of word A
|
||||
n >= 24 -> bit (n-24) of word B
|
||||
bit set ? AVAILABLE : LOCKED
|
||||
```
|
||||
|
||||
Word A and word B are read from a **singleton** (`0x821707C0`; the object pointer
|
||||
lives at the global `0x828F48B0`, with a construct-once flag at `0x828F48BC`) at
|
||||
offsets **`+80`** and **`+1956`**. So challenge
|
||||
availability is one bit in a progress bitfield, and `REQUIREMENT` is that bit's index
|
||||
— **not** a stage number, a score, or a difficulty.
|
||||
|
||||
### 5.2 The bit space is CLEARED STAGES ✅
|
||||
|
||||
Word A has exactly **one** writer in the image, and finding it settles the question.
|
||||
Scanning all 22 callers of the `+80` struct copier (`0x82175110`) for one that stores
|
||||
to the copy's **word 0** gives a single hit, `0x821C1820`, inside
|
||||
**`GamePart_StageClear`** (`0x821C09D8`–`0x821C29F0`):
|
||||
|
||||
```asm
|
||||
if (this+1004 & 0x20000) skip ; already recorded
|
||||
this+80 = 1
|
||||
copy local = singleton->progress ; bl 0x82175110, src = obj+80
|
||||
x = this+84
|
||||
local.word0 |= 1 << x ; slw r10, r26, r10
|
||||
if changed: singleton->set(local) ; bl 0x8216FF70 (assign + async persist)
|
||||
```
|
||||
|
||||
**`this+84` is the stage number**, and two independent uses prove it:
|
||||
|
||||
- `0x821C1760` indexes a **20-byte record array** with it — `this + (x+7)*20`, whose
|
||||
records are written as three words plus an 8-byte timestamp, i.e. the shape of the
|
||||
savegame's 16×20-byte `SHAB` table;
|
||||
- `0x821C1EEC` and `0x821C1924` pass it as the **index into the config key `STAGE`**
|
||||
(`0x820A2540`) — the debriefing config's `px_deb_stage01…16` sprite list.
|
||||
|
||||
So **word A is a "stage cleared" bitmask**, and a challenge mission's `REQUIREMENT n`
|
||||
means **"stage `n` has been cleared"**. The `< 24` / `>= 24` split then lines up with
|
||||
the disc's own stage numbering from §1: story `1`–`16` and tutorial `18`–`23` sit in
|
||||
word A, and the challenge stages `24`–`29` are exactly word B's bits `0`–`5`.
|
||||
|
||||
> ⚠️ **Third revision of this claim — the first two were wrong, and how they went
|
||||
> wrong is worth keeping.** An earlier pass read the split at 24 as "the game's 24
|
||||
> achievements" because `ACHIEVEMENTS_REQUIREMENTS` has exactly 24 entries. That is a
|
||||
> **coincidence**: the achievement count and the first challenge stage id are both 24
|
||||
> for unrelated reasons. The achievement work itself stands — the `XACH` table, the
|
||||
> 1000G self-check, and the `XACHIEVEMENT_DETAILS`/XAM enumeration are all solid, and
|
||||
> are documented in [structures/achievements.md](structures/achievements.md) — it just
|
||||
> **does not gate the challenge missions**. The lesson: a numeric coincidence is not a
|
||||
> join; find the writer.
|
||||
|
||||
### 5.3 What the six requirement values are ✅/🟡
|
||||
|
||||
The record's numeric tokens are `16`, `25`, `26`, `27`, `29`, with `24`/`28` already
|
||||
in the pool earlier (they double as font metrics) and so **deduped away** if used.
|
||||
IDXD dedup makes positional key/value pairing unsound in general — but `16` sits
|
||||
*immediately* before `REQUIREMENT` (tokens 96 → 97), the documented value-before-key
|
||||
adjacency, so the first mission's value is well-supported.
|
||||
|
||||
**`TimeAttack` requires stage 16 cleared** — the final story mission. That is the
|
||||
natural gate, and it is what the *first* reading of these numbers suggested before the
|
||||
achievement detour talked me out of it.
|
||||
|
||||
The other five values are `≥ 24`, i.e. **challenge stages 24–29**: the challenge
|
||||
missions chain off each other. 🟡 on the exact pairing (which mission needs which),
|
||||
which needs the record's binary index section rather than the string pool.
|
||||
|
||||
🟡 **Word B has no known writer.** `GamePart_StageClear` sets `1 << x` into word A
|
||||
unconditionally, which for a challenge stage (`x ≥ 24`) would land on word A bits
|
||||
24–29, *not* word B — so clearing a challenge mission must be recorded by a different
|
||||
path, presumably one that also stores its `Time`/`Points` record. Not yet found.
|
||||
|
||||
**Negative worth keeping:** the requirement *text* (`TimeAttackRequirement`,
|
||||
`Extra01Requirement`, …) is **not** in `GP_CHALLENGE.pak` — building a `TextIndex`
|
||||
over it yields **0 entries** and its only wordy payload is embedded font copyright.
|
||||
The config's `PATH` is `dat\GP_CHALLENGE.pak+eng\`, a per-language branch, so those
|
||||
keys resolve through a naming scheme the current loader does not reproduce. Reading
|
||||
them would say in plain English what each mission asks for — worth one more attempt
|
||||
via `hash::TOC_NAME_SCHEMES`.
|
||||
|
||||
### 5.4 Where the mask lives, and one lead refuted
|
||||
|
||||
### 5.5 Both gate words are one record — and it is **not** the savegame ✅
|
||||
|
||||
`0x82175110` copies the record at singleton `+80` in full: two words, an 8-byte pair, a
|
||||
184-byte `memcpy`, 8 words from `+200`, then **816 bytes at `+232`, 816 more at
|
||||
`+1048`**, a sub-object at `+1864`, and a final word at **`+1876`**. So the record runs
|
||||
`+0 … ~+1880`, i.e. singleton `+80 … +1960` — which means
|
||||
|
||||
- **word A** is record `+0` (singleton `+80`), and
|
||||
- **word B is record `+1876`** (singleton `+1956`).
|
||||
|
||||
That answers §5.3's "word B has no known writer": there is no separate `stw` because
|
||||
the whole record is copied out, modified and assigned back as a unit
|
||||
(`0x8216FF70` → compare `0x822C3708`, assign `0x82170650`, then spawn a worker
|
||||
`0x821700A8` under a lock which retries a commit `0x822C33B8` up to five times).
|
||||
`GamePart_Debriefing` uses the same get/modify/set to accumulate saturating career
|
||||
counters at `+200`…`+224` (the last 64-bit) — the quantities the achievement
|
||||
requirements test.
|
||||
|
||||
**And the record is not what `savedata` holds.** Two independent checks:
|
||||
|
||||
- **Size.** Every real save on disk is a **276-byte** container deflating to a
|
||||
**545-byte** payload. The progress record is **~1 880 bytes**. It does not fit.
|
||||
- **Files.** After many sessions the game's content tree holds only
|
||||
`game0N/savedata` + `game0N/__thumbnail.png` per slot and the three
|
||||
`Headers/*.header` — no second data file anywhere.
|
||||
|
||||
Also checked and negative: the savegame object is `*(*(this+4)) + 304`, and the
|
||||
singleton's holder address (`0x828F48B0`) is referenced **nowhere but inside the
|
||||
accessor itself**, so `this+4` is a different holder — the save block is not a window
|
||||
into this record.
|
||||
|
||||
⚠️ **So hand-editing a save cannot unlock the challenge missions.** That kills the
|
||||
operational hope this section previously carried; the earlier savegame-editing win
|
||||
does not extend here.
|
||||
|
||||
### 5.6 CONFIRMED ON THE RUNNING GAME ✅
|
||||
|
||||
Read live from a booted title (`tools/re-capture/gpoke.py r32`):
|
||||
|
||||
```
|
||||
0x828F40C0 = 0x00000002 word A
|
||||
0x828F4814 = 0x00000000 word B
|
||||
```
|
||||
|
||||
**Word A = 2 = bit 1 set.** The profile's save is *Stage 02, "At Standby"* — i.e.
|
||||
**stage 01 cleared** — so the mask is exactly one bit, at the index of the one
|
||||
cleared stage, **1-based**. Reproduced on two separate cold boots. That confirms, on
|
||||
the running game and against a known progress state:
|
||||
|
||||
- the singleton really is the static object at `0x828F4070`;
|
||||
- word A is a **cleared-stage bitmask**, not achievements, not a stage number;
|
||||
- bit index = **stage id, 1-based** — so `TimeAttack`'s `REQUIREMENT 16` is
|
||||
"clear stage 16", the last story mission;
|
||||
- word B is the challenge half and is `0` on a story-only profile, as expected.
|
||||
|
||||
Both words were then poked (`0xFFFFFFFF` / `0x3F`) and read back OK.
|
||||
|
||||
**MISSION SELECT shows the mask directly** ([capture](captures/mission-select-stage01-only.png)).
|
||||
With word A = 2 the screen lists `Stage01` **selectable, with a High Score and Best
|
||||
Time**, and `Stage02`–`Stage08` **greyed out**. One cleared stage, one selectable
|
||||
entry, at the bit index that names it — the semantics are visible on screen, not
|
||||
inferred.
|
||||
|
||||
**And the mask drives that screen.** Two runs, identical navigation, fresh boot each:
|
||||
|
||||
| run | word A | MISSION SELECT |
|
||||
|---|---|---|
|
||||
| control | `0x00000002` (untouched) | opens; Stage01 selectable, rest greyed |
|
||||
| poked | `0xFFFFFFFF` | `MmAllocatePhysicalMemoryEx` fails on a 128 MB request, guest throws, Xenia shows "Disc Read Error" |
|
||||
|
||||
So the earlier failure was **caused by the poke**, and by a careless one: `0xFFFFFFFF`
|
||||
claims stages that do not exist (`0`, `17`, `24`–`31` in word A). Poking only real
|
||||
story ids (`0x0001FFFE` = stages 1–16) does **not** blow the heap. That the list
|
||||
screen changes behaviour with the mask is itself confirmation that word A feeds it.
|
||||
|
||||
**Poking real stage ids unlocks the story campaign in the menu** ✅. With word A =
|
||||
`0x0001FFFE` (stages 1–16) every entry `Stage01`…`Stage16` is selectable
|
||||
([capture](captures/mission-select-all-story-unlocked.png)) where the control had only
|
||||
`Stage01`; `Stage16` reads *"Lonely Blue Planet — NO RECORD"*. So **any story stage
|
||||
can now be launched from the menu**, with no save editing, by poking one word.
|
||||
|
||||
**But MISSION SELECT is story-only** ❌. With word B = `0x3F` (challenge stages 24–29
|
||||
marked cleared) the list still **saturates at `Stage16`**
|
||||
([capture](captures/mission-select-ends-at-stage16.png)) — the cursor stops there and
|
||||
further presses do nothing. That matches the data: the debriefing config declares
|
||||
exactly `px_deb_stage01…16`, so the list length is capped by the disc, not by the
|
||||
mask. **The challenge missions cannot be reached through this screen**, and word B
|
||||
does not feed it.
|
||||
|
||||
**What the poke did not do (yet):** `EXTRAS` still shows only `MISSION SELECT /
|
||||
MOVIE THEATER / BACK` — no challenge entry — although the menu was built 26 s
|
||||
*after* the poke, so this is not staleness. Entering `MISSION SELECT` then failed,
|
||||
and the log gives the real reason: **`MmAllocatePhysicalMemoryEx` could not satisfy
|
||||
a 128 MB request** (`parent free 30633/131072 pages`), the guest threw a C++
|
||||
exception, and Xenia surfaced it as its generic *"Disc Read Error"* dialog. It is
|
||||
preceded by `BaseHeap::Release failed because address is not a region start`, a
|
||||
failed release that leaks the range. So that is an emulator/heap problem on the way
|
||||
into the screen, **not** evidence about the gate. Open: repeat without the poke to
|
||||
see whether `MISSION SELECT` fails the same way regardless.
|
||||
|
||||
### 5.7 What *would* work — static addresses for a live write
|
||||
|
||||
The singleton is a **static object at `0x828F4070`** (`0x8216F650`:
|
||||
`addis 0x828F` + `addi …, 16496`), with the holder at `0x828F48B0` pointing at it. So
|
||||
the two gate words are at fixed guest addresses, no scanning required:
|
||||
|
||||
| word | guest VA | meaning |
|
||||
|---|---|---|
|
||||
| A | **`0x828F40C0`** | cleared stages, bit = stage id (`< 24`) |
|
||||
| B | **`0x828F4814`** | cleared stages, bit = stage id − 24 (challenge 24–29) |
|
||||
|
||||
Canary maps guest RAM into `/dev/shm`, which the project already reads live
|
||||
([`tools/re-capture/gmem.py`](../../tools/re-capture/gmem.py)). Writing
|
||||
`0xFFFF` into word A and `0x3F` into word B while the title sits on a menu should
|
||||
open all six challenge missions **without playing the campaign** — and that is the
|
||||
route to the last 42 `*_EX4`/`*_EX5` units. Untested: it needs a run, and the run
|
||||
needs gamepad input to reach the menu.
|
||||
|
||||
**The obvious lead is dead.** The three `stw`s to `+1956` in `0x822AF278` /
|
||||
`sub_822C8748` looked promising because they sit in the save serializer's code region
|
||||
— they are on a **different object**. That one is fetched through `0x822CEB30`, has a
|
||||
`+2652` flag the code checks first, and stores **string pointers** at `+1956`/`+2024`
|
||||
(built by `0x822D35F8`); a pointer `AND`ed with `1 << n` would be meaningless as a
|
||||
gate. So **nothing in the image stores to this singleton's `+1956` field-wise**, which
|
||||
means it is filled by a bulk copy or by a path not yet found.
|
||||
|
||||
Two candidates remain and they need different levers: the **save** (hand-write it) or
|
||||
the **Xbox profile** (the emulator's profile data). Note that XEX imports are resolved
|
||||
**by ordinal**, so the absence of `XamUser*` name strings in the `.pe` is not evidence
|
||||
against the profile route. `+80` at least is handled as a small struct by address
|
||||
(`addi r4, obj, 80` → copy helper `0x82175110`, written back via `0x8216FF70`), which
|
||||
is what a serialised value object looks like.
|
||||
|
||||
### 5.8 The part id is never persisted — differential search ✅ (negative)
|
||||
|
||||
`sub_821749C0` creates a part from `slot+12`, and no literal 26 exists anywhere, so
|
||||
the id is computed. It can still be found by **differential search**, because the id
|
||||
is *known* at each screen from §3: `GP_EXTRAS` = 5, `GP_MISSION_SELECT` = 7. Snapshot
|
||||
both screens and intersect (`tools/re-capture/diff_words.py`):
|
||||
|
||||
```
|
||||
scanned 171 MB of allocated guest memory
|
||||
addresses reading 5 on EXTRAS and 7 on MISSION SELECT: 4
|
||||
0x708FFBEC 0x708FFCBC 0x708FFDAC 0x708FFE20
|
||||
```
|
||||
|
||||
**All four are guest stack** (thread stacks sit at `0x709…` in the same run's log).
|
||||
So the requested part id exists only as a **stack argument in flight** — there is no
|
||||
persistent field holding it, which is consistent with finding no literal store and
|
||||
means **there is nothing stable to poke**. Forcing a transition to GamePart 26 needs
|
||||
the caller's context, i.e. an emulator-side hook rather than a memory write.
|
||||
|
||||
**Every memory-and-menu route to the challenge missions is now closed** (§5.4, §5.6,
|
||||
§5.7, this section). What remains is the genuine in-game unlock — an in-mission
|
||||
attainment, per `AVSCRIPT_COMMAND_ATTAINMENT_CHALLENGE_MISSION_CARGO_SCORE` — or a
|
||||
Canary patch that forces the transition in emulator code.
|
||||
|
||||
## 6. The unlock condition, from the strings ❔
|
||||
|
||||
Three strings say challenge missions are *announced*, not menu-browsed:
|
||||
|
||||
- `AVSCRIPT_COMMAND_ATTAINMENT_CHALLENGE_MISSION_CARGO_SCORE` — a mission-script
|
||||
command that grants challenge-mission attainment from a **cargo score**;
|
||||
- `DLG_CHALLENGE_MISSION_AVAILABLE` — the "a challenge mission is now available"
|
||||
dialog;
|
||||
- `DLG_GO_CHALLENGE_MISSION_MENU` — the prompt that takes you to GamePart 26.
|
||||
|
||||
So the gate is plausibly an in-mission achievement, not a title-menu state — which is
|
||||
consistent with `Game Status = GAME_CLEAR` unlocking nothing (measured, refuted).
|
||||
Neither dialog key is reachable by xref: they are selected **by index** through a
|
||||
config lookup, exactly like the save screen's sprite keys, so there is nothing to
|
||||
grep back to. Naming the flag needs the challenge-availability check disassembled
|
||||
from the screen side.
|
||||
|
||||
## 7. What this makes actionable
|
||||
|
||||
`roster_target` against the harvested CSV, with the stage families now named:
|
||||
|
||||
| stage | family | roster units not yet read |
|
||||
|---|---|---|
|
||||
| `S27` | challenge | `f003_ArrowHead_EX4`, `e107_AAFrigate_EX4`, `e010_Attacker_S_EX4`, `e011_Attacker_B_EX4`, `e007_Turret_EX4`, `e008_TurretPlus_EX4` |
|
||||
| `S28` | challenge | `f004_DeltaSaber_A_Player`, `f001_DeltaSaber_T_EX5(_el)`, `f003_ArrowHead_EX5`, `f104_Battleship_EX5`, `f105_Cruiser_EX5` |
|
||||
| `S25` | challenge | `e101_SDBattleshipEX`, `e102_BattleshipEX`, `e105_CruiserEX`, `e108_ASFrigateEX` |
|
||||
| `S29` | challenge | `e101_SDBattleshipEX`, `e102_BattleshipEX`, `e105_CruiserEX` |
|
||||
| `S24` | challenge | `e105_CruiserEX`, `e106_DestroyerEX` |
|
||||
| `S10` | **story** | `e005_ADAN_ElanTypeQ_Margras` |
|
||||
|
||||
⚠️ **The `S10` row contradicts "the story campaign is complete."** One story stage
|
||||
still fields a unit that has never been read. Either `S10` was never flown (it is the
|
||||
smallest stage container on the disc — 7 XBG7 resources — so it may be a cutscene
|
||||
stage that is not flyable), or the completeness claim was one unit optimistic. Flagged
|
||||
rather than resolved: it costs one ordinary story run to settle.
|
||||
|
||||
Remaining coverage: 42 units missing, of which the rosters above account for ~23. The
|
||||
rest are not in any stage roster table and need a different lever.
|
||||
|
||||
## Reproduce
|
||||
|
||||
```bash
|
||||
cargo run --release -q -p sylpheed-formats --example challenge_map -- <disc-root>
|
||||
cargo run --release -q -p sylpheed-formats --example challenge_screen -- <disc-root>
|
||||
python3 xenia-rs/zq.py dis 0x82184df0 0x82184e30 # the three-way section switch
|
||||
python3 xenia-rs/zq.py dis 0x82185ed0 0x82185f10 # the same switch, second site
|
||||
python3 xenia-rs/zq.py dis 0x82189860 0x821899f0 # the challenge availability gate
|
||||
```
|
||||
@@ -420,3 +420,319 @@ measurement specified above.
|
||||
**The clock ratio has now been measured three times in three flights: 1.260, 1.311,
|
||||
1.383.** It is not a property of the machine but of the moment, so any rate probe
|
||||
must bracket its own phases.
|
||||
|
||||
|
||||
## Roll, re-measured about the forward axis — the withdrawal is reversed ✅
|
||||
|
||||
The earlier roll result was withdrawn because the *measurement* was wrong, not the
|
||||
input: watching a non-forward row of the rotation matrix sees **any** rotation that
|
||||
moves that row, and pitch moves it as much as roll, which is why two different stick
|
||||
axes produced the same rates. `tools/re-capture/roll_axis.py` measures rotation
|
||||
**about** the forward axis instead — express the new up-vector in the old
|
||||
`(up, right)` basis and take `atan2(u_new·w_old, u_new·u_old)`, so any component
|
||||
along forward (what pitch produces) is dropped by construction.
|
||||
|
||||
Run on stage 02 with the container-safe file pad, which matters here: the pad state
|
||||
is written whole, so the stick is deflected on **exactly one axis** (`lx=32767`,
|
||||
every other channel exactly 0) while the throttle trigger is held in the same write.
|
||||
5-second settles, 8-second dwells, and each phase bracketed by HUD-clock screenshots
|
||||
because the clock ratio is a property of the moment (1.260/1.311/1.383 previously).
|
||||
|
||||
| phase | swept | wall | HUD clock | per **game** second | definition |
|
||||
|---|---|---|---|---|---|
|
||||
| min speed (`LT`) | 1 856.9° | 8.01 s | 01:20.64 → 01:30.63 (×1.247) | **185.9 °/s** | `AV_Roll_Min` **200** |
|
||||
| max speed (`RT`) | 1 181.2° | 8.02 s | 01:37.11 → 01:46.88 (×1.218) | **120.9 °/s** | `AV_Roll_Max` **125** |
|
||||
|
||||
**Roll DOES depend on speed**, and the withdrawn claim ("roll shows no speed
|
||||
dependence, unlike pitch") is **reversed**. Roll behaves exactly like pitch: the rate
|
||||
cap falls as speed rises, and `_Min`/`_Max` mean *at minimum / at maximum speed* —
|
||||
the same rule the pitch work established, now confirmed on a second axis rather than
|
||||
contradicted by it.
|
||||
|
||||
Both measurements land **just under** their caps (93 % and 97 %), which is the right
|
||||
side for a rate limit. 🟡 The shortfall is not explained: candidates are the craft not
|
||||
being exactly at min/max speed after 5 s, and the swept-angle sum slightly
|
||||
undercounting at 20 Hz. Neither is worth a claim without measuring it.
|
||||
|
||||
Method note: the tell that the *old* result was broken was two conditions agreeing
|
||||
too well (pitch ≈ roll). The tell that this one is sound is that they now **disagree
|
||||
in the direction the definitions predict**, on two independently-bracketed phases.
|
||||
Data: [`captures/roll-about-forward-axis.csv`](captures/roll-about-forward-axis.csv).
|
||||
|
||||
|
||||
## Which input drives which axis — and the yaw answer ✅
|
||||
|
||||
`tools/re-capture/axis_probe.py` holds one channel at a time (file pad, so every
|
||||
other channel is exactly zero) and decomposes the result into **all three** rotation
|
||||
components at once, rather than measuring one axis through a row that moves under any
|
||||
rotation — the flaw that once made roll and pitch produce identical numbers.
|
||||
|
||||
**The row labelling is measured, not assumed.** `entities2` pins row 2 = forward
|
||||
against velocity; the probe pins the other two by comparing **world-Y** in flight:
|
||||
|
||||
```
|
||||
row world-Y means: [0.469, 0.883, -0.000] forward = row 2
|
||||
-> up = row 1, right = row 0 CONFIDENT
|
||||
```
|
||||
|
||||
That is the **opposite** of the D3D convention an earlier run assumed, which means
|
||||
that run's *yaw* and *pitch* columns were swapped — under the correct labels its
|
||||
`ly+` reading of 154.1 °/wall-s is **pitch**, which is what a left-stick Y axis
|
||||
should do.
|
||||
|
||||
| input | roll | yaw | pitch | |
|
||||
|---|---|---|---|---|
|
||||
| `rx` right-stick X | 0.0 | 0.0 | 0.0 | nothing |
|
||||
| `ry` right-stick Y | 0.0 | 0.0 | 0.0 | nothing |
|
||||
| `LB` | 0.0 | 0.0 | 0.0 | nothing |
|
||||
| `RB` | 0.0 | 0.0 | 0.0 | nothing (it is the nose gun) |
|
||||
| `lx` left-stick X | 209.8 | ~0 | ~10 | **roll** |
|
||||
| `ly` left-stick Y | ~0 | ~0 | 154.1 | **pitch** |
|
||||
|
||||
These zeros are **trustworthy**, unlike the previous attempt's: the four unknown
|
||||
inputs were measured *first*, each passing a liveness check, and the run aborted the
|
||||
moment the craft stopped moving instead of reporting the clean zeros a destroyed
|
||||
craft produces.
|
||||
|
||||
**So no pad input yaws the craft directly.** `AV_Yaw_*` (45/25) exists in the unit
|
||||
definitions but nothing on the right stick or the shoulders drives it — the earlier
|
||||
❔ *"no input found"* is upgraded from "we could not find one" to **"the remaining
|
||||
candidates measurably do nothing"**. The natural reading is that yaw is a
|
||||
*consequence* of banking rather than a commanded axis, which a reimplementation
|
||||
should model as such.
|
||||
|
||||
🟡 Not covered: the **d-pad** (the tactical map) and the **face buttons** (fire /
|
||||
weapon select). Neither is a plausible flight axis, but neither was measured here.
|
||||
Data: [`captures/axis-probe-rows-pinned.csv`](captures/axis-probe-rows-pinned.csv).
|
||||
|
||||
|
||||
## ⚠️ Angular-rate probes measure a MOVING speed — the dwell bleeds it
|
||||
|
||||
Measuring pitch with the rows properly pinned gave, for
|
||||
`UN_f001_TCAF_DeltaSaber_T_Player` (whose own disc values are
|
||||
`AV_PitchPlus_Min` **150°**, `AV_PitchPlus_Max` **70°**):
|
||||
|
||||
| phase | swept | clock | per game-s | cap |
|
||||
|---|---|---|---|---|
|
||||
| min speed (`LT`) | 1 391.0° / 8.00 s | ×1.326 | **131.1** | 150 |
|
||||
| max speed (`RT`) | 989.0° / 8.05 s | ×1.318 | **93.2** | 70 — **133 % of cap** |
|
||||
|
||||
A rate 33 % *above* a cap is not a finding, it is a broken instrument, so read the
|
||||
HUD speed off the same bracketing screenshots that gave the clock:
|
||||
|
||||
```
|
||||
slow phase 102 -> 18
|
||||
fast phase 1193 -> 589
|
||||
```
|
||||
|
||||
**The speed is not constant during the dwell — pitching bleeds it hard**, halving it
|
||||
in 8 seconds. The rate cap is itself speed-dependent, so as the craft slowed its cap
|
||||
*rose*, and the 8-second average necessarily lands between the max-speed cap and a
|
||||
mid-speed one. The 133 % is fully explained by the instrument.
|
||||
|
||||
**This weakens the roll numbers above as well.** They were taken the same way, so
|
||||
`120.9` vs `AV_Roll_Max` `125` is *consistent* but is **not a tight test** — the true
|
||||
max-speed roll cap could be lower and still produce that average. The min-speed
|
||||
figures are less affected (there is little speed left to lose).
|
||||
|
||||
**How to measure it properly** (not yet done): keep the dwell to ~1–2 s so the speed
|
||||
barely moves, or sample the HUD speed continuously and fit rate against
|
||||
*instantaneous* speed rather than assuming the endpoint. The second is strictly
|
||||
better and gives the whole rate-vs-speed curve rather than two points.
|
||||
|
||||
**And a flight-model finding in its own right:** *turning costs speed*, steeply —
|
||||
1 193 → 589 under 8 s of full pitch, with the throttle still held at maximum. A
|
||||
reimplementation that treats the throttle as setting a speed the craft simply holds
|
||||
will be wrong during manoeuvres. Data:
|
||||
[`captures/pitch-rate-speed-bleed.csv`](captures/pitch-rate-speed-bleed.csv).
|
||||
|
||||
|
||||
## ⚠️ Polling faster than the guest updates manufactures a curve
|
||||
|
||||
Trying to fit rate against *instantaneous* speed (`rate_curve.py`, one long hold so
|
||||
the speed-bleed sweeps the range) produced a beautifully clean result that is
|
||||
entirely an artefact:
|
||||
|
||||
```
|
||||
speed 0- 599 rate 29.4 speed 1199-1798 rate 279.8
|
||||
speed 599-1199 rate 174.9 speed 1798-2398 rate 317.5
|
||||
```
|
||||
|
||||
Rate rising with speed, and speeds to **4 795** when the craft's maximum is 1 200.
|
||||
The cause: **20 Hz polling is faster than the guest updates these fields**, so a
|
||||
per-read delta is either exactly zero (no update yet) or a whole frame's worth
|
||||
divided by a fraction of a frame. In that run **111 of 352 reads were zero on BOTH
|
||||
channels** — position and attitude update on the same frame, so the two are
|
||||
perfectly correlated, and dividing each by the short wall `dt` produced a tidy
|
||||
correlation out of nothing.
|
||||
|
||||
**Fix: aggregate over windows spanning many frames** (0.5 s here). The sum of
|
||||
`|Δ|` over a window is right however the updates fall inside it.
|
||||
|
||||
**This does NOT affect the swept-total probes** (`roll_axis.py`, `rate_probe.py`):
|
||||
they already summed over the whole dwell, which is immune for the same reason. Only
|
||||
per-sample instantaneous rates were ever wrong.
|
||||
|
||||
🟡 **The windowed re-run is not yet a result.** It gives plausible magnitudes
|
||||
(speed 320–935, rate 80–149 °/wall-s) but still shows rate *rising* with speed,
|
||||
against the definition's `AV_PitchPlus_Min` 150 > `_Max` 70. Two disqualifiers: it
|
||||
ran on an instance where the craft was already tumbling from the previous sweep, so
|
||||
the row pinning reported **WEAK — craft may not be level**, and the starting speed
|
||||
was mid-range rather than maximum. A clean answer needs a **fresh flight**, pinning
|
||||
CONFIDENT, one sweep, nothing before it. Recorded as an open question rather than a
|
||||
finding — the definitions' `_Min`/`_Max` meaning is exactly what is in doubt, so a
|
||||
measurement taken through a doubtful instrument cannot settle it.
|
||||
|
||||
Data: [aliased, for reference](captures/rate-curve-aliased-BAD.csv) ·
|
||||
[windowed](captures/rate-curve-windowed.csv).
|
||||
|
||||
|
||||
## The clean sweep: magnitudes agree, the LAW does not follow 🟡
|
||||
|
||||
Fresh flight, row pinning **CONFIDENT** (margin 0.413), one sweep and nothing before
|
||||
it — the conditions the previous attempt lacked. Clock 01:16.08 → 01:41.46 (×1.26).
|
||||
Binned by speed, both converted to game units:
|
||||
|
||||
| speed (game) | measured °/game-s | linear interpolation of the caps |
|
||||
|---|---|---|
|
||||
| ~435 | 100.8 | 125.6 |
|
||||
| ~572 | 113.8 | 115.7 |
|
||||
| ~709 | 126.3 | 105.7 |
|
||||
| ~846 | 83.1 | 95.7 |
|
||||
| ~983 | 72.7 | 85.8 |
|
||||
|
||||
(prediction = `AV_PitchPlus_Min` 150 at `MinimumVelocity` 100 → `_Max` 70 at
|
||||
`MaximumVelocity` 1200, interpolated linearly.)
|
||||
|
||||
**What this supports:** the magnitudes are right — measured 73–126 °/game-s across
|
||||
speeds 400–1 050 against a predicted 86–126 — and the high-speed end falls, as a
|
||||
speed-dependent cap should.
|
||||
|
||||
**What it does not support:** the interpolation *law*. The scatter is ±25 %, the two
|
||||
fastest bins hold only 1 and 2 windows each (they are the first moments before the
|
||||
speed bled), and the slowest bin disagrees in the wrong direction (100.8 measured vs
|
||||
125.6 predicted). A sweep that is *driven* by the speed bleeding cannot spend long at
|
||||
either extreme, which is exactly where the law is most testable.
|
||||
|
||||
**The design that would settle it**, and why this one cannot: hold a *settled*
|
||||
throttle, pitch for only **~1 second**, and read the rate — the speed barely moves
|
||||
inside a 1 s burst, so each burst yields one honest `(speed, rate)` point. Repeat at
|
||||
`LT` / neutral / `RT` for three clean points at known speeds, instead of one smeared
|
||||
sweep. Data:
|
||||
[`captures/pitch-rate-curve-clean.csv`](captures/pitch-rate-curve-clean.csv).
|
||||
|
||||
|
||||
## ✅ Short bursts at settled speeds: the law's SHAPE is confirmed, clock-free
|
||||
|
||||
The design the sweep could not provide: settle the throttle, measure the settled
|
||||
speed, then pitch for **one second** so the speed barely moves inside the burst.
|
||||
Three throttle settings, two repeats each, row pin **CONFIDENT**, fresh flight.
|
||||
|
||||
| throttle | burst speed (wall) | rate (°/wall-s) |
|
||||
|---|---|---|
|
||||
| `LT` min | ~105 | 113.6, 109.5 |
|
||||
| none, cruise | ~383 | 100.2, 88.4 |
|
||||
| `RT` max | ~1 483 | 52.2, 70.5 |
|
||||
|
||||
**Rate falls monotonically with speed** — 111.5 → 94.3 → 61.4 — measured at three
|
||||
*known, settled* speeds rather than smeared across a bleeding one.
|
||||
|
||||
**And the decisive comparison needs no clock at all.** Absolute rates depend on the
|
||||
run's clock ratio, but the **min:max ratio cancels it**:
|
||||
|
||||
```
|
||||
measured min:max = 1.82
|
||||
AV_PitchMinus_Min/Max 75/40 = 1.88 -> 3.0 % apart
|
||||
AV_PitchPlus_Min/Max 150/70 = 2.14 -> 15.1 % apart
|
||||
```
|
||||
|
||||
So two things follow, neither resting on a clock measurement:
|
||||
|
||||
1. **`_Min` / `_Max` really do mean "at minimum / at maximum speed"**, with the rate
|
||||
interpolating between them — the shape is confirmed to 3 %.
|
||||
2. **`ly+` drives pitch-MINUS**, not pitch-plus. The craft has asymmetric pitch
|
||||
authority (75/40 down versus 150/70 up), and the ratio picks the pair cleanly.
|
||||
|
||||
🟡 **Absolute magnitudes are still open** — this run did not bracket the HUD clock, so
|
||||
deg/*game*-second cannot be computed from it, and choosing a ratio that makes the
|
||||
numbers fit would be circular. The probe now takes clock screenshots at both ends so
|
||||
the next run closes it. Data:
|
||||
[`captures/pitch-burst-settled-speeds.csv`](captures/pitch-burst-settled-speeds.csv).
|
||||
|
||||
|
||||
## Both axes, three settled speeds: shape ✅, absolute scale 🟡
|
||||
|
||||
Repeat of the burst design with the HUD clock bracketed (01:01.86 → 02:10.61) and
|
||||
roll measured in the same flight. Clock ≈ ×1.26.
|
||||
|
||||
| axis | min speed | cruise | max speed | min:max | definition ratio |
|
||||
|---|---|---|---|---|---|
|
||||
| pitch | 87.0 °/game-s @ 85 | 82.8 @ 308 | 47.0 @ 1 077 | **1.85** | `PitchMinus` 75/40 = **1.88** |
|
||||
| roll | 125.0 @ 76 | 119.0 @ 291 | 82.6 @ 1 023 | **1.51** | `Roll` 200/125 = **1.60** |
|
||||
|
||||
**The shape is confirmed on both axes** — the rate interpolates between `_Min` (at
|
||||
minimum speed) and `_Max` (at maximum speed), matching to **1.6 %** for pitch and
|
||||
**5.6 %** for roll. These ratios are **clock-independent**, so they stand regardless
|
||||
of the conversion. Pitch's ratio also re-confirms `ly+` = **pitch-minus** (1.88)
|
||||
rather than plus (2.14), reproducing the earlier run's 1.82.
|
||||
|
||||
**The absolute scale does not match, and not in the same direction:**
|
||||
|
||||
```
|
||||
pitch measured / predicted = 1.15, 1.21, 1.07 (consistently OVER)
|
||||
roll measured / predicted = 0.62, 0.64, 0.60 (consistently UNDER, and very flat)
|
||||
```
|
||||
|
||||
**A clock error cannot explain this** — it would scale both axes the same way, and
|
||||
these go opposite. So the discrepancy is per-axis. Two candidates, neither measured:
|
||||
|
||||
- a **1-second burst may not complete the angular-acceleration ramp**, which would
|
||||
*under*-read — that fits roll's remarkably constant 0.62 but not pitch's excess;
|
||||
- a **per-axis multiplier** applied to the cap that is not yet identified.
|
||||
|
||||
Recorded as an open question. The next measurement that would separate them is cheap:
|
||||
**1 s versus 3 s bursts at the same throttle** — if the ramp is the cause, the longer
|
||||
burst reads higher. Data:
|
||||
[pitch](captures/pitch-burst-final.csv) · [roll](captures/roll-burst-final.csv).
|
||||
|
||||
## Note on the WEAK-pin guard
|
||||
|
||||
The roll run refused to start: after the pitch bursts the craft was no longer level,
|
||||
so the row pin came back **WEAK** (margin 0.109). That refusal is correct for pitch
|
||||
and yaw — but **roll is immune to the up/right labelling**, which is exactly what the
|
||||
guard's own message says, so `ALLOW_WEAK_PIN=1` is the documented and legitimate
|
||||
override there. It is worth having a guard that states its own exception.
|
||||
|
||||
|
||||
## The ramp test is inconclusive — and says what the next tool must be 🟡
|
||||
|
||||
To separate "a 1 s burst never reaches the steady rate" from "a per-axis multiplier",
|
||||
the cheapest test measures *inside* one hold: successive 0.25 s windows of a single
|
||||
3 s press, so speed, attitude and starting conditions are held constant by
|
||||
construction. Two repeats, roll, at cruise:
|
||||
|
||||
```
|
||||
rep0 rate 16 59 325 209 130 322 238 110 310 151 183
|
||||
speed 390 234 574 341 223 526 387 191 511 247 312
|
||||
rep1 rate 52 181 231 246 236 169 368 195 181 255 182
|
||||
speed 447 372 378 418 385 233 590 307 312 419 306
|
||||
```
|
||||
|
||||
**Not usable.** At 0.25 s the windows do not contain enough guest updates to average,
|
||||
so both channels swing by 3× window to window — the same aliasing that once
|
||||
manufactured a rate-vs-speed curve, reappearing at finer resolution. The first window
|
||||
is the lowest in *both* repeats, which is what a ramp would look like, but the
|
||||
sequence never plateaus, so the signal cannot be separated from the sampling.
|
||||
|
||||
**Widening the window does not rescue it**: 0.5 s windows average well enough (that is
|
||||
what the earlier sweep used), but a hold long enough to contain several of them bleeds
|
||||
speed, and speed is the very variable under test. The two effects are entangled at
|
||||
this observation rate.
|
||||
|
||||
**So this residual needs a different instrument, not another script.** Live-RAM polling
|
||||
samples an unsynchronised snapshot; what the question wants is the craft's angular
|
||||
velocity *as the guest computes it, once per frame*. That is a Canary-side hook —
|
||||
the same shape as the existing F10 ship-capture patch — and the rebuild toolchain
|
||||
already makes it cheap. Recorded as the recommendation rather than attempted as a
|
||||
seventh variation of the same measurement.
|
||||
Data: [`captures/roll-ramp-inconclusive.csv`](captures/roll-ramp-inconclusive.csv).
|
||||
|
||||
182
docs/re/structures/achievements.md
Normal file
@@ -0,0 +1,182 @@
|
||||
# Achievements — the 24-entry table, and where the earned state comes from
|
||||
|
||||
**Conf.:** ✅ for the table and for the XAM read path; 🟡 for the requirement list's
|
||||
bit numbering.
|
||||
**Spec:** [`tools/xach_dump.py`](../../../tools/xach_dump.py) ·
|
||||
[`examples/achievements_map.rs`](../../../crates/sylpheed-formats/examples/achievements_map.rs)
|
||||
**Evidence:** [`captures/achievements-xach.txt`](../captures/achievements-xach.txt)
|
||||
|
||||
> **Retracted here, and it matters:** earlier drafts of this file claimed the
|
||||
> challenge missions gate on this achievement mask. **They do not.** That gate reads a
|
||||
> *cleared-stage* bitmask whose sole writer is `GamePart_StageClear` — see
|
||||
> [challenge-mission-gate §5.2](../challenge-mission-gate.md). The link was invented
|
||||
> out of a numeric coincidence: `ACHIEVEMENTS_REQUIREMENTS` has 24 entries and the
|
||||
> challenge gate splits its two words at 24, for entirely unrelated reasons (24 is
|
||||
> also the first challenge stage's id). Everything below is what survived checking the
|
||||
> writer.
|
||||
|
||||
The title reads its earned-achievement state from the console, and separately carries
|
||||
a disc table of what each achievement requires. Both are useful to the
|
||||
reimplementation on their own terms.
|
||||
|
||||
## 1. The table: 24 achievements, 1000G ✅
|
||||
|
||||
The XEX embeds an SPA/XDBF resource. `XACH` sits at `.pe` offset **`0x8FBCBC`**:
|
||||
|
||||
| field | type | notes |
|
||||
|---|---|---|
|
||||
| magic | `char[4]` | `XACH` |
|
||||
| version | `u32` | 1 |
|
||||
| size | `u32` | 874 |
|
||||
| count | `u16` | **24** |
|
||||
|
||||
then `count` records of **36 bytes**:
|
||||
|
||||
| off | type | field |
|
||||
|---|---|---|
|
||||
| 0 | `u16` | achievement id (1…24) |
|
||||
| 2 | `u16` | name string id |
|
||||
| 4 | `u16` | unlocked-description string id |
|
||||
| 6 | `u16` | locked-description string id |
|
||||
| 8 | `u32` | image id |
|
||||
| 12 | `u16` | gamerscore |
|
||||
| 14 | `u16` | pad (0) |
|
||||
| 16 | `u32` | flags (`0x0C` throughout) |
|
||||
| 20 | — | 16 bytes of zeroes |
|
||||
|
||||
Strings come from one `XSTR` section per language (7 present); each is
|
||||
`magic[4] "XSTR"`, `version u32`, `size u32`, `count u16`, then entries of
|
||||
`id u16, len u16, len bytes ASCII`. English is table **#5**.
|
||||
|
||||
**The stride and field offsets are self-checked**: the 24 gamerscores sum to
|
||||
**exactly 1000**, the retail total. A wrong stride does not add up to a round 1000.
|
||||
|
||||
Ids run `1…24` with no gaps.
|
||||
|
||||
> **Correction 1 of 2 (same day).** This file first said "bit `n` ↔ achievement id `n+1`",
|
||||
> i.e. 0-based bits. That was wrong-headed: the only place the image is *observed*
|
||||
> turning an achievement into a bit does `1 << dwId` with `dwId` 1-based (§3.1), so
|
||||
> the bit index is the **id itself** and bit 0 is unused. The 0-based reading came
|
||||
> from assuming the config list's position was the bit, which §3 no longer supports.
|
||||
|
||||
## 2. The 24 achievements ✅
|
||||
|
||||
Ids run `1…24` with no gaps. Where the image turns one into a bit it uses `1 << dwId`
|
||||
(§3.1), so the id is the bit and bit 0 is unused — but note that this mask is *not*
|
||||
the challenge gate's (§4).
|
||||
|
||||
| id | G | name |
|
||||
|---|---|---|
|
||||
| 1 | 20 | Space Combat Award |
|
||||
| 2 | 20 | Schlos Base Defense Award |
|
||||
| 3 | 20 | Aegis of the People Medal |
|
||||
| 4 | 20 | TCAF Luna Medal |
|
||||
| 5 | 40 | TCAF Mars Medal |
|
||||
| 6 | 50 | Soldier's Charm Amulet |
|
||||
| 7 | 20 | White Griffons Patch |
|
||||
| 8 | 30 | TCAF Jupiter Medal |
|
||||
| 9 | 40 | Furious Pursuit Badge |
|
||||
| 10 | 30 | Solo Aerospace Combat Award |
|
||||
| 11 | 30 | Operation Nebula Blaze Award |
|
||||
| 12 | 40 | Guilty Roses Patch |
|
||||
| 13 | 30 | Super Battleship Slayer Patch |
|
||||
| 14 | 40 | TCAF Terra Medal |
|
||||
| 15 | 40 | Hellfires Patch |
|
||||
| 16 | 50 | Night Ravens Patch |
|
||||
| 17 | 40 | Solar System Defense Award |
|
||||
| 18 | 40 | Special Operations Medal |
|
||||
| 19 | 30 | 1,000 Units Destroyed Medal |
|
||||
| 20 | 70 | 10,000 Units Destroyed Medal |
|
||||
| 21 | 50 | Ship Hunter Award |
|
||||
| 22 | 70 | Gigaton Club Patch |
|
||||
| 23 | 80 | Weapon Lord Patch |
|
||||
| 24 | 100 | TCAF Pilot's Commendation |
|
||||
|
||||
## 3. The game evaluates them itself, from a disc config ✅ (numbering 🟡)
|
||||
|
||||
`GamePart_Debriefing` (`0x8218CF38`–`0x82191B18`, bounded by the factory creator
|
||||
thunks either side) runs `sub_8218F9A8` after a mission:
|
||||
|
||||
```asm
|
||||
for i = 0, 1, 2, …:
|
||||
node = child(ACHIEVEMENTS_REQUIREMENTS, i, &x) ; bl 0x82448338
|
||||
bit = 1 << x
|
||||
if (this+208 & bit) continue ; already awarded
|
||||
if (evaluate(this, node)) ; bl 0x8218FAB0
|
||||
this+208 |= bit
|
||||
```
|
||||
|
||||
The list is on disc — `tables.pak` entry **#16**, schema `744c0519`, the
|
||||
`GP_DEBRIEFING_PILOTLOG.pak+eng` config — and its entries are literally
|
||||
`ACHIEVEMENT01` … `ACHIEVEMENT24`, in order.
|
||||
|
||||
⚠️ **`x` is not proven to be the loop index.** `0x82448338` walks a 12-byte child
|
||||
array and writes the child entry's **first word** to the out-parameter; whether that
|
||||
word is the ordinal, an explicit id, or a name hash is not pinned. Since §3.1 shows
|
||||
the image elsewhere shifting by a **1-based achievement id**, `x` is most likely the
|
||||
id too — but this file previously stated "the list index is the bit index" as fact,
|
||||
and that is withdrawn.
|
||||
|
||||
### 3.1 The earned state comes from XAM — the console profile, not the save ✅
|
||||
|
||||
The same class enumerates a buffer of **36-byte** records (`0x8218F888`):
|
||||
|
||||
- the record count is a byte count divided by 36 — via the multiply-high magic
|
||||
**`0x38E38E39`** plus `srawi 3`, which is the standard unsigned `/36` sequence, so
|
||||
the stride is confirmed by the arithmetic and not just by inspection;
|
||||
- field **`+0`** is used as a shift amount (`1 << id`), field **`+32`** is tested for
|
||||
bit **`0x00020000`**;
|
||||
- the buffer is produced asynchronously: a handle at `this+100` is waited on
|
||||
(`0x824AA330(h, -1)`) then closed (`0x824AA3E0`).
|
||||
|
||||
That is exactly the XDK's `XACHIEVEMENT_DETAILS` —
|
||||
`{ DWORD dwId; PWSTR pwszLabel, pwszDescription, pwszUnachieved; DWORD dwImageId,
|
||||
dwCred; FILETIME ftAchieved; DWORD dwFlags; }` = 36 bytes, with
|
||||
`XACHIEVEMENT_DETAILS_ACHIEVED == 0x00020000` — fetched through the
|
||||
`XamUserCreateAchievementEnumerator` / `XEnumerate` pattern.
|
||||
|
||||
**So the title does not persist earned achievements itself: it asks the console.**
|
||||
The masks it builds (`this+208`, `+736`, `+740`) are `1 << dwId`, i.e. **bit = the
|
||||
1-based achievement id**, bit 0 unused.
|
||||
|
||||
For anything that wants to unlock achievement-gated content, the lever is therefore
|
||||
the **emulator's profile achievement data**, not the 545-byte savegame.
|
||||
|
||||
The record also carries each requirement's **type and parameters**:
|
||||
`StageClear`(`Stage`), `MissionObjective`, `Item`, `Rank` (`S`),
|
||||
`ShootDownAircrafts`(`Count` 1000 / 10000), `ShootDownShips`(`Count` 100),
|
||||
`ShootDownWeight`(`MegaTons`), `GetAllWeapons`, `GetAllAchievements`.
|
||||
|
||||
**That set independently confirms the list's ORDER** (not the bit numbering): the last
|
||||
five types line up with ids 19–24 exactly as the XACH table names them — 1 000 units,
|
||||
10 000 units, 100 warships, one gigaton, all Delta Saber equipment, and finally the
|
||||
meta `GetAllAchievements` → id 24, `TCAF Pilot's Commendation`, the 100G one. So the
|
||||
config list is in achievement-id order; what it does **not** settle is whether the bit
|
||||
the code shifts by is that position or the id (see the ⚠️ above).
|
||||
|
||||
⚠️ `GetAllAchievements` and `GetAllWeapons` **look like debug cheats and are not** —
|
||||
they are requirement *types* in the achievement table. Worth stating because the
|
||||
strings sit next to genuinely debug-looking ones in the image.
|
||||
|
||||
## 4. What this is *not*: the challenge-mission gate ✅ (refuted)
|
||||
|
||||
`GamePart_ChallengeMission` gates each mission on a bit of a word at singleton `+80`.
|
||||
That word has exactly one writer in the image — `0x821C1820` in `GamePart_StageClear`
|
||||
— and it sets `1 << (this+84)` where `this+84` is the **stage number** (it also
|
||||
indexes a 20-byte per-stage record array and the debriefing config's `STAGE` sprite
|
||||
list). So that word is a **cleared-stage** mask, not this achievement mask, and the
|
||||
`< 24` / `>= 24` split is the disc's stage numbering (story 1–16 and tutorial 18–23
|
||||
below 24; challenge 24–29 above), not a count of achievements.
|
||||
|
||||
Nothing observed copies the Debriefing's achievement masks (`+208`, `+736`, `+740`)
|
||||
into that word, which is the check that should have been done before the claim was
|
||||
made rather than after.
|
||||
|
||||
## Reproduce
|
||||
|
||||
```bash
|
||||
python3 tools/xach_dump.py "<disc>/…/Project Sylpheed ….pe"
|
||||
cargo run --release -q -p sylpheed-formats --example achievements_map -- <disc-root>
|
||||
python3 xenia-rs/zq.py dis 0x8218f9a8 0x8218fa60 # the requirement walk
|
||||
python3 xenia-rs/zq.py dis 0x8218f888 0x8218f990 # the XACHIEVEMENT_DETAILS scan
|
||||
```
|
||||
186
tools/re-capture/axis_probe.py
Executable file
@@ -0,0 +1,186 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Which pad input drives which rotation axis — all three measured at once.
|
||||
|
||||
The control mapping was left with **yaw ❔ "no input found"**: `AV_Yaw_*` exists in
|
||||
the definitions (45/25) but no stick appeared to yaw. That conclusion came from
|
||||
probes that measured one axis at a time using a non-forward matrix row, which is
|
||||
exactly the flaw that made roll and pitch produce identical numbers — a row like
|
||||
that moves under *any* rotation, so "this input yaws" and "this input pitches"
|
||||
cannot be told apart by it.
|
||||
|
||||
So decompose properly. For each small step, with the previous frame's orthonormal
|
||||
rows `(f, u, w)` = forward, up, right:
|
||||
|
||||
roll = atan2(u_new . w_old, u_new . u_old) rotation of UP about forward
|
||||
yaw = atan2(f_new . w_old, f_new . f_old) rotation of FORWARD about up
|
||||
pitch = atan2(f_new . u_old, f_new . f_old) rotation of FORWARD about right
|
||||
|
||||
Each drops the components the other two produce, so one held input yields three
|
||||
numbers and the axis it actually drives is whichever is large.
|
||||
|
||||
Every candidate is held with the file pad, which writes the WHOLE state at once —
|
||||
so `rx` really means right-stick-X with every other channel exactly zero, which a
|
||||
quantising virtual stick could not promise.
|
||||
|
||||
⚠️ TWO THINGS THIS PROBE DOES NOT YET HANDLE, both learned the hard way:
|
||||
|
||||
1. **The craft can DIE mid-run.** Holding a stick at full deflection spins the ship
|
||||
at 150-200 deg/s in a live combat mission; two minutes of that and the first run
|
||||
ended on GAME OVER. Inputs measured after that point are meaningless, and the
|
||||
symptom is only "0 player candidates, best displacement 0.000" *afterwards* --
|
||||
the earlier rows still look fine. Check the player is alive BETWEEN inputs, or
|
||||
probe somewhere nothing shoots back.
|
||||
2. **Which row is UP and which is RIGHT is not pinned.** `entities2` measures that
|
||||
row 2 is forward (against velocity), but rows 0 and 1 are assigned by the D3D
|
||||
convention, not by evidence. Roll is unaffected -- rotation of either non-forward
|
||||
row within their shared plane is roll either way -- but YAW and PITCH swap if the
|
||||
convention is wrong, so this probe's last two columns are named on an assumption.
|
||||
|
||||
Usage: axis_probe.py <out.csv> [dwell_s]
|
||||
"""
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import struct
|
||||
import sys
|
||||
import time
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import speed_law # noqa: E402
|
||||
|
||||
PAD_FILE = os.environ.get("XENIA_PAD_FILE", "/tmp/xenia_pad.txt")
|
||||
|
||||
# Candidates. Sticks at full deflection, shoulders as buttons. Triggers are the
|
||||
# throttle (already settled) and are left out.
|
||||
# Order matters: the craft can be shot down mid-run, so the UNKNOWN inputs go
|
||||
# first while it is healthy and the already-established ones (lx = roll) go last
|
||||
# as controls. A row measured after death looks like a clean zero.
|
||||
CANDIDATES = [
|
||||
("rx+", {"rx": 32767}),
|
||||
("ry+", {"ry": 32767}),
|
||||
("LB", {"press": "LB"}),
|
||||
("RB", {"press": "RB"}),
|
||||
("lx+", {"lx": 32767}),
|
||||
("ly+", {"ly": 32767}),
|
||||
]
|
||||
|
||||
|
||||
def pad_state(**kw):
|
||||
parts = [f"{k}={v}" for k, v in kw.items() if v is not None]
|
||||
tmp = PAD_FILE + ".tmp"
|
||||
with open(tmp, "w") as f:
|
||||
f.write(" ".join(parts))
|
||||
os.replace(tmp, PAD_FILE)
|
||||
|
||||
|
||||
def rows_at(fd, off, cfg):
|
||||
base = off + cfg["rot_delta"]
|
||||
out = []
|
||||
for r in range(3):
|
||||
v = struct.unpack(">3f", os.pread(fd, 12, base + r * cfg["rot_stride"]))
|
||||
n = math.sqrt(sum(c * c for c in v)) or 1.0
|
||||
out.append(tuple(c / n for c in v))
|
||||
return out
|
||||
|
||||
|
||||
def dot(a, b):
|
||||
return sum(a[i] * b[i] for i in range(3))
|
||||
|
||||
|
||||
def alive(w, off, cfg, secs=1.0):
|
||||
"""Is the craft still flying? A destroyed craft stops moving, and every
|
||||
subsequent input then measures a clean, meaningless zero — the failure mode
|
||||
that invalidated this probe's first run (it ended on GAME OVER and only said
|
||||
so afterwards)."""
|
||||
p0 = struct.unpack(">3f", os.pread(w.fd, 12, off))
|
||||
time.sleep(secs)
|
||||
p1 = struct.unpack(">3f", os.pread(w.fd, 12, off))
|
||||
return math.dist(p0, p1) > 1.0
|
||||
|
||||
|
||||
def pin_rows(w, off, cfg):
|
||||
"""Which non-forward row is UP and which is RIGHT?
|
||||
|
||||
`entities2` measures row 2 = forward against the velocity vector, but the other
|
||||
two are assigned by the D3D convention rather than evidence — and yaw and pitch
|
||||
SWAP if that is wrong, so naming them without this test is a guess.
|
||||
|
||||
Discriminator: in level flight the craft's up-vector points along world +Y and
|
||||
its right-vector lies near the horizontal plane. Sample at neutral and compare
|
||||
|world-Y| across the rows."""
|
||||
f_i = cfg["fwd_row"]
|
||||
acc = [0.0, 0.0, 0.0]
|
||||
n = 0
|
||||
for _ in range(20):
|
||||
time.sleep(0.05)
|
||||
rs = rows_at(w.fd, off, cfg)
|
||||
for r in range(3):
|
||||
acc[r] += rs[r][1] # world Y component
|
||||
n += 1
|
||||
ys = [a / n for a in acc]
|
||||
cand = [r for r in (0, 1, 2) if r != f_i]
|
||||
up_i = max(cand, key=lambda r: abs(ys[r]))
|
||||
right_i = [r for r in cand if r != up_i][0]
|
||||
print(f"# row world-Y means: {[round(y,3) for y in ys]} (forward = row {f_i})")
|
||||
margin = abs(ys[up_i]) - abs(ys[right_i])
|
||||
ok = margin > 0.3
|
||||
print(f"# -> up = row {up_i}, right = row {right_i} margin {margin:.3f} "
|
||||
f"{'CONFIDENT' if ok else 'WEAK — craft is not level'}")
|
||||
if not ok and os.environ.get("ALLOW_WEAK_PIN") != "1":
|
||||
sys.exit("refusing to measure with a WEAK row pin — fly level first, or set "
|
||||
"ALLOW_WEAK_PIN=1 if the caller genuinely does not care "
|
||||
"(roll is immune to the labelling; pitch and yaw are not)")
|
||||
return up_i, right_i
|
||||
|
||||
|
||||
def main():
|
||||
out_csv = sys.argv[1]
|
||||
dwell = float(sys.argv[2]) if len(sys.argv) > 2 else 5.0
|
||||
cfg = json.load(open("/tmp/nav-live.json"))
|
||||
w, off, nm = speed_law.find_player()
|
||||
if not w:
|
||||
sys.exit("player entity not found")
|
||||
print(f"# locked on {nm}")
|
||||
f_i = cfg["fwd_row"]
|
||||
if not alive(w, off, cfg):
|
||||
sys.exit("craft is not moving at the start — not in flight, or already dead")
|
||||
u_i, w_i = pin_rows(w, off, cfg)
|
||||
|
||||
rows = []
|
||||
print(f"# {'input':<6} {'roll':>9} {'yaw':>9} {'pitch':>9} (deg over the dwell)")
|
||||
for label, state in CANDIDATES:
|
||||
pad_state() # neutral
|
||||
time.sleep(2.0)
|
||||
prev = rows_at(w.fd, off, cfg)
|
||||
pad_state(**state)
|
||||
t0 = time.time()
|
||||
sw = [0.0, 0.0, 0.0]
|
||||
while time.time() - t0 < dwell:
|
||||
time.sleep(0.05)
|
||||
cur = rows_at(w.fd, off, cfg)
|
||||
roll = math.degrees(math.atan2(dot(cur[u_i], prev[w_i]), dot(cur[u_i], prev[u_i])))
|
||||
yaw = math.degrees(math.atan2(dot(cur[f_i], prev[w_i]), dot(cur[f_i], prev[f_i])))
|
||||
pitch = math.degrees(math.atan2(dot(cur[f_i], prev[u_i]), dot(cur[f_i], prev[f_i])))
|
||||
for k, v in enumerate((roll, yaw, pitch)):
|
||||
sw[k] += abs(v)
|
||||
prev = cur
|
||||
pad_state()
|
||||
wall = time.time() - t0
|
||||
if not alive(w, off, cfg):
|
||||
print(f"# CRAFT STOPPED MOVING after {label} — everything from here is "
|
||||
f"meaningless, aborting rather than reporting zeros")
|
||||
break
|
||||
rate = [v / wall for v in sw]
|
||||
rows.append((label, *[round(v, 2) for v in rate]))
|
||||
print(f"# {label:<6} {rate[0]:9.1f} {rate[1]:9.1f} {rate[2]:9.1f} deg/wall-s")
|
||||
time.sleep(1.5)
|
||||
|
||||
with open(out_csv, "w") as f:
|
||||
f.write("input,roll_deg_s,yaw_deg_s,pitch_deg_s\n")
|
||||
for r in rows:
|
||||
f.write(",".join(str(x) for x in r) + "\n")
|
||||
print(f"# wrote {out_csv}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
45
tools/re-capture/bin/screenshot
Executable file
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env bash
|
||||
# Grab the GAME's image, not the emulator window.
|
||||
#
|
||||
# Every pixel oracle in this toolkit (at_menu, at_title, wait_not_black, the
|
||||
# HUD readers) was measured against a bare 1280x720 game image. xenia's window
|
||||
# is a GTK window with a menu bar, so the game surface actually starts ~25 px
|
||||
# down — and when it does, all of those constants read the wrong pixels. That
|
||||
# is not a hypothetical: a run sat on a plainly visible MAIN MENU for 300 s
|
||||
# reporting "no main menu", and a later one missed the title screen entirely and
|
||||
# let the attract movie loop for ten minutes.
|
||||
#
|
||||
# The offset is not guessed: it is the xenia window's height minus 720. So this
|
||||
# works whether or not the menu bar is there, and needs no per-display constant.
|
||||
# Put this directory first on PATH and existing scripts keep working unchanged.
|
||||
set -u
|
||||
REAL=/usr/local/bin/screenshot
|
||||
OUT="${1:-}"
|
||||
if [ -z "$OUT" ]; then
|
||||
dir="${HOME:-/tmp}/shots"; mkdir -p "$dir"
|
||||
n_file="$dir/.counter"; n=$(( $(cat "$n_file" 2>/dev/null || echo 0) + 1 )); echo "$n" > "$n_file"
|
||||
OUT="$dir/shot-$(printf '%04d' "$n").png"
|
||||
fi
|
||||
|
||||
RAW="$(mktemp /tmp/shot-raw-XXXXXX.png)"
|
||||
trap 'rm -f "$RAW"' EXIT
|
||||
"$REAL" "$RAW" >/dev/null || exit 1
|
||||
|
||||
# "…": ("xenia_canary" "Xenia_canary") 1280x745+0+0 +0+0
|
||||
geo=$(xwininfo -root -children 2>/dev/null \
|
||||
| grep '"xenia_canary"' | grep -oE '[0-9]+x[0-9]+\+-?[0-9]+\+-?[0-9]+' | head -1)
|
||||
if [ -z "$geo" ]; then # no window found — hand back the raw grab
|
||||
cp "$RAW" "$OUT"; echo "$OUT"; exit 0
|
||||
fi
|
||||
W=${geo%%x*}; rest=${geo#*x}; H=${rest%%+*}; rest=${rest#*+}; X=${rest%%+*}; Y=${rest#*+}
|
||||
OFF=$(( H - 720 )); [ "$OFF" -lt 0 ] && OFF=0
|
||||
TOP=$(( Y + OFF ))
|
||||
[ "$TOP" -lt 0 ] && TOP=0
|
||||
if [ "$OFF" -eq 0 ] && [ "$TOP" -eq 0 ] && [ "$X" -eq 0 ]; then
|
||||
cp "$RAW" "$OUT"; echo "$OUT"; exit 0
|
||||
fi
|
||||
# Height is whatever is still on screen: a 1280x745 window on a 720-high root
|
||||
# loses its last rows, and every oracle point in use sits well above them.
|
||||
convert "$RAW" -crop "${W}x720+${X}+${TOP}" +repage "$OUT" 2>/dev/null \
|
||||
|| cp "$RAW" "$OUT"
|
||||
echo "$OUT"
|
||||
113
tools/re-capture/burst_probe.py
Executable file
@@ -0,0 +1,113 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Angular rate at a KNOWN, SETTLED speed — short bursts instead of one sweep.
|
||||
|
||||
Why not a sweep: turning bleeds speed hard (1 193 -> 589 in 8 s at full throttle),
|
||||
so a long hold averages a rate over a moving speed and lands between two different
|
||||
caps. A sweep *driven* by that bleed also cannot dwell at either extreme, which is
|
||||
exactly where a speed-dependent law is most testable.
|
||||
|
||||
So: settle the throttle, measure the settled speed, then pitch for only ~1 second.
|
||||
The speed barely moves inside a burst, so each burst is one honest `(speed, rate)`
|
||||
point. Three throttle settings give three clean points at known speeds.
|
||||
|
||||
Row pinning is done ONCE while the craft is still level — the pin decides which
|
||||
matrix ROW is up and which is right, which is a property of the layout, not of the
|
||||
current attitude, so it stays valid after the craft has been thrown around.
|
||||
|
||||
Accumulation is windowed over the whole burst (never per-read): polling is faster
|
||||
than the guest updates these fields, so a per-read delta is either zero or a whole
|
||||
frame divided by a fraction of one — that aliasing once manufactured an entire
|
||||
rate-vs-speed curve.
|
||||
|
||||
Usage: burst_probe.py <roll|pitch> <out.csv> [burst_s] [repeats]
|
||||
"""
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import struct
|
||||
import sys
|
||||
import time
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import speed_law # noqa: E402
|
||||
from axis_probe import alive, pad_state, pin_rows, rows_at, dot # noqa: E402
|
||||
|
||||
DRIVER = {"roll": {"lx": 32767}, "pitch": {"ly": 32767}}
|
||||
THROTTLES = [("min", {"lt": 255}), ("cruise", {}), ("max", {"rt": 255})]
|
||||
|
||||
|
||||
def pos_at(fd, off):
|
||||
return struct.unpack(">3f", os.pread(fd, 12, off))
|
||||
|
||||
|
||||
def measure(w, off, cfg, axis, u_i, w_i, f_i, hold, secs):
|
||||
"""Accumulate path length and swept angle over one interval."""
|
||||
prev_r, prev_p = rows_at(w.fd, off, cfg), pos_at(w.fd, off)
|
||||
t0 = time.time()
|
||||
path = swept = 0.0
|
||||
while time.time() - t0 < secs:
|
||||
time.sleep(0.02)
|
||||
cur_r, cur_p = rows_at(w.fd, off, cfg), pos_at(w.fd, off)
|
||||
path += math.dist(cur_p, prev_p)
|
||||
if axis == "roll":
|
||||
d = math.atan2(dot(cur_r[u_i], prev_r[w_i]), dot(cur_r[u_i], prev_r[u_i]))
|
||||
else:
|
||||
d = math.atan2(dot(cur_r[f_i], prev_r[u_i]), dot(cur_r[f_i], prev_r[f_i]))
|
||||
swept += abs(math.degrees(d))
|
||||
prev_r, prev_p = cur_r, cur_p
|
||||
span = time.time() - t0
|
||||
return path / span, swept / span
|
||||
|
||||
|
||||
def main():
|
||||
axis, out_csv = sys.argv[1], sys.argv[2]
|
||||
burst = float(sys.argv[3]) if len(sys.argv) > 3 else 1.0
|
||||
reps = int(sys.argv[4]) if len(sys.argv) > 4 else 3
|
||||
cfg = json.load(open("/tmp/nav-live.json"))
|
||||
w, off, nm = speed_law.find_player()
|
||||
if not w:
|
||||
sys.exit("player entity not found")
|
||||
f_i = cfg["fwd_row"]
|
||||
if not alive(w, off, cfg):
|
||||
sys.exit("craft is not moving")
|
||||
u_i, w_i = pin_rows(w, off, cfg) # once, while level
|
||||
print(f"# locked on {nm}, {axis} in {burst}s bursts x{reps}")
|
||||
|
||||
# ⚠️ Gap in the first run of this probe: it did NOT bracket the HUD clock, so
|
||||
# the absolute deg/GAME-second could not be computed and only the (clock-free)
|
||||
# min:max ratio was usable. Screenshot the HUD at the start and end.
|
||||
import subprocess
|
||||
subprocess.run(["screenshot", "/sylph-home/re/shots/burst_clock_a.png"],
|
||||
capture_output=True)
|
||||
|
||||
rows = []
|
||||
for tname, tstate in THROTTLES:
|
||||
for rep in range(reps):
|
||||
pad_state(**tstate)
|
||||
time.sleep(6.0) # settle the throttle, wings level-ish
|
||||
settled, _ = measure(w, off, cfg, axis, u_i, w_i, f_i, None, 1.0)
|
||||
pad_state(**tstate, **DRIVER[axis])
|
||||
bspeed, rate = measure(w, off, cfg, axis, u_i, w_i, f_i, None, burst)
|
||||
pad_state(**tstate)
|
||||
rows.append((tname, rep, round(settled, 1), round(bspeed, 1), round(rate, 2)))
|
||||
print(f"# {tname:<6} rep{rep} settled {settled:7.1f} "
|
||||
f"during {bspeed:7.1f} rate {rate:6.1f} deg/wall-s")
|
||||
if not alive(w, off, cfg):
|
||||
print("# CRAFT STOPPED MOVING — aborting")
|
||||
pad_state()
|
||||
break
|
||||
else:
|
||||
continue
|
||||
break
|
||||
pad_state()
|
||||
subprocess.run(["screenshot", "/sylph-home/re/shots/burst_clock_b.png"],
|
||||
capture_output=True)
|
||||
with open(out_csv, "w") as f:
|
||||
f.write("throttle,rep,settled_speed,burst_speed,rate_deg_per_wall_s\n")
|
||||
for r in rows:
|
||||
f.write(",".join(str(x) for x in r) + "\n")
|
||||
print(f"# wrote {out_csv}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
137
tools/re-capture/challenge_probe.sh
Executable file
@@ -0,0 +1,137 @@
|
||||
#!/usr/bin/env bash
|
||||
# Probe the challenge-mission gate on the running game.
|
||||
#
|
||||
# The gate (docs/re/challenge-mission-gate.md): GamePart_ChallengeMission tests a
|
||||
# CLEARED-STAGE bitmask on a static singleton at guest 0x828F4070 —
|
||||
# word A 0x828F40C0 bit = stage id, for ids < 24 (story 1-16, tutorial 18-23)
|
||||
# word B 0x828F4814 bit = stage id - 24 (challenge 24-29)
|
||||
# so setting every bit should make all six challenge missions available without
|
||||
# playing the campaign.
|
||||
#
|
||||
# POKE=1 (default) sets both words; POKE=0 runs the identical navigation without
|
||||
# touching them. Run it BOTH ways: the first poked run hit a Xenia heap failure on
|
||||
# the way into MISSION SELECT, and only the control says whether that failure has
|
||||
# anything to do with the poke.
|
||||
#
|
||||
# Runs as ONE blocking foreground call on purpose: setsid'd processes are reaped
|
||||
# at turn boundaries, so a session split across calls loses its emulator.
|
||||
#
|
||||
# Usage: [POKE=0|1] [TAG=name] challenge_probe.sh [boot_timeout_s]
|
||||
set -u
|
||||
export HOME=/sylph-home/re
|
||||
export DISPLAY=:99
|
||||
export SDL_AUDIODRIVER=dummy
|
||||
export XENIA_PAD_FILE=/tmp/xenia_pad.txt
|
||||
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||
pad() { python3 "$HERE/pad.py" "$@"; }
|
||||
poke() { python3 "$HERE/gpoke.py" "$@"; }
|
||||
SHOTS="$HOME/shots"
|
||||
BOOT_TIMEOUT="${1:-400}"
|
||||
POKE="${POKE:-1}"
|
||||
POKE_A="${POKE_A:-0x0001FFFE}" # stages 1-16 cleared
|
||||
POKE_B="${POKE_B:-0x0000003F}" # challenge stages 24-29 cleared
|
||||
TAG="${TAG:-$([ "$POKE" = 1 ] && echo poked || echo control)}"
|
||||
mkdir -p "$SHOTS"
|
||||
|
||||
say() { echo "[$(date +%H:%M:%S)] $*"; }
|
||||
shot() { screenshot "$SHOTS/chal-$TAG-$1.png" >/dev/null 2>&1; }
|
||||
|
||||
# --- clean slate -------------------------------------------------------------
|
||||
pkill -9 -x xenia_canary 2>/dev/null
|
||||
sleep 1
|
||||
rm -f /dev/shm/xenia_* 2>/dev/null
|
||||
: > "$XENIA_PAD_FILE"
|
||||
LOG="$HOME/canary.stdout"
|
||||
|
||||
# --- launch ------------------------------------------------------------------
|
||||
say "launching canary (lavapipe, file pad) — POKE=$POKE tag=$TAG"
|
||||
run-canary --audio --apu=sdl --log_mask=13 \
|
||||
--logged_profile_slot_0_xuid=E0300000EFBEA3D4 \
|
||||
--hid=file --pad_file="$XENIA_PAD_FILE" &
|
||||
|
||||
# Xvfb keeps the LAST instance's framebuffer until the new one draws, so a
|
||||
# screenshot taken seconds after launch shows the PREVIOUS run's screen. That is
|
||||
# how a control run once reported "MAIN MENU reached after 1s" against a menu
|
||||
# belonging to a process that no longer existed. Blank the root, and refuse to
|
||||
# believe any screen oracle until the emulator has had time to draw its own.
|
||||
xsetroot -solid black 2>/dev/null || true
|
||||
LAUNCH_GRACE=40
|
||||
|
||||
# --- reach the MAIN MENU, verifying instead of pressing blind ----------------
|
||||
# Two oracles, both sampled from real screenshots:
|
||||
# title screen : green "PRESS (A) BUTTON" glyph at (625,618)
|
||||
# main menu : the "NEW GAME" text at (648,221) is pure white (254,254,254),
|
||||
# where the title has the yellow planet (208,189,88)
|
||||
# The first control run pressed A while the ATTRACT MOVIE happened to show a
|
||||
# greenish pixel at the title-glyph spot, then navigated a menu that was never
|
||||
# open and reported "0 failures" for a screen it never reached. Verify the menu.
|
||||
px() { convert /tmp/nav-probe.png -format \
|
||||
"%[fx:int(255*p{$1}.r)] %[fx:int(255*p{$1}.g)] %[fx:int(255*p{$1}.b)]" info: 2>/dev/null; }
|
||||
|
||||
# A screen is identified by a PATTERN of sampled points, never by one pixel. A
|
||||
# single "is (648,221) white?" test matched a white loading flash, and the run
|
||||
# then navigated a menu that was not on screen -- the same class of mistake as
|
||||
# trusting the stale framebuffer. Require the menu's contrast: white "NEW GAME"
|
||||
# text AND the dark blue panel behind it.
|
||||
at_menu() {
|
||||
screenshot /tmp/nav-probe.png >/dev/null 2>&1 || return 1
|
||||
read -r r g b < <(px "648,221") # NEW GAME text: white
|
||||
[ -n "${r:-}" ] || return 1
|
||||
[ "$r" -gt 230 ] && [ "$g" -gt 230 ] && [ "$b" -gt 230 ] || return 1
|
||||
read -r r2 g2 b2 < <(px "560,300") # panel left of LOAD GAME: dark blue
|
||||
[ -n "${r2:-}" ] || return 1
|
||||
[ "$r2" -lt 120 ] && [ "$b2" -gt "$r2" ]
|
||||
}
|
||||
at_title() {
|
||||
screenshot /tmp/nav-probe.png >/dev/null 2>&1 || return 1
|
||||
read -r r g b < <(px "625,618")
|
||||
[ -n "${g:-}" ] && [ "$g" -gt 130 ] && [ $((g - r)) -gt 45 ] && [ $((g - b)) -gt 45 ]
|
||||
}
|
||||
|
||||
say "waiting for the main menu (up to ${BOOT_TIMEOUT}s)"
|
||||
MENU=0
|
||||
for i in $(seq 1 "$BOOT_TIMEOUT"); do
|
||||
if [ "$i" -lt "$LAUNCH_GRACE" ]; then sleep 1; continue; fi
|
||||
if at_menu && sleep 1 && at_menu; then say "MAIN MENU reached after ${i}s"; MENU=1; break; fi
|
||||
if at_title; then say " title visible — tapping A"; pad tap A 0.25; sleep 2; fi
|
||||
sleep 1
|
||||
done
|
||||
[ "$MENU" = 1 ] || { say "TIMEOUT: never reached the main menu"; shot 00-timeout; exit 1; }
|
||||
shot 01-mainmenu
|
||||
|
||||
say "gate words:"
|
||||
poke r32 0x828F40C0 1
|
||||
poke r32 0x828F4814 1
|
||||
|
||||
if [ "$POKE" = 1 ]; then
|
||||
# Default to REAL stage ids only. 0xFFFFFFFF claims stages that do not exist
|
||||
# (0, 17, and 24-31 in word A), and that run blew the guest heap:
|
||||
# MmAllocatePhysicalMemoryEx could not satisfy 128 MB and the guest threw.
|
||||
# Word A bits 1..16 = the story campaign; 18..23 would be the tutorials.
|
||||
say "poking word A = $POKE_A, word B = $POKE_B"
|
||||
poke w32 0x828F40C0 "$POKE_A"
|
||||
poke w32 0x828F4814 "$POKE_B"
|
||||
else
|
||||
say "control run — leaving the words untouched"
|
||||
fi
|
||||
|
||||
# --- main menu -> EXTRAS -----------------------------------------------------
|
||||
# NEW GAME / LOAD GAME / TUTORIAL / OPTIONS / EXTRAS
|
||||
for _ in 1 2 3 4; do pad dpad down 0.06; sleep 0.35; done
|
||||
sleep 0.5
|
||||
pad tap A 0.15
|
||||
sleep 3
|
||||
shot 02-extras
|
||||
|
||||
# --- EXTRAS -> MISSION SELECT ------------------------------------------------
|
||||
ALLOC_BEFORE=$(grep -c 'MmAllocatePhysicalMemoryEx: Allocation failed' "$LOG" 2>/dev/null | head -1)
|
||||
pad tap A 0.15
|
||||
sleep 4
|
||||
shot 03-missionselect
|
||||
ALLOC_AFTER=$(grep -c 'MmAllocatePhysicalMemoryEx: Allocation failed' "$LOG" 2>/dev/null | head -1)
|
||||
|
||||
say "MmAllocatePhysicalMemoryEx failures: before=$ALLOC_BEFORE after=$ALLOC_AFTER"
|
||||
say "guest C++ exceptions: $(grep -c 'Guest attempted to throw a C++ exception' "$LOG" 2>/dev/null | head -1)"
|
||||
say "shots: $SHOTS/chal-$TAG-*.png"
|
||||
say "done — emulator left running"
|
||||
74
tools/re-capture/diff_words.py
Executable file
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Find guest addresses holding one value in snapshot A and another in snapshot B.
|
||||
|
||||
The classic differential search, which is how you locate a field whose ADDRESS is
|
||||
unknown but whose VALUE is known at two moments. Used here for the GamePart slot:
|
||||
`GP_EXTRAS` = 5 on the extras screen, `GP_MISSION_SELECT` = 7 on the next one, so
|
||||
the slot is a word that reads 5 then 7.
|
||||
|
||||
Both snapshots are sparse copies of `/dev/shm/xenia_memory_*`, so the ~4.6 GB of
|
||||
holes cost nothing: walk A's allocated extents with SEEK_DATA and only look at B
|
||||
where A already matched.
|
||||
|
||||
diff_words.py <snapA> <valA> <snapB> <valB> [--near <va> <span>]
|
||||
"""
|
||||
import os
|
||||
import struct
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from gmem import off_to_vas # noqa: E402
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 5:
|
||||
print(__doc__)
|
||||
return 1
|
||||
pa, va_, pb, vb_ = sys.argv[1], int(sys.argv[2], 0), sys.argv[3], int(sys.argv[4], 0)
|
||||
pat_a = struct.pack(">I", va_)
|
||||
fa, fb = open(pa, "rb"), open(pb, "rb")
|
||||
size = min(os.path.getsize(pa), os.path.getsize(pb))
|
||||
|
||||
hits, scanned = [], 0
|
||||
off = 0
|
||||
while off < size:
|
||||
try:
|
||||
off = os.lseek(fa.fileno(), off, os.SEEK_DATA)
|
||||
except OSError:
|
||||
break
|
||||
if off >= size:
|
||||
break
|
||||
try:
|
||||
end = min(os.lseek(fa.fileno(), off, os.SEEK_HOLE), size)
|
||||
except OSError:
|
||||
end = size
|
||||
while off < end:
|
||||
n = min(1 << 22, end - off)
|
||||
fa.seek(off)
|
||||
buf = fa.read(n)
|
||||
scanned += len(buf)
|
||||
i = 0
|
||||
while True:
|
||||
i = buf.find(pat_a, i)
|
||||
if i < 0:
|
||||
break
|
||||
p = off + i
|
||||
if p % 4 == 0:
|
||||
fb.seek(p)
|
||||
if fb.read(4) == struct.pack(">I", vb_):
|
||||
hits.append(p)
|
||||
i += 1
|
||||
off += n
|
||||
|
||||
print(f"scanned {scanned/1e6:.0f} MB of allocated guest memory")
|
||||
print(f"addresses reading {va_} in A and {vb_} in B: {len(hits)}")
|
||||
for h in hits[:40]:
|
||||
vas = off_to_vas(h)
|
||||
print(f" file off {h:#012x} guest VA {[hex(v) for v in vas][:2]}")
|
||||
if len(hits) > 40:
|
||||
print(f" … and {len(hits)-40} more")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -145,7 +145,18 @@ def main():
|
||||
return
|
||||
|
||||
if cmd == "self":
|
||||
# The "_Player" suffix is STAGE-SPECIFIC. Stage 02 fields
|
||||
# UN_f002_TCAF_DeltaSaber_W_Player, but stage 01 fields
|
||||
# UN_f001_TCAF_DeltaSaber_T with no suffix at all, and this filter then
|
||||
# found nothing while the game was visibly flying. Fall back to the
|
||||
# player's craft class and prefer the instance that is actually moving —
|
||||
# a mission holds more than one.
|
||||
me = [e for e in ents if "Player" in e[1]]
|
||||
if not me:
|
||||
me = [e for e in ents if "DeltaSaber" in e[1] or "ArrowHead" in e[1]]
|
||||
me.sort(key=lambda e: -e[3]) # fastest first: the flown one
|
||||
if me:
|
||||
print(f"# no *_Player entity; falling back to craft class -> {me[0][1]}")
|
||||
if not me:
|
||||
sys.exit("player entity not found")
|
||||
off, nm, pos, sp = me[0]
|
||||
|
||||
69
tools/re-capture/find_partslot.sh
Executable file
@@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env bash
|
||||
# Locate the GamePart "slot" in guest memory by DIFFERENTIAL SEARCH.
|
||||
#
|
||||
# sub_821749C0 creates a part from slot+12 (the requested part id) and stores the
|
||||
# result at *(slot+16). The id is computed from a menu selection -- no literal 26
|
||||
# exists anywhere -- so it cannot be found statically. But the id is KNOWN at each
|
||||
# screen from the GamePart table (docs/re/challenge-mission-gate.md section 3):
|
||||
#
|
||||
# GP_EXTRAS = 5 GP_MISSION_SELECT = 7
|
||||
#
|
||||
# so the slot is simply the word that reads 5 on the EXTRAS screen and 7 on the
|
||||
# MISSION SELECT screen. Snapshot both, intersect, and the candidates are few.
|
||||
#
|
||||
# Usage: find_partslot.sh [boot_timeout_s]
|
||||
set -u
|
||||
export HOME=/sylph-home/re DISPLAY=:99 SDL_AUDIODRIVER=dummy
|
||||
export XENIA_PAD_FILE=/tmp/xenia_pad.txt
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||
pad() { python3 "$HERE/pad.py" "$@"; }
|
||||
poke() { python3 "$HERE/gpoke.py" "$@"; }
|
||||
BOOT_TIMEOUT="${1:-400}"
|
||||
say() { echo "[$(date +%H:%M:%S)] $*"; }
|
||||
px() { convert /tmp/nav-probe.png -format \
|
||||
"%[fx:int(255*p{$1}.r)] %[fx:int(255*p{$1}.g)] %[fx:int(255*p{$1}.b)]" info: 2>/dev/null; }
|
||||
at_menu() {
|
||||
screenshot /tmp/nav-probe.png >/dev/null 2>&1 || return 1
|
||||
read -r r g b < <(px "648,221"); [ -n "${r:-}" ] || return 1
|
||||
[ "$r" -gt 230 ] && [ "$g" -gt 230 ] && [ "$b" -gt 230 ] || return 1
|
||||
read -r r2 g2 b2 < <(px "560,300"); [ -n "${r2:-}" ] || return 1
|
||||
[ "$r2" -lt 120 ] && [ "$b2" -gt "$r2" ]
|
||||
}
|
||||
at_title() {
|
||||
screenshot /tmp/nav-probe.png >/dev/null 2>&1 || return 1
|
||||
read -r r g b < <(px "625,618"); [ -n "${g:-}" ] || return 1
|
||||
[ "$g" -gt 130 ] && [ $((g - r)) -gt 45 ] && [ $((g - b)) -gt 45 ]
|
||||
}
|
||||
snap() { SHM=$(ls /dev/shm/xenia_memory_* 2>/dev/null | head -1); cp --sparse=always "$SHM" "$1"; }
|
||||
|
||||
pkill -9 -x xenia_canary 2>/dev/null; sleep 1
|
||||
rm -f /dev/shm/xenia_* 2>/dev/null; : > "$XENIA_PAD_FILE"
|
||||
say "launching"
|
||||
run-canary --audio --apu=sdl --log_mask=13 \
|
||||
--logged_profile_slot_0_xuid=E0300000EFBEA3D4 \
|
||||
--hid=file --pad_file="$XENIA_PAD_FILE" &
|
||||
xsetroot -solid black 2>/dev/null || true
|
||||
|
||||
MENU=0
|
||||
for i in $(seq 1 "$BOOT_TIMEOUT"); do
|
||||
[ "$i" -lt 40 ] && { sleep 1; continue; }
|
||||
if at_menu && sleep 1 && at_menu; then say "MAIN MENU after ${i}s"; MENU=1; break; fi
|
||||
at_title && { say " title — tapping A"; pad tap A 0.25; sleep 2; }
|
||||
sleep 1
|
||||
done
|
||||
[ "$MENU" = 1 ] || { say "TIMEOUT"; exit 1; }
|
||||
|
||||
say "-> EXTRAS (GamePart 5)"
|
||||
for _ in 1 2 3 4; do pad dpad down 0.06; sleep 0.35; done
|
||||
sleep 0.5; pad tap A 0.15; sleep 4
|
||||
screenshot "$HOME/shots/slot-01-extras.png" >/dev/null
|
||||
snap /sylph-home/re/snap-extras.bin; say "snapshot A (EXTRAS) taken"
|
||||
|
||||
say "-> MISSION SELECT (GamePart 7)"
|
||||
pad tap A 0.15; sleep 5
|
||||
screenshot "$HOME/shots/slot-02-missionselect.png" >/dev/null
|
||||
snap /sylph-home/re/snap-msel.bin; say "snapshot B (MISSION SELECT) taken"
|
||||
|
||||
say "intersecting: word == 5 in A and == 7 in B"
|
||||
python3 "$HERE/diff_words.py" /sylph-home/re/snap-extras.bin 5 /sylph-home/re/snap-msel.bin 7
|
||||
say "done — emulator left running"
|
||||
37
tools/re-capture/fly_stage.sh
Executable file
@@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env bash
|
||||
# Launch any STORY stage from MISSION SELECT and snapshot guest RAM in flight.
|
||||
#
|
||||
# This is the LAUNCH half; the navigation lives in nav_to_flight.sh so a Canary
|
||||
# that is already up (a boot to the title costs minutes under lavapipe) can be
|
||||
# driven without paying for a second one.
|
||||
#
|
||||
# Usage: fly_stage.sh <stage 1..16> [boot_timeout_s]
|
||||
set -u
|
||||
STAGE="${1:?usage: fly_stage.sh <stage 1..16>}"
|
||||
BOOT_TIMEOUT="${2:-400}"
|
||||
# DISPLAY is overridable: the container entrypoint's own Xvfb owns :99's lock as
|
||||
# root, and once that server dies an unprivileged relaunch cannot clear
|
||||
# /tmp/.X99-lock — so a session may have to bring its display up elsewhere.
|
||||
export HOME=/sylph-home/re DISPLAY="${DISPLAY:-:99}" SDL_AUDIODRIVER=dummy
|
||||
export XENIA_PAD_FILE=/tmp/xenia_pad.txt
|
||||
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||
say() { echo "[$(date +%H:%M:%S)] $*"; }
|
||||
|
||||
pkill -9 -x xenia_canary 2>/dev/null; sleep 1
|
||||
rm -f /dev/shm/xenia_* 2>/dev/null
|
||||
: > "$XENIA_PAD_FILE"
|
||||
|
||||
say "launching canary for stage $STAGE"
|
||||
# CANARY_EXTRA lets a caller add cvars (e.g. --frame_probe_log=...) without
|
||||
# forking this script; unquoted on purpose so it can carry several.
|
||||
# shellcheck disable=SC2086
|
||||
run-canary --audio --apu=sdl --log_mask=13 \
|
||||
--logged_profile_slot_0_xuid=E0300000EFBEA3D4 \
|
||||
--hid=file --pad_file="$XENIA_PAD_FILE" ${CANARY_EXTRA:-} &
|
||||
# Xvfb keeps the previous instance's framebuffer until the new one draws.
|
||||
xsetroot -solid black 2>/dev/null || true
|
||||
# Nothing is on screen for the first ~40 s, so do not start polling into it.
|
||||
sleep 40
|
||||
|
||||
exec "$HERE/nav_to_flight.sh" "$STAGE" "$BOOT_TIMEOUT" --snapshot
|
||||
109
tools/re-capture/frame_burst.py
Executable file
@@ -0,0 +1,109 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Drive stick bursts while Canary samples the craft's transform ONCE PER FRAME.
|
||||
|
||||
Why this exists. Every earlier rate measurement polled guest RAM from the host
|
||||
through /dev/shm. That read is unsynchronised with the guest: adjacent samples
|
||||
are separated by an unknown number of guest updates, so short windows alias --
|
||||
`ramp_probe.py` got 3x swings between neighbouring 0.25 s windows and could not
|
||||
tell "the rate ramps up after the stick goes over" from "the sampler is lying".
|
||||
See docs/re/flight-speed-law.md, which names a Canary-side hook as the tool the
|
||||
residual needs.
|
||||
|
||||
That hook now exists (`--frame_probe_log`, sampled in VdSwap, one line per guest
|
||||
frame). This script only has to point it at the player craft and drive the pad,
|
||||
recording when each hold starts and ends in the SAME clock the probe stamps its
|
||||
lines with, so the analysis can cut the log at the exact frame the stick moved.
|
||||
|
||||
Usage: frame_burst.py <events.csv> [hold_s] [repeats]
|
||||
Env: XENIA_FRAME_PROBE control file (default /tmp/xenia_frame_probe.txt)
|
||||
"""
|
||||
import os
|
||||
import struct
|
||||
import sys
|
||||
import time
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import gmem # noqa: E402
|
||||
import speed_law # noqa: E402
|
||||
from axis_probe import pad_state # noqa: E402
|
||||
|
||||
PROBE = os.environ.get("XENIA_FRAME_PROBE", "/tmp/xenia_frame_probe.txt")
|
||||
|
||||
# The transform block sits BELOW the position triple: three 16-byte-strided rows
|
||||
# starting at pos-112, position at pos+0 (nav-live.json: rot_delta -112,
|
||||
# rot_stride 16). One region covers both.
|
||||
ROT_DELTA = -112
|
||||
REGION_LEN = 128
|
||||
|
||||
# Full deflection on each axis, plus the throttle settings the burst design uses.
|
||||
BURSTS = [
|
||||
("roll_cruise", {"lx": 32767}),
|
||||
("pitch_cruise", {"ly": 32767}),
|
||||
]
|
||||
|
||||
|
||||
def write_regions(regions):
|
||||
tmp = PROBE + ".tmp"
|
||||
with open(tmp, "w") as f:
|
||||
f.write("# written by frame_burst.py\n")
|
||||
for va, ln in regions:
|
||||
f.write(f"{va:08x} {ln}\n")
|
||||
os.replace(tmp, PROBE)
|
||||
|
||||
|
||||
def pos_at(fd, off):
|
||||
return struct.unpack(">3f", os.pread(fd, 12, off))
|
||||
|
||||
|
||||
def main():
|
||||
out_csv = sys.argv[1]
|
||||
hold = float(sys.argv[2]) if len(sys.argv) > 2 else 3.0
|
||||
reps = int(sys.argv[3]) if len(sys.argv) > 3 else 2
|
||||
|
||||
w, off, nm = speed_law.find_player()
|
||||
if not w:
|
||||
sys.exit("player entity not found")
|
||||
base_off = off + ROT_DELTA
|
||||
va = gmem.primary_va(base_off)
|
||||
if va is None:
|
||||
sys.exit(f"file offset {base_off:#x} has no guest VA")
|
||||
print(f"# locked on {nm}: transform block at VA {va:#010x} (+{REGION_LEN})")
|
||||
write_regions([(va, REGION_LEN)])
|
||||
|
||||
# The probe only starts emitting once the emulator notices the control file,
|
||||
# which is at most one frame. Give it a moment, then prove the craft is
|
||||
# actually flying before spending the run on it.
|
||||
time.sleep(1.0)
|
||||
p0 = pos_at(w.fd, off)
|
||||
time.sleep(1.0)
|
||||
p1 = pos_at(w.fd, off)
|
||||
moved = sum((p1[i] - p0[i]) ** 2 for i in range(3)) ** 0.5
|
||||
if moved < 1.0:
|
||||
sys.exit(f"craft is not moving ({moved:.3f}) — not in flight, or dead")
|
||||
print(f"# craft is flying ({moved:.1f} units/s)")
|
||||
|
||||
rows = []
|
||||
for rep in range(reps):
|
||||
for label, state in BURSTS:
|
||||
pad_state()
|
||||
time.sleep(5.0)
|
||||
t_pre = time.time()
|
||||
pad_state(**state)
|
||||
t_on = time.time()
|
||||
time.sleep(hold)
|
||||
pad_state()
|
||||
t_off = time.time()
|
||||
rows.append((rep, label, f"{t_pre:.6f}", f"{t_on:.6f}", f"{t_off:.6f}"))
|
||||
print(f"# rep{rep} {label}: held {t_on:.3f} -> {t_off:.3f}", flush=True)
|
||||
time.sleep(2.0)
|
||||
pad_state()
|
||||
|
||||
with open(out_csv, "w") as f:
|
||||
f.write("rep,label,t_pre,t_on,t_off\n")
|
||||
for r in rows:
|
||||
f.write(",".join(str(x) for x in r) + "\n")
|
||||
print(f"# wrote {out_csv}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
59
tools/re-capture/frame_session.sh
Executable file
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env bash
|
||||
# One blocking foreground session: boot to flight, then measure angular rate with
|
||||
# the per-frame probe instead of host polling.
|
||||
#
|
||||
# Everything (Xvfb, openbox, xenia) is a plain child of THIS script. Anything
|
||||
# `setsid`'d is reaped at the agent's turn boundary — see the session-lifetime
|
||||
# note in launch_mission.sh — so the whole run has to fit in one call.
|
||||
#
|
||||
# Usage: frame_session.sh [stage] [hold_s] [repeats]
|
||||
set -u
|
||||
STAGE="${1:-10}"; HOLD="${2:-3.0}"; REPS="${3:-2}"
|
||||
export HOME=/sylph-home/re DISPLAY="${DISPLAY:-:99}" SDL_AUDIODRIVER=dummy
|
||||
export XENIA_PAD_FILE=/tmp/xenia_pad.txt
|
||||
export XENIA_FRAME_PROBE=/tmp/xenia_frame_probe.txt
|
||||
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||
OUT="$HOME/frameprobe"; mkdir -p "$OUT"
|
||||
LOG="$OUT/frame-stage$STAGE.log"
|
||||
say() { echo "[$(date +%H:%M:%S)] $*"; }
|
||||
|
||||
ensure_display() {
|
||||
if ! xdpyinfo -display "$DISPLAY" >/dev/null 2>&1; then
|
||||
rm -f "/tmp/.X${DISPLAY#:}-lock" 2>/dev/null || true
|
||||
nohup bash -c 'Xvfb "$0" -screen 0 1280x720x24 -ac -nolisten tcp \
|
||||
+extension GLX +extension RANDR >/tmp/xvfb${DISPLAY#:}.log 2>&1
|
||||
echo "$(date +%T) XVFB EXIT $? (128+N means signal N)" >>/tmp/xvfb-exit.log' \
|
||||
"$DISPLAY" </dev/null >/dev/null 2>&1 &
|
||||
for _ in $(seq 1 50); do xdpyinfo -display "$DISPLAY" >/dev/null 2>&1 && break; sleep 0.2; done
|
||||
nohup env DISPLAY="$DISPLAY" HOME=/sylph-home openbox </dev/null >/tmp/openbox${DISPLAY#:}.log 2>&1 &
|
||||
sleep 1
|
||||
fi
|
||||
xdpyinfo -display "$DISPLAY" >/dev/null 2>&1 || { say "DISPLAY UNAVAILABLE"; exit 1; }
|
||||
}
|
||||
|
||||
ensure_display
|
||||
# A stale control file would make the probe log the PREVIOUS run's addresses from
|
||||
# the first frame, which land in whatever occupies them now.
|
||||
rm -f "$XENIA_FRAME_PROBE"
|
||||
|
||||
# REUSE=1 drives a Canary that is already up (it must have been launched with the
|
||||
# same --frame_probe_log). Booting to the title costs minutes under lavapipe and
|
||||
# a run that only failed to navigate should not pay for it again.
|
||||
if [ "${REUSE:-0}" = 1 ] && pgrep -x xenia_canary >/dev/null; then
|
||||
say "re-using the running emulator; navigating to stage $STAGE"
|
||||
"$HERE/nav_to_flight.sh" "$STAGE" 300 || { say "navigation failed ($?)"; exit 2; }
|
||||
else
|
||||
rm -f "$LOG"
|
||||
say "booting to flight on stage $STAGE with the per-frame probe armed"
|
||||
CANARY_EXTRA="--frame_probe_log=$LOG --frame_probe=$XENIA_FRAME_PROBE" \
|
||||
"$HERE/fly_stage.sh" "$STAGE" 300 || { say "boot failed ($?)"; exit 2; }
|
||||
fi
|
||||
|
||||
say "arming the probe on the player craft and driving the bursts"
|
||||
python3 "$HERE/frame_burst.py" "$OUT/events-stage$STAGE.csv" "$HOLD" "$REPS"
|
||||
rc=$?
|
||||
# Stop sampling before the emulator is torn down, so the log ends cleanly.
|
||||
rm -f "$XENIA_FRAME_PROBE"
|
||||
say "frame log: $LOG ($(wc -l < "$LOG" 2>/dev/null || echo 0) lines)"
|
||||
exit $rc
|
||||
76
tools/re-capture/gpoke.py
Executable file
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env python3
|
||||
"""WRITE to the live guest memory of a running Xenia Canary.
|
||||
|
||||
The read-side companion is `gmem.py`, and this shares its guest-VA → file-offset
|
||||
table. Canary backs the whole guest address space with one shared-memory file
|
||||
(`/dev/shm/xenia_memory_*`), so a write here lands in the running guest with no
|
||||
debugger and no pause.
|
||||
|
||||
gpoke.py w32 <va> <value> [...] write big-endian u32s at consecutive VAs
|
||||
gpoke.py r32 <va> [n] read back n big-endian u32s (verify)
|
||||
|
||||
Values and addresses accept `0x` form. Every write prints the before/after word,
|
||||
because a poke you cannot see is a poke you cannot trust.
|
||||
|
||||
⚠️ This mutates a running game. It is a research tool: there is no undo, and a
|
||||
wrong address will corrupt whatever it lands on. Read back before believing.
|
||||
"""
|
||||
|
||||
import os
|
||||
import struct
|
||||
import sys
|
||||
|
||||
from gmem import MAP, va_to_off # noqa: F401 (MAP re-exported for callers)
|
||||
|
||||
|
||||
def shm_path():
|
||||
cands = [f"/dev/shm/{n}" for n in os.listdir("/dev/shm") if n.startswith("xenia_memory")]
|
||||
if not cands:
|
||||
raise SystemExit("no /dev/shm/xenia_memory_* — is Canary running?")
|
||||
if len(cands) > 1:
|
||||
raise SystemExit(f"several guest images, refusing to guess: {cands}")
|
||||
return cands[0]
|
||||
|
||||
|
||||
def read32(f, va):
|
||||
f.seek(va_to_off(va))
|
||||
return struct.unpack(">I", f.read(4))[0]
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 3:
|
||||
print(__doc__)
|
||||
return 1
|
||||
cmd = sys.argv[1]
|
||||
path = shm_path()
|
||||
if cmd == "r32":
|
||||
va = int(sys.argv[2], 0)
|
||||
n = int(sys.argv[3], 0) if len(sys.argv) > 3 else 1
|
||||
with open(path, "rb") as f:
|
||||
for i in range(n):
|
||||
a = va + 4 * i
|
||||
print(f" {a:#010x} = {read32(f, a):#010x}")
|
||||
return 0
|
||||
if cmd == "w32":
|
||||
va = int(sys.argv[2], 0)
|
||||
vals = [int(v, 0) for v in sys.argv[3:]]
|
||||
if not vals:
|
||||
print("nothing to write")
|
||||
return 1
|
||||
with open(path, "r+b") as f:
|
||||
for i, v in enumerate(vals):
|
||||
a = va + 4 * i
|
||||
before = read32(f, a)
|
||||
f.seek(va_to_off(a))
|
||||
f.write(struct.pack(">I", v))
|
||||
f.flush()
|
||||
after = read32(f, a)
|
||||
ok = "OK" if after == v else "!! MISMATCH"
|
||||
print(f" {a:#010x}: {before:#010x} -> {after:#010x} {ok}")
|
||||
return 0
|
||||
print(f"unknown command {cmd!r}")
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
134
tools/re-capture/nav_to_flight.sh
Executable file
@@ -0,0 +1,134 @@
|
||||
#!/usr/bin/env bash
|
||||
# Drive an ALREADY-RUNNING Canary from the title/attract movie to flight on any
|
||||
# story stage. Split out of fly_stage.sh so a live emulator can be re-used: a
|
||||
# boot to the title costs minutes under lavapipe, and a run that only failed to
|
||||
# navigate should not have to pay for it twice.
|
||||
#
|
||||
# Poking ONE word marks every story stage cleared, so MISSION SELECT will launch
|
||||
# any of them — 0x828F40C0 = 0x0001FFFE (cleared-stage mask, bits 1..16); see
|
||||
# docs/re/challenge-mission-gate.md. Nothing is written to disc.
|
||||
#
|
||||
# Usage: nav_to_flight.sh <stage 1..16> [menu_timeout_s] [--snapshot]
|
||||
set -u
|
||||
STAGE="${1:?usage: nav_to_flight.sh <stage 1..16>}"
|
||||
MENU_TIMEOUT="${2:-400}"
|
||||
SNAPSHOT="${3:-}"
|
||||
# DISPLAY is overridable: the container entrypoint's own Xvfb owns :99's lock as
|
||||
# root, and once that server dies an unprivileged relaunch cannot clear
|
||||
# /tmp/.X99-lock — so a session may have to bring its display up elsewhere.
|
||||
export HOME=/sylph-home/re DISPLAY="${DISPLAY:-:99}" SDL_AUDIODRIVER=dummy
|
||||
export XENIA_PAD_FILE=/tmp/xenia_pad.txt
|
||||
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||
# bin/screenshot crops the emulator's menu bar away so a saved shot looks like
|
||||
# the bare game image; the oracles below no longer depend on that, but the shots
|
||||
# kept for evidence are easier to compare against older ones.
|
||||
export PATH="$HERE/bin:$PATH"
|
||||
pad() { python3 "$HERE/pad.py" "$@"; }
|
||||
poke() { python3 "$HERE/gpoke.py" "$@"; }
|
||||
SHOTS="$HOME/shots"; mkdir -p "$SHOTS"
|
||||
say() { echo "[$(date +%H:%M:%S)] $*"; }
|
||||
shot() { screenshot "$SHOTS/fly$STAGE-$1.png" >/dev/null 2>&1; }
|
||||
screen() { python3 "$HERE/screen_id.py" /tmp/nav-probe.png --json 2>/dev/null; }
|
||||
|
||||
# Screens are identified by WHOLE-IMAGE statistics (screen_id.py), never by named
|
||||
# pixels. The pixel oracles this replaces were measured against a bare 1280x720
|
||||
# game image, and xenia's window puts a menu bar above it on some displays -- so
|
||||
# every constant read ~25 px too high, silently. That cost one run 300 s staring
|
||||
# at a visible MAIN MENU and another ten minutes of attract movie.
|
||||
# One grab per poll, classified once: a screenshot costs seconds while lavapipe
|
||||
# has every core, so asking three separate questions per second does not work.
|
||||
which_screen() {
|
||||
screenshot /tmp/nav-probe.png >/dev/null 2>&1 || { echo none; return; }
|
||||
screen | python3 -c 'import json,sys; print(json.load(sys.stdin)["screen"])' 2>/dev/null || echo none
|
||||
}
|
||||
|
||||
pgrep -x xenia_canary >/dev/null || { say "no xenia_canary running"; exit 1; }
|
||||
|
||||
say "waiting for the main menu"
|
||||
MENU=0
|
||||
LAST=
|
||||
for i in $(seq 1 "$MENU_TIMEOUT"); do
|
||||
NOW=$(which_screen)
|
||||
[ "$NOW" != "$LAST" ] && { say " screen: $NOW"; LAST=$NOW; }
|
||||
# Two consecutive reads, because a movie frame can momentarily look like a menu.
|
||||
if [ "$NOW" = menu ] && sleep 1 && [ "$(which_screen)" = menu ]; then
|
||||
say "MAIN MENU after ${i}s"; MENU=1; break
|
||||
fi
|
||||
[ "$NOW" = title ] && { say " title — tapping A"; pad tap A 0.25; sleep 2; }
|
||||
sleep 1
|
||||
done
|
||||
[ "$MENU" = 1 ] || { say "TIMEOUT: no main menu"; shot 00-timeout; exit 1; }
|
||||
|
||||
say "poking the cleared-stage mask so every story stage is selectable"
|
||||
poke w32 0x828F40C0 0x0001FFFE
|
||||
|
||||
# main menu -> EXTRAS -> MISSION SELECT
|
||||
for _ in 1 2 3 4; do pad dpad down 0.06; sleep 0.35; done
|
||||
sleep 0.5; pad tap A 0.15; sleep 3; shot 01-extras
|
||||
pad tap A 0.15; sleep 4; shot 02-missionselect
|
||||
|
||||
# the list starts on Stage01
|
||||
say "stepping to Stage$STAGE"
|
||||
for _ in $(seq 2 "$STAGE"); do pad dpad down 0.06; sleep 0.30; done
|
||||
sleep 1; shot 03-selected
|
||||
pad tap A 0.20; sleep 3; shot 04-after-A
|
||||
pad tap A 0.20; sleep 3; shot 05-after-A2
|
||||
|
||||
# Wait for a screen instead of sleeping a guess. The fixed sleeps below used to be
|
||||
# enough and then were not: one run's stage load ran long, the script pressed START
|
||||
# into a black loading screen, and every later step went to nothing while the shots
|
||||
# recorded a plausible-looking sequence. Same lesson as the menu oracles.
|
||||
wait_not_black() { # $1 = timeout s
|
||||
for _ in $(seq 1 "$1"); do
|
||||
if screenshot /tmp/nav-probe.png >/dev/null 2>&1; then
|
||||
lit=$(screen | python3 -c 'import json,sys; d=json.load(sys.stdin); print(1 if d.get("r",0)+d.get("g",0)+d.get("b",0) > 20 else 0)' 2>/dev/null)
|
||||
[ "$lit" = 1 ] && return 0
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# Selecting a stage lands on the mission BRIEFING (A: Continue), which leads to the
|
||||
# READY ROOM (TAKE OFF / BRIEFINGS / HANGAR / ...) with BRIEFINGS pre-highlighted.
|
||||
# A snapshot taken at the briefing yields ZERO unit-definition objects — they are
|
||||
# instantiated at stage load proper — so the run has to reach flight.
|
||||
# START (Skip) clears the briefing. `A` does NOT: it pages through the brief and
|
||||
# ten taps still left the run sitting on a briefing screen, twice. One START press
|
||||
# lands on the READY ROOM.
|
||||
say "waiting for the stage to finish loading"
|
||||
if ! wait_not_black 180; then
|
||||
say "STILL BLACK after 180s — the load hung (this happens; check the log for"
|
||||
say " 'PhysicalHeap::Release failed'), aborting instead of pressing into nothing"
|
||||
shot 06-stuck-black
|
||||
exit 2
|
||||
fi
|
||||
sleep 3
|
||||
say "skipping the briefing (START, not A)"
|
||||
pad tap START 0.30; sleep 6
|
||||
shot 06-readyroom
|
||||
say "READY ROOM -> TAKE OFF (one up from the pre-highlighted BRIEFINGS)"
|
||||
pad dpad up 0.06; sleep 1; shot 07-takeoff-hl
|
||||
pad tap A 0.20; sleep 8
|
||||
# TAKE OFF starts its own load, and it hangs black just as the stage load can --
|
||||
# guarding only the first transition left a run pressing A into a black screen and
|
||||
# then reporting "player entity not found" from a game that never reached flight.
|
||||
say "waiting for the take-off load"
|
||||
if ! wait_not_black 180; then
|
||||
say "STILL BLACK after take-off — load hung, aborting"
|
||||
shot 08-stuck-black
|
||||
exit 2
|
||||
fi
|
||||
for _ in $(seq 1 6); do pad tap A 0.20; sleep 4; done
|
||||
say "waiting for flight to settle"
|
||||
sleep 25
|
||||
shot 08-flight
|
||||
if [ "$SNAPSHOT" = "--snapshot" ]; then
|
||||
SHM=$(ls /dev/shm/xenia_memory_* 2>/dev/null | head -1)
|
||||
if [ -n "$SHM" ]; then
|
||||
OUT="$HOME/snap-stage$STAGE.bin"
|
||||
cp --sparse=always "$SHM" "$OUT" && say "snapshot -> $OUT ($(du -h "$OUT" | cut -f1) actual)"
|
||||
fi
|
||||
fi
|
||||
say "shots: $SHOTS/fly$STAGE-*.png"
|
||||
70
tools/re-capture/pad.py
Executable file
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Drive Xenia Canary's `--hid=file` pad — the container-safe controller.
|
||||
|
||||
Replaces the old `vgamepad` path, which created its device through `/dev/uinput`.
|
||||
Input devices are not namespaced, so that device registered with the HOST's input
|
||||
stack and every scripted press leaked to the user's desktop. This one writes a
|
||||
text file the emulator polls; nothing leaves the container.
|
||||
|
||||
pad.py set "press=A" set the pad state and leave it held
|
||||
pad.py clear release everything
|
||||
pad.py tap A [secs] press, hold `secs` (default 0.10), release
|
||||
pad.py dpad down [secs] one menu step (default 0.06 — longer
|
||||
auto-repeats and overshoots)
|
||||
pad.py hold "lt=255" secs hold an arbitrary state for `secs`
|
||||
|
||||
Buttons: UP DOWN LEFT RIGHT START BACK LS RS LB RB A B X Y.
|
||||
Path from $XENIA_PAD_FILE, default /tmp/xenia_pad.txt (matches --pad_file).
|
||||
|
||||
Menu conventions in this game: A = OK, B = Back, Y = Gallery/extra, X = Delete.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
PAD = os.environ.get("XENIA_PAD_FILE", "/tmp/xenia_pad.txt")
|
||||
|
||||
|
||||
def write(state: str):
|
||||
# The driver re-parses on any (mtime-ns, size) change, so a plain rewrite is
|
||||
# enough — but write through a temp + rename so a poll can never observe a
|
||||
# half-written file.
|
||||
tmp = PAD + ".tmp"
|
||||
with open(tmp, "w") as f:
|
||||
f.write(state)
|
||||
os.replace(tmp, PAD)
|
||||
|
||||
|
||||
def main():
|
||||
a = sys.argv[1:]
|
||||
if not a:
|
||||
print(__doc__)
|
||||
return 1
|
||||
cmd = a[0]
|
||||
if cmd == "set":
|
||||
write(a[1])
|
||||
elif cmd == "clear":
|
||||
write("")
|
||||
elif cmd == "tap":
|
||||
secs = float(a[2]) if len(a) > 2 else 0.10
|
||||
write(f"press={a[1]}")
|
||||
time.sleep(secs)
|
||||
write("")
|
||||
elif cmd == "dpad":
|
||||
secs = float(a[2]) if len(a) > 2 else 0.06
|
||||
write(f"press={a[1].upper()}")
|
||||
time.sleep(secs)
|
||||
write("")
|
||||
elif cmd == "hold":
|
||||
write(a[1])
|
||||
time.sleep(float(a[2]))
|
||||
write("")
|
||||
else:
|
||||
print(f"unknown command {cmd!r}")
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
94
tools/re-capture/ramp_probe.py
Executable file
@@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Does the angular rate RAMP after the stick goes over?
|
||||
|
||||
Burst measurements came out a strikingly flat 0.60-0.64x of the roll cap and
|
||||
1.07-1.21x of the pitch cap. A clock error cannot do that (it would move both the
|
||||
same way), so something per-axis is going on, and the cheapest candidate is that a
|
||||
short burst never reaches the steady rate — an angular-acceleration ramp would make
|
||||
a 1-second burst under-read.
|
||||
|
||||
Rather than compare separate bursts (which differ in speed, attitude and starting
|
||||
conditions), measure INSIDE one hold: successive 0.25 s windows of a single 3 s
|
||||
press. A ramp shows up as the first windows reading low and the later ones
|
||||
plateauing, with everything else held constant by construction.
|
||||
|
||||
Windows, never per-read deltas: polling outruns the guest's update of these fields.
|
||||
|
||||
Usage: ramp_probe.py <roll|pitch> <out.csv> [hold_s] [repeats]
|
||||
"""
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import struct
|
||||
import sys
|
||||
import time
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import speed_law # noqa: E402
|
||||
from axis_probe import alive, pad_state, pin_rows, rows_at, dot # noqa: E402
|
||||
|
||||
DRIVER = {"roll": {"lx": 32767}, "pitch": {"ly": 32767}}
|
||||
WIN = 0.25
|
||||
|
||||
|
||||
def pos_at(fd, off):
|
||||
return struct.unpack(">3f", os.pread(fd, 12, off))
|
||||
|
||||
|
||||
def main():
|
||||
axis, out_csv = sys.argv[1], sys.argv[2]
|
||||
hold = float(sys.argv[3]) if len(sys.argv) > 3 else 3.0
|
||||
reps = int(sys.argv[4]) if len(sys.argv) > 4 else 2
|
||||
cfg = json.load(open("/tmp/nav-live.json"))
|
||||
w, off, nm = speed_law.find_player()
|
||||
if not w:
|
||||
sys.exit("player entity not found")
|
||||
f_i = cfg["fwd_row"]
|
||||
if not alive(w, off, cfg):
|
||||
sys.exit("craft is not moving")
|
||||
u_i, w_i = pin_rows(w, off, cfg)
|
||||
print(f"# locked on {nm}, {axis} ramp over {hold}s in {WIN}s windows")
|
||||
|
||||
rows = []
|
||||
for rep in range(reps):
|
||||
pad_state() # cruise: least speed bleed of the three
|
||||
time.sleep(6.0)
|
||||
prev_r, prev_p = rows_at(w.fd, off, cfg), pos_at(w.fd, off)
|
||||
pad_state(**DRIVER[axis])
|
||||
t0 = wt0 = time.time()
|
||||
path = swept = 0.0
|
||||
seq = []
|
||||
while time.time() - t0 < hold:
|
||||
time.sleep(0.02)
|
||||
now = time.time()
|
||||
cur_r, cur_p = rows_at(w.fd, off, cfg), pos_at(w.fd, off)
|
||||
path += math.dist(cur_p, prev_p)
|
||||
if axis == "roll":
|
||||
d = math.atan2(dot(cur_r[u_i], prev_r[w_i]), dot(cur_r[u_i], prev_r[u_i]))
|
||||
else:
|
||||
d = math.atan2(dot(cur_r[f_i], prev_r[u_i]), dot(cur_r[f_i], prev_r[f_i]))
|
||||
swept += abs(math.degrees(d))
|
||||
prev_r, prev_p = cur_r, cur_p
|
||||
if now - wt0 >= WIN:
|
||||
span = now - wt0
|
||||
seq.append((round(now - t0, 2), round(path / span, 1), round(swept / span, 1)))
|
||||
wt0, path, swept = now, 0.0, 0.0
|
||||
pad_state()
|
||||
for t, sp, rt in seq:
|
||||
rows.append((rep, t, sp, rt))
|
||||
print(f"# rep{rep}: " + " ".join(f"{rt:.0f}" for _, _, rt in seq))
|
||||
print(f"# speed " + " ".join(f"{sp:.0f}" for _, sp, _ in seq))
|
||||
if not alive(w, off, cfg):
|
||||
print("# CRAFT STOPPED MOVING — aborting")
|
||||
break
|
||||
time.sleep(2.0)
|
||||
|
||||
with open(out_csv, "w") as f:
|
||||
f.write("rep,t,speed,rate_deg_per_wall_s\n")
|
||||
for r in rows:
|
||||
f.write(",".join(str(x) for x in r) + "\n")
|
||||
print(f"# wrote {out_csv}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
117
tools/re-capture/rate_curve.py
Executable file
@@ -0,0 +1,117 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Angular rate as a FUNCTION of speed — fitted, not sampled at two endpoints.
|
||||
|
||||
The two-point probe (`rate_probe.py`) assumed the craft held the speed its throttle
|
||||
selects. It does not: full pitch bleeds 1 193 -> 589 in eight seconds *with the
|
||||
throttle still at maximum*, so an 8-second average is taken over a moving speed and
|
||||
lands between two different caps. That is how a measured pitch rate came out 33 %
|
||||
ABOVE its own cap.
|
||||
|
||||
Turn the bug into the instrument. Because the speed bleeds on its own, one long hold
|
||||
sweeps the whole range, so sampling position *and* attitude together gives
|
||||
`(speed, rate)` pairs across it — the entire curve from a single phase, with no
|
||||
assumption about what speed the craft is at.
|
||||
|
||||
Speed comes from the position deltas (no HUD OCR needed). Both speed and rate are
|
||||
per wall-second here; the run's clock ratio converts both to game units afterwards
|
||||
and cancels out of the SHAPE of the curve.
|
||||
|
||||
⚠️ SAMPLE IN WINDOWS, NOT PER READ. Polling at 20 Hz is faster than the guest
|
||||
updates these fields, so a per-read delta is either exactly zero (no update yet) or
|
||||
a whole frame's worth divided by a fraction of a frame. In a first run 111 of 352
|
||||
reads were zero on BOTH channels — position and attitude update on the same frame,
|
||||
so the two are perfectly correlated, and dividing each by the short wall dt
|
||||
manufactured a clean "rate rises with speed" curve out of pure aliasing. Summing
|
||||
|delta| over a window that spans many frames is immune: the total is right however
|
||||
the updates fall inside it. (This is also why the swept-total probes were never
|
||||
affected — only per-sample instantaneous rates were.)
|
||||
|
||||
Usage: rate_curve.py <roll|pitch> <out.csv> [dwell_s]
|
||||
"""
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import speed_law # noqa: E402
|
||||
from axis_probe import alive, pad_state, pin_rows, rows_at, dot # noqa: E402
|
||||
|
||||
DRIVER = {"roll": {"lx": 32767}, "pitch": {"ly": 32767}}
|
||||
|
||||
|
||||
def shot(n):
|
||||
subprocess.run(["screenshot", f"/sylph-home/re/shots/{n}.png"], capture_output=True)
|
||||
|
||||
|
||||
def pos_at(fd, off):
|
||||
return struct.unpack(">3f", os.pread(fd, 12, off))
|
||||
|
||||
|
||||
def main():
|
||||
axis, out_csv = sys.argv[1], sys.argv[2]
|
||||
dwell = float(sys.argv[3]) if len(sys.argv) > 3 else 16.0
|
||||
cfg = json.load(open("/tmp/nav-live.json"))
|
||||
w, off, nm = speed_law.find_player()
|
||||
if not w:
|
||||
sys.exit("player entity not found")
|
||||
f_i = cfg["fwd_row"]
|
||||
if not alive(w, off, cfg):
|
||||
sys.exit("craft is not moving")
|
||||
u_i, w_i = pin_rows(w, off, cfg)
|
||||
print(f"# locked on {nm}, sweeping {axis}")
|
||||
|
||||
# Start at maximum speed and let the turn bleed it: one hold, whole range.
|
||||
pad_state(rt=255)
|
||||
time.sleep(6.0)
|
||||
shot(f"curve_{axis}_a")
|
||||
prev_r, prev_p, prev_t = rows_at(w.fd, off, cfg), pos_at(w.fd, off), time.time()
|
||||
pad_state(rt=255, **DRIVER[axis])
|
||||
t0 = time.time()
|
||||
rows = []
|
||||
WIN = 0.5 # seconds per emitted point: many guest frames
|
||||
win_t0, win_path, win_swept = time.time(), 0.0, 0.0
|
||||
while time.time() - t0 < dwell:
|
||||
time.sleep(0.02)
|
||||
now = time.time()
|
||||
cur_r, cur_p = rows_at(w.fd, off, cfg), pos_at(w.fd, off)
|
||||
win_path += math.dist(cur_p, prev_p)
|
||||
if axis == "roll":
|
||||
d = math.atan2(dot(cur_r[u_i], prev_r[w_i]), dot(cur_r[u_i], prev_r[u_i]))
|
||||
else:
|
||||
d = math.atan2(dot(cur_r[f_i], prev_r[u_i]), dot(cur_r[f_i], prev_r[f_i]))
|
||||
win_swept += abs(math.degrees(d))
|
||||
prev_r, prev_p = cur_r, cur_p
|
||||
if now - win_t0 >= WIN:
|
||||
span = now - win_t0
|
||||
rows.append((round(now - t0, 2), round(win_path / span, 1),
|
||||
round(win_swept / span, 2)))
|
||||
win_t0, win_path, win_swept = now, 0.0, 0.0
|
||||
pad_state()
|
||||
shot(f"curve_{axis}_b")
|
||||
|
||||
with open(out_csv, "w") as f:
|
||||
f.write("t,speed_units_per_wall_s,rate_deg_per_wall_s\n")
|
||||
for r in rows:
|
||||
f.write(",".join(str(x) for x in r) + "\n")
|
||||
|
||||
# Bin by speed so the shape is visible without any curve-fitting assumption.
|
||||
print(f"# {len(rows)} windows of {WIN}s; rate binned by speed (per WALL second):")
|
||||
lo = min(r[1] for r in rows)
|
||||
hi = max(r[1] for r in rows)
|
||||
print(f"# speed swept {lo:.0f} -> {hi:.0f} units/wall-s")
|
||||
nb = 6
|
||||
for b in range(nb):
|
||||
a = lo + (hi - lo) * b / nb
|
||||
z = lo + (hi - lo) * (b + 1) / nb
|
||||
sel = [r[2] for r in rows if a <= r[1] < z]
|
||||
if sel:
|
||||
print(f"# speed {a:7.0f}-{z:7.0f} n={len(sel):3d} rate {sum(sel)/len(sel):6.1f}")
|
||||
print(f"# wrote {out_csv}; read HUD TIME off curve_{axis}_[ab] for the clock")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
94
tools/re-capture/rate_probe.py
Executable file
@@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Angular rate cap for one axis, at minimum and maximum speed.
|
||||
|
||||
Generalises `roll_axis.py`. Two things it does that the probes before it did not:
|
||||
|
||||
* **Pins the matrix rows.** `entities2` measures row 2 = forward against velocity,
|
||||
but up-vs-right was previously taken from the D3D convention — and measuring
|
||||
world-Y in flight showed the convention is *backwards* here (up = row 1, right =
|
||||
row 0). Roll is immune to that (rotation of either non-forward row within their
|
||||
shared plane is roll either way), but **pitch is not**, so a pitch number taken
|
||||
without this pinning is naming an axis by assumption.
|
||||
* **Brackets each phase with the HUD clock.** Rates are per GAME second and the
|
||||
clock ratio is a property of the moment — 1.260, 1.311, 1.383 and 1.247/1.218
|
||||
across five flights — so it cannot be assumed and must be read per phase.
|
||||
|
||||
roll driven by lx, measured as rotation of UP about forward
|
||||
pitch driven by ly, measured as rotation of FORWARD toward up
|
||||
|
||||
Usage: rate_probe.py <roll|pitch> <out.csv> [dwell_s]
|
||||
Then read HUD TIME off the `rate_<axis>_<phase>_[ab]` screenshots for the ratio.
|
||||
"""
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import speed_law # noqa: E402
|
||||
from axis_probe import alive, pad_state, pin_rows, rows_at, dot # noqa: E402
|
||||
|
||||
DRIVER = {"roll": {"lx": 32767}, "pitch": {"ly": 32767}}
|
||||
|
||||
|
||||
def shot(n):
|
||||
subprocess.run(["screenshot", f"/sylph-home/re/shots/{n}.png"], capture_output=True)
|
||||
|
||||
|
||||
def main():
|
||||
axis = sys.argv[1]
|
||||
out_csv = sys.argv[2]
|
||||
dwell = float(sys.argv[3]) if len(sys.argv) > 3 else 8.0
|
||||
if axis not in DRIVER:
|
||||
sys.exit(f"axis must be one of {list(DRIVER)}")
|
||||
cfg = json.load(open("/tmp/nav-live.json"))
|
||||
w, off, nm = speed_law.find_player()
|
||||
if not w:
|
||||
sys.exit("player entity not found")
|
||||
print(f"# locked on {nm}, measuring {axis}")
|
||||
f_i = cfg["fwd_row"]
|
||||
if not alive(w, off, cfg):
|
||||
sys.exit("craft is not moving — not in flight, or already dead")
|
||||
u_i, w_i = pin_rows(w, off, cfg)
|
||||
|
||||
rows = []
|
||||
for label, trig in (("slow", "lt"), ("fast", "rt")):
|
||||
pad_state(**{trig: 255})
|
||||
time.sleep(5.0) # settle: 2 s was shown wrong twice
|
||||
shot(f"rate_{axis}_{label}_a")
|
||||
prev = rows_at(w.fd, off, cfg)
|
||||
pad_state(**{trig: 255}, **DRIVER[axis])
|
||||
t0, swept, seq = time.time(), 0.0, []
|
||||
while time.time() - t0 < dwell:
|
||||
time.sleep(0.05)
|
||||
cur = rows_at(w.fd, off, cfg)
|
||||
if axis == "roll":
|
||||
d = math.atan2(dot(cur[u_i], prev[w_i]), dot(cur[u_i], prev[u_i]))
|
||||
else:
|
||||
d = math.atan2(dot(cur[f_i], prev[u_i]), dot(cur[f_i], prev[f_i]))
|
||||
d = math.degrees(d)
|
||||
swept += abs(d)
|
||||
seq.append((round(time.time() - t0, 3), round(d, 4)))
|
||||
prev = cur
|
||||
pad_state(**{trig: 255})
|
||||
wall = seq[-1][0]
|
||||
shot(f"rate_{axis}_{label}_b")
|
||||
rows += [(label, *s) for s in seq]
|
||||
print(f"# {label:<5} wall {wall:5.2f}s {axis} swept {swept:7.1f}deg "
|
||||
f"rate {swept / wall:6.1f} deg/wall-s")
|
||||
if not alive(w, off, cfg):
|
||||
print("# CRAFT STOPPED MOVING — aborting rather than reporting zeros")
|
||||
break
|
||||
pad_state()
|
||||
with open(out_csv, "w") as f:
|
||||
f.write(f"phase,t,d{axis}_deg\n")
|
||||
for r in rows:
|
||||
f.write(",".join(str(x) for x in r) + "\n")
|
||||
print(f"# wrote {out_csv} — read HUD TIME off rate_{axis}_*_[ab] for the clock")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
63
tools/re-capture/rebuild_canary.sh
Executable file
@@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env bash
|
||||
# Surgical rebuild of xenia_canary inside the sylph box.
|
||||
#
|
||||
# The image has no cmake/ninja/clang of its own and the build/ cache cannot be
|
||||
# re-configured (see project_sylph_canary_patch_toolchain memory), so a full
|
||||
# `ninja` is impossible: several TUs need dev headers this image lacks. What DOES
|
||||
# work is compiling only the objects that changed, refreshing their archive, and
|
||||
# re-running the link command lifted out of the generated ninja file.
|
||||
#
|
||||
# Usage: rebuild.sh <ninja object path> [more objects...]
|
||||
# e.g. rebuild.sh src/xenia/kernel/CMakeFiles/xenia-kernel.dir/Release/xboxkrnl/xboxkrnl_video.cc.o
|
||||
# Objects are paths relative to build/. The archive each belongs to is derived
|
||||
# from the .dir name (xenia-kernel.dir -> obj/Linux/Release/libxenia-kernel.a).
|
||||
set -eu
|
||||
|
||||
PROJ="${PROJECT_DIR:-/home/fabi/RE - Project Sylpheed}"
|
||||
BUILD="$PROJ/xenia-canary-native/build"
|
||||
PFX=/sylph-home/re/toolchain
|
||||
export PATH="$PFX/usr/lib/llvm-18/bin:/sylph-home/.local/bin:$PATH"
|
||||
export LD_LIBRARY_PATH="$PFX/usr/lib/x86_64-linux-gnu:$PFX/usr/lib/llvm-18/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
|
||||
|
||||
[ $# -ge 1 ] || { echo "usage: rebuild.sh <object.o> [...]" >&2; exit 2; }
|
||||
cd "$BUILD"
|
||||
|
||||
echo "[rebuild] compiling $# object(s)"
|
||||
ninja -f build-Release.ninja "$@"
|
||||
|
||||
declare -A LIBS=()
|
||||
for o in "$@"; do
|
||||
d="${o#*CMakeFiles/}"; d="${d%%.dir/*}"
|
||||
lib="obj/Linux/Release/lib$d.a"
|
||||
[ -f "$lib" ] || { echo "[rebuild] no archive $lib for $o" >&2; exit 3; }
|
||||
ar r "$lib" "$o"
|
||||
LIBS["$lib"]=1
|
||||
done
|
||||
for lib in "${!LIBS[@]}"; do ranlib "$lib"; echo "[rebuild] refreshed $lib"; done
|
||||
|
||||
# Lift the link command out of the generated ninja rather than hard-coding it, so
|
||||
# it stays correct if the object or library list ever changes.
|
||||
python3 - <<'PY' > /tmp/xenia_link.sh
|
||||
import re, shlex
|
||||
NJ = "CMakeFiles/impl-Release.ninja"
|
||||
txt = open(NJ).read()
|
||||
m = re.search(r"^build bin/Linux/Release/xenia_canary: (\S+) (.*?)(?=\n\S|\n\n)",
|
||||
txt, re.M | re.S)
|
||||
assert m, "link build statement not found"
|
||||
head = m.group(2)
|
||||
# $in = the explicit inputs, up to the first | (implicit deps)
|
||||
ins = head.split("\n")[0].split(" | ")[0].split(" || ")[0].strip()
|
||||
body = m.group(0)
|
||||
var = dict(re.findall(r"^ (\w+) = (.*)$", body, re.M))
|
||||
cmd = (f'{shlex.quote("/sylph-home/re/toolchain/usr/lib/llvm-18/bin/clang++")} '
|
||||
f'{var["FLAGS"]} {var["LINK_FLAGS"]} {ins} -o {var["TARGET_FILE"]} '
|
||||
f'{var.get("LINK_PATH","")} {var["LINK_LIBRARIES"]}')
|
||||
print(cmd)
|
||||
PY
|
||||
|
||||
echo "[rebuild] linking bin/Linux/Release/xenia_canary"
|
||||
# The image ships runtime sonames only (libgtk-3.so.0, not libgtk-3.so) and no
|
||||
# libstdc++fs.a; linkshim/ supplies both as symlinks + an empty archive.
|
||||
LIBRARY_PATH="$PFX/linkshim${LIBRARY_PATH:+:$LIBRARY_PATH}" bash /tmp/xenia_link.sh
|
||||
ls -la bin/Linux/Release/xenia_canary
|
||||
echo "[rebuild] done"
|
||||
101
tools/re-capture/roll_axis.py
Normal file
@@ -0,0 +1,101 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Roll rate measured ABOUT THE FORWARD AXIS, so pitch cannot leak into it.
|
||||
|
||||
The previous attempt watched a non-forward row of the rotation matrix and got
|
||||
numbers within a few per cent of the pitch run — because pitch moves that row as
|
||||
much as roll does. The fix is to measure the rotation *in the plane perpendicular
|
||||
to forward*: express the new up-vector in the OLD (up, right) basis and take
|
||||
`atan2(u_new·w_old, u_new·u_old)`. Any component along forward — which is what
|
||||
pitch produces — is dropped by construction.
|
||||
|
||||
Each phase is bracketed by HUD screenshots so the mission clock converts wall
|
||||
seconds to game seconds within this run (the ratio has measured 1.260, 1.311 and
|
||||
1.383 in three flights, so it cannot be assumed).
|
||||
|
||||
Usage: roll_axis.py <out.csv> [dwell_s]
|
||||
"""
|
||||
import json, math, os, struct, subprocess, sys, time
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import speed_law
|
||||
|
||||
PAD_FILE = os.environ.get("XENIA_PAD_FILE", "/tmp/xenia_pad.txt")
|
||||
|
||||
|
||||
def pad_state(**kw):
|
||||
"""Write the WHOLE pad state at once.
|
||||
|
||||
The file pad is a snapshot, not a set of independent channels: each write
|
||||
replaces everything, so holding a trigger *while* deflecting a stick has to be
|
||||
one write. (`vgamepad`'s per-channel calls are what this replaces; that device
|
||||
also leaked to the host, see the driver header.) Values are exact — no virtual
|
||||
stick quantisation — which is what makes a single-axis roll hold trustworthy.
|
||||
"""
|
||||
parts = [f"{k}={v}" for k, v in kw.items() if v is not None]
|
||||
tmp = PAD_FILE + ".tmp"
|
||||
with open(tmp, "w") as f:
|
||||
f.write(" ".join(parts))
|
||||
os.replace(tmp, PAD_FILE)
|
||||
|
||||
|
||||
def pad_neutral():
|
||||
pad_state()
|
||||
|
||||
def shot(n):
|
||||
subprocess.run(["screenshot", f"/sylph-home/re/shots/{n}.png"], capture_output=True)
|
||||
|
||||
def rows_at(fd, off, cfg):
|
||||
base = off + cfg["rot_delta"]
|
||||
out = []
|
||||
for r in range(3):
|
||||
v = struct.unpack(">3f", os.pread(fd, 12, base + r * cfg["rot_stride"]))
|
||||
n = math.sqrt(sum(c * c for c in v)) or 1.0
|
||||
out.append(tuple(c / n for c in v))
|
||||
return out
|
||||
|
||||
def dot(a, b):
|
||||
return sum(a[i] * b[i] for i in range(3))
|
||||
|
||||
def main():
|
||||
out_csv = sys.argv[1]
|
||||
dwell = float(sys.argv[2]) if len(sys.argv) > 2 else 8.0
|
||||
cfg = json.load(open("/tmp/nav-live.json"))
|
||||
w, off, nm = speed_law.find_player()
|
||||
if not w:
|
||||
sys.exit("player entity not found after retries")
|
||||
print(f"# locked on {nm}")
|
||||
f_i = cfg.get("fwd_row", 0)
|
||||
u_i, w_i = [r for r in (0, 1, 2) if r != f_i]
|
||||
rows = []
|
||||
for label, (trig, tv) in (("slow", ("LT", 1.0)), ("fast", ("RT", 1.0))):
|
||||
# settle at the target speed with the stick CENTRED (5 s: 2 s was shown
|
||||
# wrong twice this session)
|
||||
pad_state(**{trig.lower(): 255})
|
||||
time.sleep(5.0)
|
||||
shot(f"rollax_{label}_a")
|
||||
# full left-stick X, everything else exactly zero, trigger still held
|
||||
pad_state(**{trig.lower(): 255, "lx": 32767})
|
||||
t0, prev, swept = time.time(), rows_at(w.fd, off, cfg), 0.0
|
||||
seq = []
|
||||
while time.time() - t0 < dwell:
|
||||
time.sleep(0.05)
|
||||
cur = rows_at(w.fd, off, cfg)
|
||||
# roll = rotation of `up` within the OLD (up, right) plane
|
||||
d = math.degrees(math.atan2(dot(cur[u_i], prev[w_i]), dot(cur[u_i], prev[u_i])))
|
||||
swept += abs(d)
|
||||
seq.append((round(time.time() - t0, 3), round(d, 4)))
|
||||
prev = cur
|
||||
pad_state(**{trig.lower(): 255})
|
||||
wall = seq[-1][0]
|
||||
shot(f"rollax_{label}_b")
|
||||
rows += [(label, *s) for s in seq]
|
||||
print(f"# {label:<5} wall {wall:5.2f}s roll swept {swept:7.1f}deg "
|
||||
f"rate {swept / wall:6.1f} deg/wall-s")
|
||||
pad_neutral()
|
||||
with open(out_csv, "w") as f:
|
||||
f.write("phase,t,droll_deg\n")
|
||||
for r in rows:
|
||||
f.write(",".join(str(x) for x in r) + "\n")
|
||||
print("# read HUD TIME off rollax_slow_a/b and rollax_fast_a/b for this run's clock ratio")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
88
tools/re-capture/screen_id.py
Executable file
@@ -0,0 +1,88 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Identify which game screen a screenshot shows, without fixed pixel positions.
|
||||
|
||||
The nav scripts used to test named pixels ("648,221 is white"). That works only
|
||||
while the game image sits at a known place on the root window, and it does not:
|
||||
xenia's GTK window has a menu bar, so on some displays the image is ~25 px lower
|
||||
and every constant reads the wrong row. The failure is silent and expensive — a
|
||||
run sat on a plainly visible MAIN MENU for 300 s reporting "no main menu", and a
|
||||
later one missed the title screen so the attract movie looped for ten minutes.
|
||||
|
||||
So identify screens by WHOLE-IMAGE statistics instead, which no vertical shift
|
||||
(or scale, or letterbox) can move:
|
||||
|
||||
green fraction of pixels that are the game's green UI text/HUD colour
|
||||
white fraction of near-white pixels
|
||||
mean per-channel mean
|
||||
|
||||
Measured on known-good captures of each screen (1280x720, no menu bar):
|
||||
|
||||
title green 0.11% white 7.4% mean (62, 75, 84) blue-ish, bright
|
||||
main menu green 0.04% white 3.5% mean (25, 38, 70) dark, strongly blue
|
||||
in flight green 1.2% white 3.4% mean (54, 39, 37) green HUD everywhere
|
||||
movie green 0% white varies no green at all
|
||||
|
||||
Usage: screen_id.py <png> [--json]
|
||||
Prints one of: title | menu | flight | other, plus the features.
|
||||
"""
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
# Downscaled first: the features are area fractions, so 320x180 gives the same
|
||||
# answer for a fraction of the work (a full-size pure-python pass costs seconds,
|
||||
# and these oracles are polled once a second).
|
||||
W, H = 320, 180
|
||||
|
||||
|
||||
def features(path):
|
||||
raw = subprocess.run(
|
||||
["convert", path, "-alpha", "off", "-resize", f"{W}x{H}!", "-depth", "8",
|
||||
"rgb:-"], capture_output=True).stdout
|
||||
n = len(raw) // 3
|
||||
if not n:
|
||||
return None
|
||||
green = white = 0
|
||||
sr = sg = sb = 0
|
||||
for i in range(0, n * 3, 3):
|
||||
r, g, b = raw[i], raw[i + 1], raw[i + 2]
|
||||
sr += r; sg += g; sb += b
|
||||
if g > 130 and g - r > 45 and g - b > 45:
|
||||
green += 1
|
||||
if r > 230 and g > 230 and b > 230:
|
||||
white += 1
|
||||
return {"green": green / n, "white": white / n,
|
||||
"r": sr / n, "g": sg / n, "b": sb / n}
|
||||
|
||||
|
||||
def classify(f):
|
||||
if f is None:
|
||||
return "none"
|
||||
# Flight first: the HUD paints far more green than any menu.
|
||||
if f["green"] > 0.004:
|
||||
return "flight"
|
||||
# The main menu is dark and strongly blue, with the five white labels.
|
||||
if f["b"] - f["r"] > 30 and f["r"] < 45 and 0.015 < f["white"] < 0.075:
|
||||
return "menu"
|
||||
# The title is brighter, still blue-ish, and carries the green PRESS A text.
|
||||
if f["green"] > 0.0004 and f["b"] > 60 and f["white"] > 0.03:
|
||||
return "title"
|
||||
return "other"
|
||||
|
||||
|
||||
def main():
|
||||
path = sys.argv[1]
|
||||
f = features(path)
|
||||
name = classify(f)
|
||||
if "--json" in sys.argv:
|
||||
print(json.dumps({"screen": name, **(f or {})}))
|
||||
elif f:
|
||||
print(f"{name} green={f['green']:.4f} white={f['white']:.4f} "
|
||||
f"mean=({f['r']:.1f},{f['g']:.1f},{f['b']:.1f})")
|
||||
else:
|
||||
print(name)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
94
tools/xach_dump.py
Executable file
@@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Dump the title's Xbox 360 achievement table (XACH) out of the decrypted `.pe`.
|
||||
|
||||
The XEX embeds an SPA/XDBF resource holding the achievement definitions and one
|
||||
string table per language. `GamePart_Debriefing` awards these (it walks the
|
||||
`ACHIEVEMENTS_REQUIREMENTS` config list and sets bit *i* for entry *i*), and
|
||||
`GamePart_ChallengeMission` gates each challenge mission on a bit of the same
|
||||
space — so this table names the bits.
|
||||
|
||||
Layout, derived here and self-checked (see below):
|
||||
XDBF section header : magic[4] "XACH", version u32, size u32, count u16
|
||||
record, 36 bytes : id u16, name_id u16, unlocked_desc_id u16,
|
||||
locked_desc_id u16, image_id u32, gamerscore u16,
|
||||
pad u16, flags u32, then 16 bytes of zeroes
|
||||
XSTR section header : magic[4] "XSTR", version u32, size u32, count u16
|
||||
string entry : id u16, len u16, `len` bytes of ASCII
|
||||
|
||||
Self-check: the 24 records' gamerscore sums to **1000**, the retail total — a
|
||||
wrong stride or field offset does not add up to a round 1000.
|
||||
|
||||
Usage: xach_dump.py <path-to.pe> [--lang-index N]
|
||||
"""
|
||||
import struct
|
||||
import sys
|
||||
|
||||
|
||||
def find_all(buf, needle):
|
||||
out, i = [], 0
|
||||
while True:
|
||||
i = buf.find(needle, i)
|
||||
if i < 0:
|
||||
return out
|
||||
out.append(i)
|
||||
i += 1
|
||||
|
||||
|
||||
def parse_xstr(d, off):
|
||||
count = struct.unpack_from(">H", d, off + 12)[0]
|
||||
o, table = off + 14, {}
|
||||
for _ in range(count):
|
||||
sid, ln = struct.unpack_from(">HH", d, o)
|
||||
table[sid] = d[o + 4 : o + 4 + ln].decode("ascii", "replace")
|
||||
o += 4 + ln
|
||||
return table
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print(__doc__)
|
||||
return 1
|
||||
d = open(sys.argv[1], "rb").read()
|
||||
|
||||
# The real XACH section is the one whose header count is small (the other
|
||||
# "XACH" hit is the XDBF entry table, which merely *names* it).
|
||||
xach = None
|
||||
for off in find_all(d, b"XACH"):
|
||||
ver, size, count = struct.unpack_from(">IIH", d, off + 4)
|
||||
if ver == 1 and 0 < count < 256 and size >= count * 36:
|
||||
xach = (off, count)
|
||||
break
|
||||
if xach is None:
|
||||
print("no XACH section found", file=sys.stderr)
|
||||
return 2
|
||||
off, count = xach
|
||||
|
||||
# One XSTR per language. Default to English: the only table whose achievement
|
||||
# strings are pure 7-bit ASCII (every other localisation carries accents or
|
||||
# multi-byte text, which our ASCII decode turns into replacement chars).
|
||||
strs = [parse_xstr(d, o) for o in find_all(d, b"XSTR") if len(d) - o > 16]
|
||||
ids = [struct.unpack_from(">HHHH", d, off + 14 + i * 36)[1:] for i in range(count)]
|
||||
lang = int(sys.argv[sys.argv.index("--lang-index") + 1]) if "--lang-index" in sys.argv else None
|
||||
if lang is None:
|
||||
def ascii_score(t):
|
||||
txt = "".join(t.get(s, "") for rec in ids for s in rec)
|
||||
return (txt.count("<EFBFBD>") == 0 and len(txt) > 0, len(txt))
|
||||
lang = max(range(len(strs)), key=lambda i: ascii_score(strs[i]))
|
||||
S = strs[lang]
|
||||
|
||||
print(f"XACH @0x{off:X} {count} achievements (string table #{lang} of {len(strs)})\n")
|
||||
total = 0
|
||||
for i in range(count):
|
||||
b = off + 14 + i * 36
|
||||
aid, nid, did, lid = struct.unpack_from(">HHHH", d, b)
|
||||
gs = struct.unpack_from(">H", d, b + 12)[0]
|
||||
total += gs
|
||||
print(f"id {aid:2d} | bit {aid - 1:2d} | {gs:3d}G | {S.get(nid, '?')}")
|
||||
print(f" unlocked: {S.get(did, '?')}")
|
||||
print(f" locked : {S.get(lid, '?')}")
|
||||
print(f"\ntotal gamerscore = {total}" + (" [OK: retail total]" if total == 1000 else " [!! expected 1000]"))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||