diff --git a/crates/sylpheed-formats/examples/default_owners.rs b/crates/sylpheed-formats/examples/default_owners.rs new file mode 100644 index 0000000..c27a03b --- /dev/null +++ b/crates/sylpheed-formats/examples/default_owners.rs @@ -0,0 +1,50 @@ +//! Scratch analysis: for one IDXD key, list every object that DECLARES it and +//! whether it carries a value on disc or is left at the title-code default. +//! +//! Run: cargo run -p sylpheed-formats --example default_owners -- [KEY...] + +use sylpheed_formats::idxd::IdxdObject; +use sylpheed_formats::pak::PakArchive; + +fn is_number(s: &str) -> bool { + let s = s.strip_suffix(['f', 'F']).unwrap_or(s); + let t = s.strip_prefix(['-', '+']).unwrap_or(s); + !t.is_empty() && t.chars().all(|c| c.is_ascii_digit() || c == '.') +} +fn is_key(s: &str) -> bool { + !s.is_empty() + && s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-') + && s.chars().next().is_some_and(|c| c.is_ascii_alphabetic() || c == '_') + && !is_number(s) +} +fn is_value(s: &str) -> bool { + is_number(s) || !is_key(s) +} + +fn main() { + let root = std::env::var("SYLPHEED_DISC") + .unwrap_or_else(|_| "/home/fabi/RE - Project Sylpheed/sylph_extract".into()); + let wanted: Vec = std::env::args().skip(1).collect(); + let arc = PakArchive::open(std::path::Path::new(&root).join("dat/GP_MAIN_GAME_E.pak")).unwrap(); + + for e in arc.entries() { + let Ok(bytes) = arc.read(e) else { continue }; + let Ok(obj) = IdxdObject::parse(&bytes) else { continue }; + let toks = obj.tokens().to_vec(); + let mut hits: Vec = vec![]; + for (i, t) in toks.iter().enumerate() { + if !wanted.iter().any(|w| w == t) { + continue; + } + let valued = i > 0 && is_value(&toks[i - 1]); + hits.push(if valued { + format!("{t}={}", toks[i - 1]) + } else { + format!("{t}=") + }); + } + if !hits.is_empty() { + println!("0x{:08x} {:<44} {}", obj.schema_hash, obj.identity(), hits.join(" ")); + } + } +} diff --git a/crates/sylpheed-formats/examples/defaulted_fields.rs b/crates/sylpheed-formats/examples/defaulted_fields.rs new file mode 100644 index 0000000..b780150 --- /dev/null +++ b/crates/sylpheed-formats/examples/defaulted_fields.rs @@ -0,0 +1,135 @@ +//! Scratch analysis: which IDXD fields are DECLARED but left at their default on +//! disc, per schema. Those defaults live in title code, so they can only be read +//! from the running game β€” this prints the shopping list for that dynamic capture. +//! +//! Run: cargo run -p sylpheed-formats --example defaulted_fields -- + +use std::collections::{BTreeMap, BTreeSet}; +use sylpheed_formats::idxd::IdxdObject; +use sylpheed_formats::pak::PakArchive; + +fn is_number(s: &str) -> bool { + let s = s.strip_suffix(['f', 'F']).unwrap_or(s); + let t = s.strip_prefix(['-', '+']).unwrap_or(s); + !t.is_empty() && t.chars().all(|c| c.is_ascii_digit() || c == '.') +} + +/// Same shape-test the parser uses: an identifier-looking token that is not a +/// value literal is a field-name key. +fn is_key(s: &str) -> bool { + !s.is_empty() + && s.chars() + .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-') + && s.chars().next().is_some_and(|c| c.is_ascii_alphabetic() || c == '_') + && !is_number(s) +} + +fn is_value(s: &str) -> bool { + is_number(s) || !is_key(s) +} + +fn main() { + let root = std::env::args() + .nth(1) + .unwrap_or_else(|| "/home/fabi/RE - Project Sylpheed/sylph_extract".into()); + let paks: Vec = std::env::args().skip(2).collect(); + let paks = if paks.is_empty() { + vec![ + "dat/GP_MAIN_GAME_E.pak".to_string(), + "dat/GP_HANGAR_ARSENAL.pak".to_string(), + ] + } else { + paks + }; + + for pak_rel in &paks { + let path = std::path::Path::new(&root).join(pak_rel); + let Ok(arc) = PakArchive::open(&path) else { + eprintln!("-- skip {pak_rel} (open failed)"); + continue; + }; + println!("\n================ {pak_rel} ================"); + + // schema -> key -> (n_set, n_defaulted, distinct values, example owners) + type Stat = (usize, usize, BTreeSet, Vec); + let mut per_schema: BTreeMap)> = BTreeMap::new(); + + for e in arc.entries() { + let Ok(bytes) = arc.read(e) else { continue }; + let Ok(obj) = IdxdObject::parse(&bytes) else { + continue; + }; + let ident = obj.identity(); + let toks = obj.tokens().to_vec(); + let entry = per_schema.entry(obj.schema_hash).or_default(); + entry.0 += 1; + // Track which keys this object declares, and whether each is valued. + let mut seen_here: BTreeMap> = BTreeMap::new(); + for (i, t) in toks.iter().enumerate() { + if !is_key(t) { + continue; + } + let prev = if i == 0 { None } else { Some(&toks[i - 1]) }; + let valued = prev.map(|p| is_value(p)).unwrap_or(false); + let v = if valued { Some(toks[i - 1].clone()) } else { None }; + seen_here.entry(t.clone()).or_insert(v); + } + for (k, v) in seen_here { + let s = entry.1.entry(k).or_default(); + match v { + Some(val) => { + s.0 += 1; + if s.2.len() < 12 { + s.2.insert(val); + } + } + None => { + s.1 += 1; + if s.3.len() < 6 { + s.3.push(ident.clone()); + } + } + } + } + } + + for (schema, (n_obj, keys)) in per_schema { + if n_obj < 2 { + continue; + } + let name = match schema { + 0x0426_e81d => "PLAYER", + 0x6ab4_825a => "WEAPON", + 0x43fa_a517 => "UNIT", + 0x3c5b_0549 => "VESSEL", + 0xbd86_d41c => "CHARACTER", + 0x3c9a_e32e => "STAGE", + 0xb412_e6d8 => "MESSAGE", + _ => "?", + }; + let defaulted: Vec<_> = keys + .iter() + .filter(|(_, s)| s.1 > 0) + .collect(); + if defaulted.is_empty() { + continue; + } + println!("\n--- schema 0x{schema:08x} {name} ({n_obj} objects) ---"); + println!( + "{:<30} {:>5} {:>5} {}", + "KEY", "set", "dflt", "values seen (≀12) | owners defaulting" + ); + for (k, (n_set, n_def, vals, owners)) in defaulted { + let vv: Vec<&str> = vals.iter().map(|s| s.as_str()).collect(); + println!( + "{:<30} {:>5} {:>5} {} | {}", + k, + n_set, + n_def, + vv.join(","), + owners.join(",") + ); + } + } + } +} diff --git a/docs/re/INDEX.md b/docs/re/INDEX.md index b35bdb4..0586166 100644 --- a/docs/re/INDEX.md +++ b/docs/re/INDEX.md @@ -21,6 +21,7 @@ Promote to a prose `structures/…md` file when a format needs behavioural notes | Fonts (ttf/otf/ttc) | βœ… | `sylpheed-formats/src/font.rs` | standard OpenType, parsed via ttf-parser | | XBG7 mesh | 🟑/❔ | `sylpheed-formats/src/mesh.rs` + `tests/mesh_disc.rs` ([xbg7](structures/xbg7-mesh.md)) | weapons/props: declaration-driven variable stride (36 models), GPU-confirmed. **Stage containers: 5662 sub-models across 22 stages** via content-anchored grouped pools (`stage_models`). Quantized hero bodies (DeltaSaber `f004`) still declined | | Capital-ship part placement | 🟑 | `sylpheed-formats/src/ship.rs` (static) + [runtime capture](ship-placement-runtime-capture.md) | hull placement static-exact; external parts approximate statically. **Runtime capture** (Canary F10 β†’ VS-constant WorldView) gives ground truth β€” validated on `e106` destroyer; not yet baked into the viewer | +| Weapon fields defaulted on disc | βœ…/🟑 | [runtime DATA SHEET](weapon-datasheet-runtime.md) | The Arsenal Gallery-Mode panel reads the live record: `Ammo Capacity` = `LoadingCount` βœ…, `Max. Lock Ons` = `TriggerShotCount` βœ…. Recovers `TriggerShotCount` for `wep_05`/`wep_60` (both **4**); brackets defaulted `Power`/`MaximumRange` via the letter classes. 9 of ~61 weapons reachable at 5 % save progress | ## Functions / code paths diff --git a/docs/re/captures/weapon-datasheet/asm-hound-smh.png b/docs/re/captures/weapon-datasheet/asm-hound-smh.png new file mode 100644 index 0000000..137fc74 Binary files /dev/null and b/docs/re/captures/weapon-datasheet/asm-hound-smh.png differ diff --git a/docs/re/captures/weapon-datasheet/asm-terrier-smh.png b/docs/re/captures/weapon-datasheet/asm-terrier-smh.png new file mode 100644 index 0000000..96fa035 Binary files /dev/null and b/docs/re/captures/weapon-datasheet/asm-terrier-smh.png differ diff --git a/docs/re/captures/weapon-datasheet/beam-dagger.png b/docs/re/captures/weapon-datasheet/beam-dagger.png new file mode 100644 index 0000000..d679966 Binary files /dev/null and b/docs/re/captures/weapon-datasheet/beam-dagger.png differ diff --git a/docs/re/captures/weapon-datasheet/beam-pilum-bp.png b/docs/re/captures/weapon-datasheet/beam-pilum-bp.png new file mode 100644 index 0000000..ea02378 Binary files /dev/null and b/docs/re/captures/weapon-datasheet/beam-pilum-bp.png differ diff --git a/docs/re/captures/weapon-datasheet/beam-stiletto.png b/docs/re/captures/weapon-datasheet/beam-stiletto.png new file mode 100644 index 0000000..733a550 Binary files /dev/null and b/docs/re/captures/weapon-datasheet/beam-stiletto.png differ diff --git a/docs/re/captures/weapon-datasheet/br-dart-23-rocket.png b/docs/re/captures/weapon-datasheet/br-dart-23-rocket.png new file mode 100644 index 0000000..c14b1fc Binary files /dev/null and b/docs/re/captures/weapon-datasheet/br-dart-23-rocket.png differ diff --git a/docs/re/captures/weapon-datasheet/cn-tomahawk-alpha-rail-gun.png b/docs/re/captures/weapon-datasheet/cn-tomahawk-alpha-rail-gun.png new file mode 100644 index 0000000..4cec88c Binary files /dev/null and b/docs/re/captures/weapon-datasheet/cn-tomahawk-alpha-rail-gun.png differ diff --git a/docs/re/captures/weapon-datasheet/gun-broad-sword-sg1.png b/docs/re/captures/weapon-datasheet/gun-broad-sword-sg1.png new file mode 100644 index 0000000..3a4dac2 Binary files /dev/null and b/docs/re/captures/weapon-datasheet/gun-broad-sword-sg1.png differ diff --git a/docs/re/captures/weapon-datasheet/gun-light-machine-gun-mg1.png b/docs/re/captures/weapon-datasheet/gun-light-machine-gun-mg1.png new file mode 100644 index 0000000..0e34e05 Binary files /dev/null and b/docs/re/captures/weapon-datasheet/gun-light-machine-gun-mg1.png differ diff --git a/docs/re/captures/weapon-datasheet/gun-mg1-full.png b/docs/re/captures/weapon-datasheet/gun-mg1-full.png new file mode 100644 index 0000000..ff5efbc Binary files /dev/null and b/docs/re/captures/weapon-datasheet/gun-mg1-full.png differ diff --git a/docs/re/captures/weapon-datasheet/hangar-mainweapon1-falcon.png b/docs/re/captures/weapon-datasheet/hangar-mainweapon1-falcon.png new file mode 100644 index 0000000..685061d Binary files /dev/null and b/docs/re/captures/weapon-datasheet/hangar-mainweapon1-falcon.png differ diff --git a/docs/re/captures/weapon-datasheet/mpm-buzzard-10am.png b/docs/re/captures/weapon-datasheet/mpm-buzzard-10am.png new file mode 100644 index 0000000..b653232 Binary files /dev/null and b/docs/re/captures/weapon-datasheet/mpm-buzzard-10am.png differ diff --git a/docs/re/captures/weapon-datasheet/mpm-falcon-9am.png b/docs/re/captures/weapon-datasheet/mpm-falcon-9am.png new file mode 100644 index 0000000..0b38298 Binary files /dev/null and b/docs/re/captures/weapon-datasheet/mpm-falcon-9am.png differ diff --git a/docs/re/weapon-datasheet-runtime.md b/docs/re/weapon-datasheet-runtime.md new file mode 100644 index 0000000..1f11b49 --- /dev/null +++ b/docs/re/weapon-datasheet-runtime.md @@ -0,0 +1,150 @@ +# Weapon DATA SHEET β€” runtime capture (Route B) + +**Status:** 🟑 first dynamic capture, 2026-07-28. The Arsenal's *Gallery Mode* panel is a +direct runtime readout of the IDXD weapon record, which makes it an oracle for the fields +the disc leaves **defaulted**. Two field mappings are βœ… `CONFIRMED`; two defaulted values +are recovered at 🟑 `PROBABLE`. Captured from the retail game under Xenia Canary +(software Vulkan, headless) β€” see [the container recipe](#how-this-was-captured). + +## Problem + +`sylpheed-formats::game_data` reads the combat tables out of `dat/GP_MAIN_GAME_E.pak`, but +**a field left at its default value carries no value on disc** β€” the key is present in the +IDXD string pool with no value token in front of it. Those defaults live in title code, so +a static read can only ever say "not set", never *what* the game uses. That is a real hole +for the reimplementation: e.g. `Weapon_DSaber_P_wep_01_Beam` β€” the Delta Saber's starting +beam gun β€” has no `TriggerShotCount` and no `Power` on disc. + +Ruled out first: the defaults are **not** hiding in another pak. `hidden/DefTables.pak` +contains no `WEAPON`-schema (`0x6ab4825a`) objects at all, and the other `GP_MAIN_GAME_*` +paks are localized duplicates of the English one. + +The two scratch analyses that produced the shopping list live next to the crate: +`crates/sylpheed-formats/examples/defaulted_fields.rs` (per-schema: which keys are declared +but defaulted, and by whom) and `examples/default_owners.rs` (per-key: every owner, valued +or ``). + +> Caveat on those tools: they classify a token as a *value* only if it is numeric or +> non-identifier-shaped. **Boolean/enum-valued fields therefore read as `` +> spuriously** (`Yes`, `Homing`, `Single`, `Burst` are identifier-shaped). Every finding +> below concerns numeric fields, where the classification is sound. + +## Finding β€” the DATA SHEET reads the record + +In **ARSENAL β†’ (weapon type) β†’ Y (Gallery Mode)** each entry shows a `DATA SHEET`, and it +is shown for weapons that have **not** been developed yet β€” only fully hidden (dashed) +entries are withheld. The same panel appears in **HANGAR β†’ (hard point)**, with an extra +`Weapon Type` row. + +| DATA SHEET row | IDXD field | Confidence | Evidence | +|---|---|---|---| +| `Ammo Capacity` | `LoadingCount` | βœ… CONFIRMED | 9/9 exact matches across 4 weapon types β€” 3000, 6000, 4000, 600, 300, 600, 45, 18, 75 | +| `Max. Lock Ons` | `TriggerShotCount` | βœ… CONFIRMED | FALCON 9AM = 12 vs `wep_02`'s 12; BUZZARD 10AM = 22 vs `wep_04`'s 22 (two distinctive values, two independent records) | +| `Hard Point` | mount slot | βœ… CONFIRMED | matches the HANGAR slot the weapon is mountable/equipped on (`STILETTO BG I` β†’ NOSE, `FALCON 9AM` β†’ MAIN WEAPON1) | +| `Range Class` | bucket of `MaximumRange` | 🟑 PROBABLE | monotone in the on-disc metres, see the bracket table below | +| `Damage Class` | bucket of `Power` | 🟑 PROBABLE | monotone in the on-disc power, see below | +| `Weight Class` | bucket of the hangar-table `Weight` | ❔ HYPOTHESIS | only one clean pair so far (`wep_33`, Weight 0.3 β†’ "Light") | +| `Speed Class` | ❔ | ❔ | present only on missiles (MPM = A, ASM = B); no on-disc pairing established | +| `Sight Homing`, `Lock on Overlap` | ❔ (`Available` / `–`) | ❔ | the plausible on-disc partners (`Homing`, `OverlapLockon`) are identifier-valued and not yet decoded; **NEEDS-HUMAN** | + +## Finding β€” recovered defaults + +| Weapon | UI name | Field | On disc | **Runtime** | Conf. | +|---|---|---|---|---|---| +| `Weapon_DSaber_P_wep_05_ASMissile` | TERRIER SMH | `TriggerShotCount` | *(defaulted)* | **4** | 🟑 | +| `Weapon_DSaber_P_wep_60_ASMissile` | HOUND SMH | `TriggerShotCount` | *(defaulted)* | **4** | 🟑 | + +Both defaulted weapons read **4**, which is consistent with a single title-code default of +`TriggerShotCount = 4` rather than two per-weapon constants β€” but two samples cannot tell +those apart. ❔ HYPOTHESIS: *the title-code default for `TriggerShotCount` is 4.* It would +be confirmed by a third weapon that defaults the field and also reads 4 (candidates that +were still locked in this save: `wep_27`, `wep_30`, and the seven Beams), or refuted by one +that reads anything else. + +`Power` and `MaximumRange` defaults are **not** exactly recoverable from this panel β€” it +shows only the letter bucket. They are bracketed instead (below). + +## Captured rows + +All values are from one session on save slot 01 (Stage 02, "At Standby", 5 % clear, 4101 P); +`βœ“` marks a value that matches the on-disc record exactly. + +| UI name | Record | Rng | Dmg | Spd | Weight | Ammo | Lock | Homing | Overlap | Hard point | +|---|---|---|---|---|---|---|---|---|---|---| +| LIGHT MACHINE GUN MG I | `wep_33_Gun` | D | E | – | Light | 3000 βœ“ | – | – | – | NOSE WEAPON (Nose) | +| BROAD SWORD SG I | *see below* | E | D | – | Light | 200 | – | – | – | NOSE WEAPON (Nose) | +| STILETTO BG I | `wep_01_Beam` | E | E | – | Light | 6000 βœ“ | – | – | – | NOSE WEAPON (Nose) | +| DAGGER BG2 | `wep_37_Beam` | D | E | – | Light | 4000 βœ“ | – | – | – | NOSE WEAPON (Nose) | +| PILUM BP | `wep_24_Beam` | B | D | – | Light | 600 βœ“ | – | – | – | MAIN WEAPON1 (Fore) | +| FALCON 9AM | `wep_02_Missile` | D | D | A | Heavy | 300 βœ“ | 12 βœ“ | Available | – | MAIN WEAPON1 (Fore) | +| BUZZARD 10AM | `wep_04_Missile` | C | D | A | Heavy | 600 βœ“ | 22 βœ“ | – | Available | MAIN WEAPON1 (Fore) | +| DART 23 ROCKET | `wep_55_Rocket` | C | D | – | Heavy | 600 βœ“ | – | – | – | MAIN WEAPON1 (Fore) | +| TERRIER SMH | `wep_05_ASMissile` | D | C | B | Heavy | 45 βœ“ | **4** | Available | Available | MAIN WEAPON2 (Rear) | +| HOUND SMH | `wep_60_ASMissile` | B | C | B | Medium | 18 βœ“ | **4** | Available | Available | MAIN WEAPON2 (Rear) | +| TOMAHAWK ALPHA RAIL GUN | `wep_03_Cannon` | C | C | – | Medium | 75 βœ“ | – | – | – | MAIN WEAPON3 (Lower) | + +**BROAD SWORD SG I is unidentified β€” NEEDS-HUMAN.** `Ammo Capacity 200` narrows it to +`wep_38` / `wep_41` / `wep_42_Shotgun` (all `LoadingCount = 200`); "SG" and the GUN tab fit +a shotgun. `wep_42` is excluded by range (2500 m would not share class E with `wep_38`/ +`wep_41`'s 3000 m *if* the class is a pure range bucket), leaving `wep_38` vs `wep_41`, +which the panel cannot separate. Its `Damage Class D` also does not fit the `Power` bracket +below (all three shotguns are `Power ≀ 16`, i.e. bucket E), so either the identification or +the "Damage Class = bucket of `Power`" model is wrong for shotguns. + +### Letter-class brackets + +Sorting the identified rows by their on-disc numbers gives monotone, non-overlapping bands: + +``` +Range Class E: 3000 (wep_01) + D: 3500 Β· 4000 Β· 4000 Β· 4000 (wep_33, wep_37, wep_02, wep_05) + C: 4500 Β· 5000 Β· 5000 (wep_55, wep_04, wep_03) + B: 6500 Β· 6500 (wep_24, wep_60) + +Damage Class E: 10 Β· 14 Β· 16 (wep_33, wep_01, wep_37) + D: 70 Β· 75 Β· 100 (wep_24, wep_04, wep_55) + C: 200 Β· 400 (wep_03, wep_05) +``` + +Two consequences for the reimplementation: + +- The **thresholds are not pinned** β€” only bracketed (e.g. the D/C range boundary lies in + (4000, 4500]). More weapons, or a static read of the title-code table, would pin them. +- They **bracket the defaulted numbers**: `wep_02_Missile`'s defaulted `Power` sits in the + D band (β‰ˆ 17…150 by the observed edges) and `wep_60_ASMissile`'s in the C band + (β‰ˆ 150…500). 🟑 PROBABLE, and only as good as the bucket model. + +## How this was captured + +Container recipe (`sylph-container/mission.md`), with two corrections worth keeping: + +- **Skip the intro movie with A.** The brief warns it crashes; it does not. Skipping cuts + boot-to-main-menu from ~13 min to **~1 min** under lavapipe. (Thanks: user tip.) +- **The title screen falls back to the attract loop within a few seconds**, so a + screenshotβ†’lookβ†’tap cycle always misses it. Poll the framebuffer and tap in the same + process. Both are automated in `scratchpad/skip_intro.sh` (movie detected by frame-to- + frame RMSE; title by the green β’Ά glyph at pixel 625,618). +- Under lavapipe the game polls input at its own low frame rate: **a 60 ms d-pad tap is + dropped roughly half the time**; 200 ms is reliable and 300 ms starts to auto-repeat. +- The Hangar hard-point weapon carousel is cycled with d-pad **down**, not left/right. + +Path: title β†’ A β†’ LOAD GAME β†’ slot 01 β†’ *Load game?* **YES** β†’ READY ROOM β†’ ARSENAL β†’ +type tab (LB/RB) β†’ **Y** for the DATA SHEET β†’ d-pad down through the list. + +## Open / next + +- Only **9 weapons of ~61** are revealed at 5 % completion, and none of the four whose + `LoadingCount` is defaulted (`wep_11`, `wep_28`, `wep_36`, `wep_70`) is among them. + Progressing the save (or a later save) is what unlocks the rest β€” the panel itself + already shows undeveloped weapons, so no points need to be spent. +- **Craft (`UNIT`-schema) defaults are not reachable this way.** The Hangar exposes exactly + one craft-level runtime number, `Gross Weight` (a class, "Light"). The defaulted craft + fields (`ShieldRatio`, `BarrelRoll_Count*`, `HoldPosition_*Ratio`, `Slalom_TurnCount_Max`, + `UsingChaffRatio`, …) are AI/flight-model constants with no UI surface; they would need + in-flight behavioural measurement or a guest-memory read, not a menu screenshot. +- The `Sight Homing` / `Lock on Overlap` on-disc partners are still unidentified. + +Screenshots for every row above: `/sylph-home/re/caps/` in the container. + +Evidence PNGs are committed under [`captures/weapon-datasheet/`](captures/weapon-datasheet/) +(64-colour quantized for size; the numbers stay legible). diff --git a/tools/re-capture/README.md b/tools/re-capture/README.md new file mode 100644 index 0000000..b7882e6 --- /dev/null +++ b/tools/re-capture/README.md @@ -0,0 +1,20 @@ +# Runtime-capture harness (sylph-re container) + +Screenshot-driven scripts for reading the running retail game's menus under Xenia +Canary + lavapipe, headless. They assume the container helpers `screenshot`, +`vgamepad`, `pad` are on `$PATH` and `HOME=/sylph-home/re`. + +| Script | What it does | +|---|---| +| `skip_intro.sh` | Boot β†’ main menu, unattended. Taps A only while the intro movie is actually playing (frame-to-frame RMSE), then once at the `PRESS β’Ά BUTTON` title. Static logo screens are left alone, so a stray tap can never land on NEW GAME. | +| `wait_title.sh` | Older variant: wait for the title (green β’Ά glyph at px 625,618) and tap A. Superseded by `skip_intro.sh`. | +| `step.sh` | One Arsenal navigation step (`down`/`up`/`next`/`prev`/`none`) + a compact capture: weapon list stacked over the `DATA SHEET`. | +| `sweep.sh` | Walk a whole weapon-type list, capturing **only** rows that show a `DATA SHEET` β€” locked rows (a "Conditions to Develop" panel) are detected by the brightness of the `Range Class` label box and skipped. | +| `type.sh` | Change weapon-type tab N times (RB) and report the header strip. | +| `hp.sh` / `cyc.sh` | Hangar hard-point carousel: `cyc.sh` steps it (d-pad **down**, not left/right) and captures the Name + `DATA SHEET`. | + +**Input timing under lavapipe:** the game polls input at its own low frame rate, so a +60 ms d-pad tap is dropped roughly half the time. 200 ms is reliable; 300 ms starts to +auto-repeat (two rows per press). + +Findings produced with these: [`docs/re/weapon-datasheet-runtime.md`](../../docs/re/weapon-datasheet-runtime.md). diff --git a/tools/re-capture/cyc.sh b/tools/re-capture/cyc.sh new file mode 100755 index 0000000..25417e3 --- /dev/null +++ b/tools/re-capture/cyc.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +# Step the Hangar weapons-container carousel right and capture the Name+DATA SHEET. +set -u +export HOME=/sylph-home/re +OUT=/sylph-home/re/caps; mkdir -p "$OUT" +vgamepad dpad right; sleep 0.30; vgamepad dpad center; sleep 3.0 +screenshot /tmp/h.png >/dev/null 2>&1 +convert /tmp/h.png -crop 530x480+700+95 +repage "$OUT/$1.png" +echo "$OUT/$1.png" diff --git a/tools/re-capture/hp.sh b/tools/re-capture/hp.sh new file mode 100755 index 0000000..a39fe5b --- /dev/null +++ b/tools/re-capture/hp.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +# Cycle the Hangar hard-point weapon carousel one step right and capture the +# Name + DATA SHEET panel (the mountable-weapon list for that hard point). +set -u +export HOME=/sylph-home/re +OUT=/sylph-home/re/caps; mkdir -p "$OUT" +[ "${1:-}" = "none" ] || { vgamepad dpad right; sleep 0.20; vgamepad dpad center; } +sleep 2.5 +screenshot /tmp/h.png >/dev/null 2>&1 +convert /tmp/h.png -crop 495x460+705+95 +repage "$OUT/$2.png" +echo "$OUT/$2.png" diff --git a/tools/re-capture/skip_intro.sh b/tools/re-capture/skip_intro.sh new file mode 100755 index 0000000..d2b804c --- /dev/null +++ b/tools/re-capture/skip_intro.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# Drive the boot sequence to the main menu without a human in the loop. +# - movie playing (two grabs 0.6s apart differ a lot) -> tap A to skip +# - static screen -> if the green "PRESS (A) BUTTON" glyph is there, tap A and stop +# Static logo screens are left alone, so a stray tap can never land on NEW GAME. +set -u +export HOME=/sylph-home/re +deadline=$(( SECONDS + ${1:-900} )) +while [ $SECONDS -lt $deadline ]; do + screenshot /tmp/f1.png >/dev/null 2>&1; sleep 0.6 + screenshot /tmp/f2.png >/dev/null 2>&1 + d=$(compare -metric RMSE /tmp/f1.png /tmp/f2.png null: 2>&1 | sed 's/ .*//' | cut -d. -f1) + d=${d:-0} + read -r r g b < <(convert /tmp/f2.png -format "%[fx:int(255*p{625,618}.r)] %[fx:int(255*p{625,618}.g)] %[fx:int(255*p{625,618}.b)]" info:) + if [ "$g" -gt 130 ] && [ $((g - r)) -gt 45 ] && [ $((g - b)) -gt 45 ]; then + echo "TITLE at ${SECONDS}s -> A"; vgamepad tap A 250; exit 0 + fi + if [ "$d" -gt 1500 ]; then + echo "movie (rmse $d) at ${SECONDS}s -> skip A"; vgamepad tap A 250; sleep 3 + fi + sleep 1 +done +echo "TIMEOUT"; exit 1 diff --git a/tools/re-capture/step.sh b/tools/re-capture/step.sh new file mode 100755 index 0000000..1126cf5 --- /dev/null +++ b/tools/re-capture/step.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# One Arsenal navigation step + a compact capture: the weapon list (which row is +# selected) stacked over the DATA SHEET numeric rows. Everything else is dropped. +# Usage: step.sh [settle_s] +set -u +export HOME=/sylph-home/re +OUT=/sylph-home/re/caps; mkdir -p "$OUT" +case "${1}" in + down) vgamepad dpad down; sleep 0.20; vgamepad dpad center ;; + up) vgamepad dpad up; sleep 0.20; vgamepad dpad center ;; + next) vgamepad hold RB 0.35 ;; + prev) vgamepad hold LB 0.35 ;; + none) : ;; +esac +sleep "${3:-2.5}" +raw="$OUT/$2.raw.png" +screenshot "$raw" >/dev/null 2>&1 +convert "$raw" -crop 500x420+150+180 +repage /tmp/_list.png +convert "$raw" -crop 500x200+700+100 +repage /tmp/_sheet.png +convert /tmp/_list.png /tmp/_sheet.png -background black -append "$OUT/$2.png" +rm -f "$raw" +echo "$OUT/$2.png" diff --git a/tools/re-capture/sweep.sh b/tools/re-capture/sweep.sh new file mode 100755 index 0000000..447f2c6 --- /dev/null +++ b/tools/re-capture/sweep.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# Walk down a weapon-type list N rows; capture ONLY rows that show a DATA SHEET +# (locked rows show a "Conditions to Develop" panel instead β€” detected by the +# brightness of the "Range Class" label box, ~5 when absent, >>20 when present). +set -u +export HOME=/sylph-home/re +OUT=/sylph-home/re/caps; mkdir -p "$OUT" +pfx=$1; n=${2:-8} +for ((i=1;i<=n;i++)); do + vgamepad dpad down; sleep 0.20; vgamepad dpad center; sleep 3.0 + screenshot /tmp/s.png >/dev/null 2>&1 + m=$(convert /tmp/s.png -crop 150x24+730+116 +repage -colorspace Gray -format "%[fx:int(255*mean)]" info:) + if [ "$m" -gt 45 ]; then + convert /tmp/s.png -crop 500x420+150+180 +repage /tmp/_l.png + convert /tmp/s.png -crop 500x200+700+100 +repage /tmp/_s.png + convert /tmp/_l.png /tmp/_s.png -background black -append "$OUT/$pfx-r$i.png" + echo "row $i: DATA SHEET -> $OUT/$pfx-r$i.png" + else + echo "row $i: locked (mean $m)" + fi +done diff --git a/tools/re-capture/type.sh b/tools/re-capture/type.sh new file mode 100755 index 0000000..df06a6f --- /dev/null +++ b/tools/re-capture/type.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +# Change Arsenal weapon type N times (RB) and report the header strip only. +set -u +export HOME=/sylph-home/re +OUT=/sylph-home/re/caps; mkdir -p "$OUT" +n=${1:-1} +for ((i=0;i/dev/null 2>&1 +convert /tmp/hdr.png -crop 420x50+130+148 +repage "$OUT/hdr.png" +echo "$OUT/hdr.png" diff --git a/tools/re-capture/wait_title.sh b/tools/re-capture/wait_title.sh new file mode 100755 index 0000000..c39ba8f --- /dev/null +++ b/tools/re-capture/wait_title.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# Poll the framebuffer for the "PRESS (A) BUTTON" title screen (the green A glyph +# at ~625,618) and tap A the instant it appears β€” the title auto-returns to the +# attract loop after a few seconds, which is why a human-paced tap misses it. +set -u +export HOME=/sylph-home/re +TMP=/tmp/title-probe.png +deadline=$(( SECONDS + ${1:-900} )) +while [ $SECONDS -lt $deadline ]; do + if screenshot "$TMP" >/dev/null 2>&1; then + read -r r g b < <(convert "$TMP" -format "%[fx:int(255*p{625,618}.r)] %[fx:int(255*p{625,618}.g)] %[fx:int(255*p{625,618}.b)]" info:) + if [ "$g" -gt 130 ] && [ $((g - r)) -gt 45 ] && [ $((g - b)) -gt 45 ]; then + echo "TITLE detected (rgb $r,$g,$b) at ${SECONDS}s β€” tapping A" + vgamepad tap A 200 + exit 0 + fi + fi + sleep 1 +done +echo "TIMEOUT: title not seen" +exit 1