diff --git a/crates/sylpheed-formats/examples/challenge_screen.rs b/crates/sylpheed-formats/examples/challenge_screen.rs new file mode 100644 index 0000000..a861bd6 --- /dev/null +++ b/crates/sylpheed-formats/examples/challenge_screen.rs @@ -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 -- + +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} ====="), + } + } + } +} diff --git a/docs/re/captures/challenge-screen-config.txt b/docs/re/captures/challenge-screen-config.txt new file mode 100644 index 0000000..ea0fe3b --- /dev/null +++ b/docs/re/captures/challenge-screen-config.txt @@ -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 diff --git a/docs/re/challenge-mission-gate.md b/docs/re/challenge-mission-gate.md index b8382d3..a621487 100644 --- a/docs/re/challenge-mission-gate.md +++ b/docs/re/challenge-mission-gate.md @@ -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 1–16 and 24–29 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` | +| 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`, 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: @@ -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 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: @@ -151,7 +226,9 @@ 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 -- +cargo run --release -q -p sylpheed-formats --example challenge_map -- +cargo run --release -q -p sylpheed-formats --example challenge_screen -- 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 ```