1use crate::crc::type_id;
18use crate::crs::{self, Reader};
19use crate::error::{format_err, Result};
20
21#[derive(Debug, Clone, Default, PartialEq)]
23pub struct Achievement {
24 pub strings: Option<String>,
27 pub title: String,
30 pub description: String,
32 pub sprites: Option<String>,
35 pub sprite: String,
37 pub goals: Vec<Option<String>>,
40}
41
42fn achievement(r: &mut Reader) -> Result<Achievement> {
43 r.block(|r, _| {
44 let strings = r.external_ref()?;
45 let title = r.string()?;
46 let description = r.string()?;
47 let sprites = r.external_ref()?;
48 let sprite = r.string()?;
49 let goals = (0..r.count()?)
50 .map(|_| r.external_ref())
51 .collect::<Result<_>>()?;
52 Ok(Achievement {
53 strings,
54 title,
55 description,
56 sprites,
57 sprite,
58 goals,
59 })
60 })
61}
62
63#[derive(Debug, Clone, PartialEq)]
65pub enum GoalKind {
66 Building {
69 all: bool,
71 buildings: Vec<String>,
73 },
74 Population(i32),
77 Stat {
80 e30: i32,
82 e34: i32,
84 e38: i32,
86 amount: i32,
88 flag: bool,
90 r48: String,
92 r60: String,
94 e78: i32,
96 i7c: i32,
98 },
99 Time {
102 years: i32,
104 within: bool,
106 },
107 FinishTutorial(Option<String>),
110}
111
112#[derive(Debug, Clone, PartialEq)]
114pub struct Goal {
115 pub strings: Option<String>,
117 pub text: String,
119 pub kind: GoalKind,
121}
122
123pub fn goal(data: &[u8]) -> Result<Goal> {
125 let (_, v) = crs::parse(data, |r, h| {
126 let (strings, text) = r.block(|r, _| Ok((r.external_ref()?, r.string()?)))?;
127 let kind = r.block(|r, _| {
128 Ok(match h.type_id {
129 t if t == type_id("BuildingGoal") => GoalKind::Building {
130 all: r.bool()?,
131 buildings: (0..r.count()?).map(|_| r.string()).collect::<Result<_>>()?,
132 },
133 t if t == type_id("PopulationGoal") => GoalKind::Population(r.i32()?),
134 t if t == type_id("StatGoal") => GoalKind::Stat {
135 e30: r.i32()?,
136 e34: r.i32()?,
137 e38: r.i32()?,
138 amount: r.i32()?,
139 flag: r.bool()?,
140 r48: r.string()?,
141 r60: r.string()?,
142 e78: r.i32()?,
143 i7c: r.i32()?,
144 },
145 t if t == type_id("TimeGoal") => GoalKind::Time {
146 years: r.i32()?,
147 within: r.bool()?,
148 },
149 t if t == type_id("FinishTutorialGoal") => {
150 GoalKind::FinishTutorial(r.external_ref()?)
151 }
152 t => return format_err(format!("unknown goal type {t:#010x}")),
153 })
154 })?;
155 Ok(Goal {
156 strings,
157 text,
158 kind,
159 })
160 })?;
161 Ok(v)
162}
163
164pub fn achievement_description(data: &[u8]) -> Result<Achievement> {
166 let (_, v) = crs::parse(data, |r, h| {
167 if h.type_id != type_id("Achievement") {
168 return format_err(format!("expected Achievement, found {:#010x}", h.type_id));
169 }
170 achievement(r)
171 })?;
172 Ok(v)
173}
174
175#[derive(Debug, Clone, Default, PartialEq)]
177pub struct Scenario {
178 pub achievement: Achievement,
180 pub i60: i32,
183 pub i64: i32,
185 pub town_name: String,
188 pub r70: Option<String>,
191 pub r78: Option<String>,
193 pub r80: Option<String>,
195 pub save: String,
201 pub b68: bool,
204 pub b69: bool,
206}
207
208pub fn scenario(data: &[u8]) -> Result<Scenario> {
210 let (_, v) = crs::parse(data, |r, h| {
211 if h.type_id != type_id("Scenario") {
212 return format_err(format!("expected Scenario, found {:#010x}", h.type_id));
213 }
214 let achievement = achievement(r)?;
215 r.block(|r, _| {
216 Ok(Scenario {
217 achievement,
218 i60: r.i32()?,
219 i64: r.i32()?,
220 town_name: r.string()?,
221 r70: r.external_ref()?,
222 r78: r.external_ref()?,
223 r80: r.external_ref()?,
224 save: r.string()?,
225 b68: r.bool()?,
226 b69: r.bool()?,
227 })
228 })
229 })?;
230 Ok(v)
231}
232
233fn refs(r: &mut Reader) -> Result<Vec<Option<String>>> {
234 (0..r.count()?).map(|_| r.external_ref()).collect()
235}
236
237#[derive(Debug, Clone, Default, PartialEq)]
243pub struct Tutorial {
244 pub steps: Vec<Option<String>>,
246 pub window: Option<String>,
248 pub next: i32,
250}
251
252#[derive(Debug, Clone, Default, PartialEq)]
258pub struct TutorialStep {
259 pub checkpoint: bool,
261 pub strings: Option<String>,
263 pub pre: String,
265 pub post: String,
267 pub images: Vec<Option<String>>,
269 pub setups: Vec<Option<String>>,
271 pub goals: Vec<Option<String>>,
273}
274
275#[derive(Debug, Clone, PartialEq)]
277pub enum TutorialPart {
278 Tutorial(Tutorial),
280 Step(TutorialStep),
282 SetupDialog {
285 a: Vec<String>,
287 b: Vec<String>,
289 s40: String,
291 r50: Option<String>,
293 i48: i32,
295 component: String,
297 i70: i32,
299 },
300 SetupCamera([i32; 4]),
302 SetupGameSpeed {
304 speed: i32,
306 b24: bool,
308 },
309 SetupRestrictPlayer {
311 values: [i32; 5],
313 s38: String,
315 s50: String,
317 },
318 GoalButton {
320 s20: String,
322 b28: bool,
324 },
325 GoalCount {
327 e20: i32,
329 i24: i32,
331 s30: String,
333 b28: bool,
335 s48: String,
337 },
338 GoalDialog {
340 s20: String,
342 component: String,
344 },
345 GoalGameSpeed(i32),
347 Image {
350 i28: i32,
352 i2c: i32,
354 r20: Option<String>,
356 extra: ImageExtra,
358 },
359}
360
361#[derive(Debug, Clone, PartialEq)]
363pub enum ImageExtra {
364 None,
366 HotKey(String),
368 InputKey(i32),
370 ToolbarIcon(String),
372}
373
374pub fn tutorial_part(data: &[u8]) -> Result<TutorialPart> {
376 let (_, v) = crs::parse(data, |r, h| {
377 let t = h.type_id;
378 let is = |n: &str| t == type_id(n);
379 let goal = [
382 "TutorialGoalButton",
383 "TutorialGoalCount",
384 "TutorialGoalDialog",
385 "TutorialGoalGameSpeed",
386 ]
387 .iter()
388 .any(|n| is(n));
389 let image = [
390 "TutorialImageHotKey",
391 "TutorialImageInputKey",
392 "TutorialImageToolbarIcon",
393 ]
394 .iter()
395 .any(|n| is(n));
396 if goal {
397 r.block(|_, _| Ok(()))?;
398 }
399 let base = if image {
400 Some(r.block(|r, _| Ok((r.i32()?, r.i32()?, r.external_ref()?)))?)
401 } else {
402 None
403 };
404 r.block(|r, _| {
405 Ok(if is("Tutorial") {
406 let steps = refs(r)?;
407 TutorialPart::Tutorial(Tutorial {
408 steps,
409 window: r.external_ref()?,
410 next: r.i32()?,
411 })
412 } else if is("TutorialStep") {
413 TutorialPart::Step(TutorialStep {
414 checkpoint: r.bool()?,
415 strings: r.external_ref()?,
416 pre: r.string()?,
417 post: r.string()?,
418 images: refs(r)?,
419 setups: refs(r)?,
420 goals: refs(r)?,
421 })
422 } else if is("TutorialSetupDialog") {
423 let a = (0..r.count()?).map(|_| r.string()).collect::<Result<_>>()?;
424 let b = (0..r.count()?).map(|_| r.string()).collect::<Result<_>>()?;
425 TutorialPart::SetupDialog {
426 a,
427 b,
428 s40: r.string()?,
429 r50: r.external_ref()?,
430 i48: r.i32()?,
431 component: r.string()?,
432 i70: r.i32()?,
433 }
434 } else if is("TutorialSetupCamera") {
435 TutorialPart::SetupCamera([r.i32()?, r.i32()?, r.i32()?, r.i32()?])
436 } else if is("TutorialSetupGameSpeed") {
437 TutorialPart::SetupGameSpeed {
438 speed: r.i32()?,
439 b24: r.bool()?,
440 }
441 } else if is("TutorialSetupRestrictPlayer") {
442 TutorialPart::SetupRestrictPlayer {
443 values: [r.i32()?, r.i32()?, r.i32()?, r.i32()?, r.i32()?],
444 s38: r.string()?,
445 s50: r.string()?,
446 }
447 } else if is("TutorialGoalButton") {
448 TutorialPart::GoalButton {
449 s20: r.string()?,
450 b28: r.bool()?,
451 }
452 } else if is("TutorialGoalCount") {
453 TutorialPart::GoalCount {
454 e20: r.i32()?,
455 i24: r.i32()?,
456 s30: r.string()?,
457 b28: r.bool()?,
458 s48: r.string()?,
459 }
460 } else if is("TutorialGoalDialog") {
461 TutorialPart::GoalDialog {
462 s20: r.string()?,
463 component: r.string()?,
464 }
465 } else if is("TutorialGoalGameSpeed") {
466 TutorialPart::GoalGameSpeed(r.i32()?)
467 } else if is("TutorialImage")
468 || is("TutorialImageHotKey")
469 || is("TutorialImageInputKey")
470 || is("TutorialImageToolbarIcon")
471 {
472 let (i28, i2c, r20) = match base.clone() {
473 Some(b) => b,
474 None => (r.i32()?, r.i32()?, r.external_ref()?),
475 };
476 let extra = if is("TutorialImageHotKey") {
477 ImageExtra::HotKey(r.string()?)
478 } else if is("TutorialImageInputKey") {
479 ImageExtra::InputKey(r.i32()?)
480 } else if is("TutorialImageToolbarIcon") {
481 ImageExtra::ToolbarIcon(r.string()?)
482 } else {
483 ImageExtra::None
484 };
485 TutorialPart::Image {
486 i28,
487 i2c,
488 r20,
489 extra,
490 }
491 } else {
492 return format_err(format!("not a tutorial part: {t:#010x}"));
493 })
494 })
495 })?;
496 Ok(v)
497}
498
499#[cfg(test)]
500mod tests {
501 use super::*;
502
503 #[test]
505 fn every_achievement_and_tutorial_parses() {
506 let root =
507 std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../game-data/extracted");
508 let mut counts = std::collections::BTreeMap::<&str, usize>::new();
509 for dir in ["Game/Achievement", "Game/Scenario"] {
510 let Ok(entries) = std::fs::read_dir(root.join(dir)) else {
511 return;
512 };
513 for e in entries {
514 let p = e.unwrap().path();
515 let b = std::fs::read(&p).unwrap();
516 let Ok(h) = crs::header(&b) else { continue };
517 let name = p.file_name().unwrap().to_string_lossy().to_string();
518 let t = h.type_id;
519 let kind = if t == type_id("Achievement") {
520 achievement_description(&b).map(|_| "achievement")
521 } else if t == type_id("Scenario") {
522 scenario(&b).map(|_| "scenario")
523 } else if [
524 "BuildingGoal",
525 "PopulationGoal",
526 "StatGoal",
527 "TimeGoal",
528 "FinishTutorialGoal",
529 ]
530 .iter()
531 .any(|n| t == type_id(n))
532 {
533 goal(&b).map(|_| "goal")
534 } else {
535 match tutorial_part(&b) {
536 Ok(_) => Ok("tutorial"),
537 Err(e) if e.to_string().contains("not a tutorial part") => continue,
538 Err(e) => Err(e),
539 }
540 };
541 match kind {
542 Ok(k) => *counts.entry(k).or_default() += 1,
543 Err(e) => panic!("{name}: {e}"),
544 }
545 }
546 }
547 assert_eq!(counts.get("achievement"), Some(&36), "{counts:?}");
548 assert_eq!(counts.get("goal"), Some(&138), "{counts:?}");
549 assert_eq!(counts.get("scenario"), Some(&4), "{counts:?}");
550 assert_eq!(counts.get("tutorial"), Some(&557), "{counts:?}");
551 }
552}