re: runtime DATA SHEET reads the live weapon record (Route B, first capture)
Drove the retail game headless (Canary + lavapipe, vgamepad) to the Ready Room Arsenal and read the Gallery-Mode DATA SHEET for every weapon the 5%-progress save reveals. Two UI->field mappings are CONFIRMED against weapons whose values ARE on disc: Ammo Capacity == LoadingCount (9/9 exact matches) Max. Lock Ons == TriggerShotCount (FALCON 9AM 12 == wep_02; BUZZARD 22 == wep_04) That makes the panel an oracle for fields the disc leaves defaulted, and it is shown for undeveloped weapons too, so no points need spending. Recovered TriggerShotCount for wep_05 and wep_60 (both 4, on-disc absent), and bracketed the defaulted Power / MaximumRange via the Range/Damage letter classes. Ruled out first: the defaults are not in hidden/DefTables.pak (no WEAPON-schema objects there) nor in the localized GP_MAIN_GAME_* duplicates. Also adds the two analysis examples that produce the shopping list of defaulted fields, the screenshot harness used to drive the menus, and the evidence PNGs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
50
crates/sylpheed-formats/examples/default_owners.rs
Normal file
@@ -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> [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<String> = 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<String> = 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}=<DEFAULT>")
|
||||
});
|
||||
}
|
||||
if !hits.is_empty() {
|
||||
println!("0x{:08x} {:<44} {}", obj.schema_hash, obj.identity(), hits.join(" "));
|
||||
}
|
||||
}
|
||||
}
|
||||
135
crates/sylpheed-formats/examples/defaulted_fields.rs
Normal file
@@ -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 -- <disc-root>
|
||||
|
||||
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<String> = 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<String>, Vec<String>);
|
||||
let mut per_schema: BTreeMap<u32, (usize, BTreeMap<String, Stat>)> = 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<String, Option<String>> = 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(",")
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
BIN
docs/re/captures/weapon-datasheet/asm-hound-smh.png
Normal file
|
After Width: | Height: | Size: 98 KiB |
BIN
docs/re/captures/weapon-datasheet/asm-terrier-smh.png
Normal file
|
After Width: | Height: | Size: 51 KiB |
BIN
docs/re/captures/weapon-datasheet/beam-dagger.png
Normal file
|
After Width: | Height: | Size: 104 KiB |
BIN
docs/re/captures/weapon-datasheet/beam-pilum-bp.png
Normal file
|
After Width: | Height: | Size: 117 KiB |
BIN
docs/re/captures/weapon-datasheet/beam-stiletto.png
Normal file
|
After Width: | Height: | Size: 112 KiB |
BIN
docs/re/captures/weapon-datasheet/br-dart-23-rocket.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
docs/re/captures/weapon-datasheet/cn-tomahawk-alpha-rail-gun.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
docs/re/captures/weapon-datasheet/gun-broad-sword-sg1.png
Normal file
|
After Width: | Height: | Size: 45 KiB |
BIN
docs/re/captures/weapon-datasheet/gun-light-machine-gun-mg1.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
docs/re/captures/weapon-datasheet/gun-mg1-full.png
Normal file
|
After Width: | Height: | Size: 102 KiB |
BIN
docs/re/captures/weapon-datasheet/hangar-mainweapon1-falcon.png
Normal file
|
After Width: | Height: | Size: 159 KiB |
BIN
docs/re/captures/weapon-datasheet/mpm-buzzard-10am.png
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
docs/re/captures/weapon-datasheet/mpm-falcon-9am.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
150
docs/re/weapon-datasheet-runtime.md
Normal file
@@ -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 `<DEFAULT>`).
|
||||
|
||||
> 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 `<DEFAULT>`
|
||||
> 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).
|
||||
20
tools/re-capture/README.md
Normal file
@@ -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).
|
||||
9
tools/re-capture/cyc.sh
Executable file
@@ -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"
|
||||
11
tools/re-capture/hp.sh
Executable file
@@ -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"
|
||||
23
tools/re-capture/skip_intro.sh
Executable file
@@ -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
|
||||
22
tools/re-capture/step.sh
Executable file
@@ -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 <down|up|next|prev|none> <tag> [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"
|
||||
21
tools/re-capture/sweep.sh
Executable file
@@ -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
|
||||
11
tools/re-capture/type.sh
Executable file
@@ -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<n;i++)); do vgamepad hold RB 0.35; sleep 2.5; done
|
||||
sleep 1.5
|
||||
screenshot /tmp/hdr.png >/dev/null 2>&1
|
||||
convert /tmp/hdr.png -crop 420x50+130+148 +repage "$OUT/hdr.png"
|
||||
echo "$OUT/hdr.png"
|
||||
21
tools/re-capture/wait_title.sh
Executable file
@@ -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
|
||||