Skip to main content

banished_formats/
scenario.rs

1//! Achievements, scenarios and tutorials (`Game/Achievement`, `Game/Scenario`). See
2//! docs/ACHIEVEMENTS.md and docs/TUTORIAL.md.
3//!
4//! - `Achievement::Achievement`: its string table, title, description, sprite sheet and sprite, and
5//!   its goals.
6//! - A string table and text; then each class's own block: BuildingGoal, PopulationGoal, StatGoal,
7//!   TimeGoal, FinishTutorialGoal.
8//! - `Scenario`: an achievement, then the game it starts.
9//! - `Tutorial` and its steps, setups, goals and images.
10//!
11//! Original: `Achievement::Achievement::Serialize`, `Achievement::Goal`, `FUN_1400dcb70`,
12//! `FUN_1400dcd40`, `FUN_1400dfd80`, `FUN_1400dfff0`, `FUN_1400dcea0`, `FUN_1400ae840`,
13//! `FUN_1400e3fa0`.
14//!
15//! Original: Runtime `0x18009eb70`, Runtime `0x18009fd30`.
16
17use crate::crc::type_id;
18use crate::crs::{self, Reader};
19use crate::error::{format_err, Result};
20
21/// `Achievement::Achievement`.
22#[derive(Debug, Clone, Default, PartialEq)]
23pub struct Achievement {
24    // +0x20
25    /// The string table.
26    pub strings: Option<String>,
27    // +0x28, +0x30
28    /// The title's key.
29    pub title: String,
30    /// The description's key.
31    pub description: String,
32    // +0x38; +0x40
33    /// The sprite sheet.
34    pub sprites: Option<String>,
35    /// The sprite.
36    pub sprite: String,
37    // +0x48
38    /// The goals.
39    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/// A Goal's own fields, by class.
64#[derive(Debug, Clone, PartialEq)]
65pub enum GoalKind {
66    // +0x30; +0x38
67    /// Some buildings exist.
68    Building {
69        /// Whether all of the buildings must exist (else any).
70        all: bool,
71        /// The buildings (ComponentDescriptions).
72        buildings: Vec<String>,
73    },
74    // +0x30
75    /// The population is at least this many citizens.
76    Population(i32),
77    // +0x30..
78    /// A statistic compared with an amount or with a second statistic.
79    Stat {
80        /// The statistic.
81        e30: i32,
82        /// The second statistic, for odd comparisons.
83        e34: i32,
84        /// The comparison: 0/1 equal, 2/3 not, 4/5 less, 6/7 at most, 8/9 more, 10/11 at least.
85        e38: i32,
86        /// The amount the statistic is compared with.
87        amount: i32,
88        /// Whether the goal is met until the comparison holds, then fails.
89        flag: bool,
90        /// The statistic's reference.
91        r48: String,
92        /// The second statistic's reference.
93        r60: String,
94        /// Raw-material flags, for the statistics that take them.
95        e78: i32,
96        /// A number, for the statistics that take it.
97        i7c: i32,
98    },
99    // +0x30; +0x34
100    /// A number of years passed.
101    Time {
102        /// The years.
103        years: i32,
104        /// Whether the goal must be met within that time (else after it).
105        within: bool,
106    },
107    // +0x30
108    /// The tutorial is done.
109    FinishTutorial(Option<String>),
110}
111
112/// `Achievement::Goal`.
113#[derive(Debug, Clone, PartialEq)]
114pub struct Goal {
115    /// The string table.
116    pub strings: Option<String>,
117    /// The text's key.
118    pub text: String,
119    /// The class's own fields.
120    pub kind: GoalKind,
121}
122
123/// Read a [`Goal`] from the bytes of its resource.
124pub 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
164/// Read a [`Achievement`] from the bytes of its resource.
165pub 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/// `Scenario` (an Achievement).
176#[derive(Debug, Clone, Default, PartialEq)]
177pub struct Scenario {
178    /// The achievement.
179    pub achievement: Achievement,
180    // +0x60; +0x64
181    /// Not identified yet.
182    pub i60: i32,
183    /// Not identified yet.
184    pub i64: i32,
185    // +0x58
186    /// The town's name.
187    pub town_name: String,
188    // +0x70; +0x78; +0x80
189    /// Not identified yet.
190    pub r70: Option<String>,
191    /// Not identified yet.
192    pub r78: Option<String>,
193    /// Not identified yet.
194    pub r80: Option<String>,
195    // +0x88
196    /// The save to start from (`Save\{name}.sav` in the data, the tutorials'), empty for a new
197    /// game.
198    ///
199    /// Original: `FUN_14007d180`.
200    pub save: String,
201    // +0x68; +0x69
202    /// Not identified yet.
203    pub b68: bool,
204    /// Not identified yet.
205    pub b69: bool,
206}
207
208/// Read a [`Scenario`] from the bytes of its resource.
209pub 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// +0x28; +0x20; +0x38
238/// `Tutorial`: its steps, its window (a UIDescription template, opened at the right edge), the
239/// scenario to start when it is done (none below 0).
240///
241/// Original: `FUN_1400e6240`.
242#[derive(Debug, Clone, Default, PartialEq)]
243pub struct Tutorial {
244    /// The steps.
245    pub steps: Vec<Option<String>>,
246    /// The window (a UIDescription template, opened at the right edge).
247    pub window: Option<String>,
248    /// The scenario to start when it is done (none below 0).
249    pub next: i32,
250}
251
252// +0x20; +0x28; +0x30; +0x38; +0x40; +0x50; +0x60
253/// `TutorialStep`: a checkpoint (a loaded game resumes at the latest one), the string table, the
254/// texts before and after, images, setups, goals.
255///
256/// Original: `FUN_1400e69f0`, `FUN_1400e7280`.
257#[derive(Debug, Clone, Default, PartialEq)]
258pub struct TutorialStep {
259    /// Whether this is a checkpoint (a loaded game resumes at the latest one).
260    pub checkpoint: bool,
261    /// The string table.
262    pub strings: Option<String>,
263    /// The text before.
264    pub pre: String,
265    /// The text after.
266    pub post: String,
267    /// The images.
268    pub images: Vec<Option<String>>,
269    /// The set-ups.
270    pub setups: Vec<Option<String>>,
271    /// The goals.
272    pub goals: Vec<Option<String>>,
273}
274
275/// One part of a tutorial: the tutorial itself, a step, or a set-up dialog.
276#[derive(Debug, Clone, PartialEq)]
277pub enum TutorialPart {
278    /// The tutorial.
279    Tutorial(Tutorial),
280    /// A step.
281    Step(TutorialStep),
282    // +0x20; +0x30; +0x40; +0x50; +0x48; +0x58; +0x70
283    /// A set-up that limits the interface and points at an element.
284    SetupDialog {
285        /// The elements left usable.
286        a: Vec<String>,
287        /// More elements left usable.
288        b: Vec<String>,
289        /// The element to point at.
290        s40: String,
291        /// The highlight dialog.
292        r50: Option<String>,
293        /// The angle of the pointer.
294        i48: i32,
295        /// A ComponentDescription.
296        component: String,
297        /// Not identified yet.
298        i70: i32,
299    },
300    /// Moves the camera to two cells.
301    SetupCamera([i32; 4]),
302    /// Sets the game speed.
303    SetupGameSpeed {
304        /// The speed.
305        speed: i32,
306        /// Whether it unpauses the game.
307        b24: bool,
308    },
309    /// Limits what the player may place, and where.
310    SetupRestrictPlayer {
311        /// A rectangle of cells, then one more value.
312        values: [i32; 5],
313        /// The first template.
314        s38: String,
315        /// The second template.
316        s50: String,
317    },
318    /// An element was pressed.
319    GoalButton {
320        /// The element's path.
321        s20: String,
322        /// Not identified yet.
323        b28: bool,
324    },
325    /// A count reached a number.
326    GoalCount {
327        /// What is counted.
328        e20: i32,
329        /// The number.
330        i24: i32,
331        /// The template.
332        s30: String,
333        /// Whether the count must be at least the number (else exactly).
334        b28: bool,
335        /// The spinner's path, for kind 9.
336        s48: String,
337    },
338    /// A dialog is open.
339    GoalDialog {
340        /// The dialog.
341        s20: String,
342        /// The template of the entity it is open on.
343        component: String,
344    },
345    /// The game speed is this value.
346    GoalGameSpeed(i32),
347    // +0x28; +0x2c; +0x20
348    /// A TutorialImage and its classes.
349    Image {
350        /// The column.
351        i28: i32,
352        /// The row.
353        i2c: i32,
354        /// The element.
355        r20: Option<String>,
356        /// What the class adds.
357        extra: ImageExtra,
358    },
359}
360
361/// What a tutorial image adds: a key name, an input key, or a toolbar icon.
362#[derive(Debug, Clone, PartialEq)]
363pub enum ImageExtra {
364    /// Nothing.
365    None,
366    /// A toolbar item's key.
367    HotKey(String),
368    /// A key-map action's key.
369    InputKey(i32),
370    /// A toolbar item's icon.
371    ToolbarIcon(String),
372}
373
374/// Read a [`TutorialPart`] from the bytes of its resource.
375pub 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        // A base class writes its own block first: TutorialGoal's is empty; TutorialImage's
380        // holds the image.
381        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    /// Every shipped achievement, goal, scenario and tutorial part parses whole.
504    #[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}