ck3_history_extractor/game_data/
loader.rs1use 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#[derive(Debug, From, Display)]
19pub enum GameDataError {
20 #[display("a file {_0} is missing")]
22 MissingFile(String),
23 #[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
47fn 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 let mut stack = if let SaveFileObject::Map(base) = section.parse()? {
57 vec![(base, GameString::from(section.get_name()))]
58 } else {
59 continue;
61 };
62 while let Some(entry) = stack.pop() {
63 if let Some(pro) = entry.0.get("province") {
64 match pro {
65 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
90const 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
101pub 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 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 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(()); }
135 let province_dir_path = path.join(PROVINCE_DIR_PATH);
136 if !province_dir_path.exists() || !province_dir_path.is_dir() {
137 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 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}