re(challenge): the unlock is a bit test, and the mission table is on disc

Static only. Bounding each GamePart's code block by its factory creator thunk
(id -> creator recovered for 22 of 24 registrations at 0x8280C000-0x8280F800)
puts GamePart_ChallengeMission at 0x82187E60-0x8218CF10. Resolving every string
that block references gives the screen's config schema, and the record itself is
on disc -- tables.pak schema 54a10697, one copy per language, English entry #64.

Six missions: TimeAttack (record Time), ScoreAttack (record Points) and
Extra01..Extra04, each with MISSION_ID / REQUIREMENT / REQUIREMENT_DESC /
THUMBNAIL / STAGE_DESC / NEW_STAGE and a NORMAL_BUTTON / GRAY_BUTTON pair -- so
the screen always lists all six and greys out what is not earned.

THE GATE (0x82189970-0x821899D8), read off the code:

  REQUIREMENT absent      -> available
  REQUIREMENT == "Always" -> available
  else n = atoi(REQUIREMENT)
      n == 0              -> locked
      n <  24             -> test bit n      of the word at singleton+80
      n >= 24             -> test bit (n-24) of the word at singleton+1956

The singleton is 0x821707C0 (lazy, global 0x828F48BC). So availability is one
bit in a progress bitfield and REQUIREMENT is a bit INDEX -- not a stage number,
score or difficulty.

Values per mission are 🟡: the pool's numeric tokens are 16/25/26/27/29 and
24/28 already appear earlier as font metrics, so they would be deduped -- which
fits 24..29 but IDXD dedup makes positional pairing unsound here, so it is
recorded as a hypothesis, not a table.

Negative: the requirement TEXT is not in GP_CHALLENGE.pak (TextIndex over it =
0 entries; its only prose is embedded font copyright). Its PATH is a per-language
branch the loader does not currently reproduce.

Next: three stores to +1956 sit in 0x822C7DD0 / 0x822C8748, the same region as
the save serializer 0x822C00E8 -- if the bits are save-backed, a hand-written
save unlocks all six challenge missions and the last 42 units become one run.
This commit is contained in:
2026-08-13 19:14:48 +00:00
parent 10a96844ba
commit 8ecd70f1bc
3 changed files with 908 additions and 3 deletions

View 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} ====="),
}
}
}
}

View 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

View File

@@ -109,7 +109,82 @@ trap) but it is **not proven** — proving it needs a run.
range", and the kind field is the likely gate. It has never been tested — only range", and the kind field is the likely gate. It has never been tested — only
116 and 2429 were. 116 and 2429 were.
## 5. The unlock condition ❔ ## 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` |
| 36 | `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`, lazily constructed
behind the global 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 What the six requirement values are 🟡
The record's numeric tokens are `16`, `25`, `26`, `27`, `29`, and `24`/`28` are
already in the pool earlier (they double as font metrics), so they would be **deduped
away** if used. That is consistent with the six values being `24``29` — the same
range as the challenge stage records — but **IDXD dedupes the string pool, so
positional key/value pairing is not sound here** (the same trap the movie/subtitle map
hit). Recorded as a hypothesis: reading the values properly needs the record's binary
index section, not the pool.
**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.3 Why this matters operationally
If word A / word B are restored from the save, a **hand-written save with those bits
set unlocks all six challenge missions** — and the savegame round-trip is already
solved. That would turn the last 42 units of the Route-B harvest into one run instead
of an unreachable menu. Three `stw`s to `+1956` sit in `0x822C7DD0` / `0x822C8748`,
i.e. the same code region as the save serializer (`0x822C00E8`) and deserializer
(`0x822C0380`) — suggestive, **not yet checked**. That is the next step.
## 6. The unlock condition, from the strings ❔
Three strings say challenge missions are *announced*, not menu-browsed: Three strings say challenge missions are *announced*, not menu-browsed:
@@ -126,7 +201,7 @@ config lookup, exactly like the save screen's sprite keys, so there is nothing t
grep back to. Naming the flag needs the challenge-availability check disassembled grep back to. Naming the flag needs the challenge-availability check disassembled
from the screen side. from the screen side.
## 6. What this makes actionable ## 7. What this makes actionable
`roster_target` against the harvested CSV, with the stage families now named: `roster_target` against the harvested CSV, with the stage families now named:
@@ -151,7 +226,9 @@ rest are not in any stage roster table and need a different lever.
## Reproduce ## Reproduce
```bash ```bash
cargo run --release -q -p sylpheed-formats --example challenge_map -- <disc-root> 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 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 0x82185ed0 0x82185f10 # the same switch, second site
python3 xenia-rs/zq.py dis 0x82189860 0x821899f0 # the challenge availability gate
``` ```