ck3_history_extractor/game_data/
loader.rs

1use std::{error, fs::read_dir, io, mem, path::Path};
2
3use derive_more::{Display, From};
4
5use super::{
6    super::{
7        parser::{
8            yield_section, GameObjectCollection, ParsingError, SaveFile, SaveFileError,
9            SaveFileObject, SaveFileValue,
10        },
11        types::{GameId, GameString, HashMap},
12    },
13    map::MapError,
14    GameData, GameMap, Localizer,
15};
16
17/// An error that occurred while processing game data
18#[derive(Debug, From, Display)]
19pub enum GameDataError {
20    /// A file is missing at the provided path
21    #[display("a file {_0} is missing")]
22    MissingFile(String),
23    /// The data is invalid in some way with description
24    #[display("the data is invalid: {_0}")]
25    InvalidData(&'static str),
26    ParsingError(ParsingError),
27    IOError(SaveFileError),
28    MapError(MapError),
29}
30
31impl error::Error for GameDataError {
32    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
33        match self {
34            GameDataError::IOError(e) => Some(e),
35            GameDataError::ParsingError(e) => Some(e),
36            _ => None,
37        }
38    }
39}
40
41impl From<io::Error> for GameDataError {
42    fn from(e: io::Error) -> Self {
43        GameDataError::IOError(SaveFileError::from(e))
44    }
45}
46
47/// Creates a mapping from province ids to their barony title keys
48fn create_title_province_map(
49    file: &SaveFile,
50    out: &mut HashMap<GameId, GameString>,
51) -> Result<(), ParsingError> {
52    let mut tape = file.tape();
53    while let Some(res) = yield_section(&mut tape) {
54        let mut section = res?;
55        //DFS in the structure
56        let mut stack = if let SaveFileObject::Map(base) = section.parse()? {
57            vec![(base, GameString::from(section.get_name()))]
58        } else {
59            // if the base object is an array, something wonky is going on and we just politely retreat
60            continue;
61        };
62        while let Some(entry) = stack.pop() {
63            if let Some(pro) = entry.0.get("province") {
64                match pro {
65                    // apparently pdx sometimes makes an oopsie and in the files the key is doubled, thus leading us to parse that as an array
66                    SaveFileValue::Object(o) => {
67                        out.insert(o.as_array()?.get_index(0)?.as_id()?, entry.1);
68                    }
69                    s => {
70                        out.insert(s.as_id()?, entry.1);
71                    }
72                }
73            }
74            for (key, val) in entry.0 {
75                match val {
76                    SaveFileValue::Object(val) => match val {
77                        SaveFileObject::Map(val) => {
78                            stack.push((val, key.into()));
79                        }
80                        _ => {}
81                    },
82                    _ => {}
83                }
84            }
85        }
86    }
87    Ok(())
88}
89
90// File system stuff
91
92const LOCALIZATION_SUFFIX: &str = "localization";
93
94const MAP_PATH_SUFFIX: &str = "map_data";
95const PROVINCES_SUFFIX: &str = "provinces.png";
96const RIVERS_SUFFIX: &str = "rivers.png";
97const DEFINITION_SUFFIX: &str = "definition.csv";
98
99const PROVINCE_DIR_PATH: &str = "common/landed_titles/";
100
101/// A loader for game data
102pub struct GameDataLoader {
103    no_vis: bool,
104    language: &'static str,
105    map: Option<GameMap>,
106    localizer: Localizer,
107    title_province_map: HashMap<GameId, GameString>,
108}
109
110impl GameDataLoader {
111    /// Create a new game data loader with the given language and
112    /// setting for whether to load visual data
113    pub fn new(no_vis: bool, language: &'static str) -> Self {
114        GameDataLoader {
115            no_vis,
116            language,
117            map: None,
118            localizer: Localizer::default(),
119            title_province_map: HashMap::default(),
120        }
121    }
122
123    /// Search the given path for localization and map data
124    pub fn process_path<P: AsRef<Path>>(&mut self, path: P) -> Result<(), GameDataError> {
125        let path = path.as_ref();
126        let loc_path = path.join(LOCALIZATION_SUFFIX).join(self.language);
127        if loc_path.exists() && loc_path.is_dir() {
128            self.localizer.add_from_path(&loc_path);
129        }
130        if !self.no_vis {
131            let map_path = path.join(MAP_PATH_SUFFIX);
132            if !map_path.exists() || !map_path.is_dir() {
133                return Ok(()); // this is not an error, just no map data
134            }
135            let province_dir_path = path.join(PROVINCE_DIR_PATH);
136            if !province_dir_path.exists() || !province_dir_path.is_dir() {
137                // I guess having a custom map with vanilla titles is fine, but not for us
138                return Err(GameDataError::InvalidData(
139                    "custom map without custom titles",
140                ));
141            }
142            let dir = read_dir(&province_dir_path)?;
143            for entry in dir {
144                let entry = entry?;
145                if entry.file_type()?.is_file() {
146                    create_title_province_map(
147                        &SaveFile::open(entry.path())?,
148                        &mut self.title_province_map,
149                    )?;
150                }
151            }
152            self.map = Some(GameMap::new(
153                map_path.join(PROVINCES_SUFFIX),
154                map_path.join(RIVERS_SUFFIX),
155                map_path.join(DEFINITION_SUFFIX),
156                &self.title_province_map,
157            )?);
158        }
159        Ok(())
160    }
161
162    /// Finalize the game data processing
163    pub fn finalize(&mut self) -> GameData {
164        self.localizer.remove_formatting();
165        GameData {
166            map: self.map.take(),
167            localizer: mem::take(&mut self.localizer),
168            title_province_map: mem::take(&mut self.title_province_map),
169        }
170    }
171}