diff --git a/crates/sylpheed-formats/tests/phase_objectives_disc.rs b/crates/sylpheed-formats/tests/phase_objectives_disc.rs new file mode 100644 index 0000000..9ada940 --- /dev/null +++ b/crates/sylpheed-formats/tests/phase_objectives_disc.rs @@ -0,0 +1,46 @@ +//! Per-phase MAIN objectives, straight from the localisation index. +//! +//! The mission-phase work went looking for what a phase asks the player to do +//! and reconstructed it from `SUBObjective_local_string.tbl` — the *bonus* +//! objectives — plus the guide script. `TextIndex` already indexes the main +//! ones under `S_P_Objective_`, so this pins them as a disc test and +//! makes the per-phase goals a checkable artifact rather than an inference. + +use sylpheed_formats::{localization::TextIndex, PakArchive}; + +fn pak() -> Option { + let disc = std::env::var("SYLPHEED_DISC").ok()?; + PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")).ok() +} + +#[test] +fn stage02_has_a_main_objective_for_each_phase() { + let Some(pak) = pak() else { + eprintln!("SYLPHEED_DISC unset - skipping"); + return; + }; + let text = TextIndex::build(&pak); + assert!(text.len() > 0, "text index is empty"); + + for stage in ["S01", "S02"] { + for phase in 1..=3u32 { + let objs = text.objectives(stage, phase); + eprintln!("{stage} phase {phase}: {} objective line(s)", objs.len()); + for o in &objs { + eprintln!(" {o}"); + } + for h in text.hints(stage, phase) { + eprintln!(" hint: {h}"); + } + for l in text.lose_conditions(stage, phase) { + eprintln!(" lose: {l}"); + } + } + } + // Phase 1 of Stage 02 is the case the wave work could not observe; if the + // game states its goal at all, it is here. + assert!( + !text.objectives("S02", 1).is_empty(), + "no main objective text for Stage 02 phase 1" + ); +} diff --git a/docs/re/BACKLOG.md b/docs/re/BACKLOG.md index 4c22c57..a1bd073 100644 --- a/docs/re/BACKLOG.md +++ b/docs/re/BACKLOG.md @@ -560,6 +560,20 @@ search cannot find a *schedule*. the pilot (game-playing, uncertain); (2) accept the static answer (structure, rosters, routes, objective texts all decoded and cross-confirmed; only the *trigger* is inferred); (3) one 40+ min chained run betting against freezes. +* ✅✅ **(2026-08-24) THE OBJECTIVES, FROM THE GAME'S OWN TEXT** — pinned by + `tests/phase_objectives_disc.rs`; `TextIndex::objectives(stage, phase)` already + existed. **S02 phase 1: "Shoot down all invading enemy fighters while watching + out for attacks on the ACROPOLIS"** (lose: ACROPOLIS sunk). **Phase 2: "Protect + the CALIBAN until it has entered the safe zone"** — *positional, not a kill + count*, so phase triggers are NOT all the same kind. **Phase 3: "Destroy the + interstellar cruise missiles headed for Schlos Base"** (matches the 9 + `ISCMissile` + `SUBOBJ_013`). ✅ **`[OB]` = objective marker** — hints say *"Red + mission markers [OB] indicate your targets"*, so **`REMAINING OB` at + `0xbdb59668` is phase-1 progress** and is the right signal to watch, not + `deployed`. Closes a loop to the first session. 🔴 **Method lesson: the crate + already knew this** — several iterations reconstructed it the hard way; + `grep -rl TextIndex crates/` would have saved the detour. ❔ Minor: `ixud.rs` + says UTF-16BE, `localization.rs` says LE — one comment is wrong. 🔴 `SYLPH_HZ=3` refuted as a lever: it gave the LOWEST frame rate (8/s) with the most kills, so pilot polling is not the throttle. * ~~🚧 BLOCKER: t=210/240 unreachable in one turn~~ — **superseded, see above**; diff --git a/docs/re/mission-phase-objectives.md b/docs/re/mission-phase-objectives.md new file mode 100644 index 0000000..849d702 --- /dev/null +++ b/docs/re/mission-phase-objectives.md @@ -0,0 +1,71 @@ +# What each phase asks for — from the game's own objective text + +Status: ✅ pinned by a disc test (`crates/sylpheed-formats/tests/phase_objectives_disc.rs`). + +`TextIndex` in `sylpheed-formats::localization` already indexes per-phase objective +text under `S_P_Objective_`, with hints and lose conditions alongside. +Querying it settles what the wave work spent many iterations circling. + +## Stage 02 + +| phase | objective | lose condition | +|---|---|---| +| 1 | **"Shoot down all invading enemy fighters while watching out for attacks on the ACROPOLIS."** | The ACROPOLIS is sunk | +| 2 | **"Protect the Admiral's ship the CALIBAN until it has entered the safe zone."** | The CALIBAN is sunk | +| 3 | **"Destroy the interstellar cruise missiles headed for Schlos Base."** | Schlos Base is destroyed | + +Stage 01 phase 1 is *"Repel the enemy's surprise attack."*, phase 2 *"Cooperate +with your allies to repel the enemy units."* + +## ✅ This explains the phase advances, and they are not all the same kind + +* **Phase 1 is a kill-all-marked-fighters gate.** Not turrets — *fighters*. Every + run in [mission-arrival-watch.md](mission-arrival-watch.md) killed turrets + almost exclusively, so no run came close to satisfying it. +* **Phase 2 is positional, not a kill count** — it ends when the CALIBAN reaches + a safe zone. That is a completely different trigger type, and it means "what + advances a phase" never had one answer. +* **Phase 3 is destroy-the-missiles**, matching phase 3's roster of exactly nine + `UN_e201_ADAN_ISCMissile` and `SUBOBJ_013`. + +## ✅ `[OB]` is the objective marker — closing a loop to the first session + +The hints name the mechanic outright: + +> *"Red mission markers **[OB]** indicate your targets."* +> *"Take out these enemy units."* + +**`REMAINING OB`** — the HUD counter hunted in the earliest iterations of this +work and located at `0xbdb59668` — is therefore the count of **remaining marked +objective targets**, i.e. phase-1 progress itself. The reframing recorded back +then ("the counter is not a roster") was right, and this names what it *is*. + +That also makes it the correct signal to watch for a phase advance: not +`deployed`, which only changes when the next phase deploys, but `REMAINING OB` +reaching zero, which is the cause. + +## 🔴 Method lesson: the crate already knew this + +`localization.rs` has had `objectives(stage, phase)`, `hints` and +`lose_conditions` for some time. Several iterations were spent reconstructing +the same information the hard way — decoding `SUBObjective_local_string.tbl` by +hand, reading the guide script, inferring the phase-1 goal from +*"the attackers with the orange markers"*. + +The reconstruction was not wrong, and it produced the IXUD/UTF-16BE and +`language\` prefix findings independently. But **the corpus should have been +searched before the disc was**: `grep -rl TextIndex crates/` would have saved +the detour. Recorded because the same reflex will apply to the next unknown. + +Note also a discrepancy worth resolving: `ixud.rs` documents the pool as +UTF-16**BE** and `localization.rs` as UTF-16**LE**. Both work on their own +inputs, so one comment is wrong, or the two consumers genuinely differ. Not +investigated. + +## What is still not observed + +The phase-1 advance itself. The objective is now known exactly, and +[mission-objectives-text.md](mission-objectives-text.md) quantifies the cost: +the marked fighters are ~16 craft and the pilot manages about two per five +minutes. Nothing here changes that arithmetic — it only means the target list is +now certain rather than guessed.