ck3_history_extractor/parser/
game_object.rs

1use std::{
2    any::type_name,
3    error,
4    fmt::{self, Debug},
5    num::{ParseFloatError, ParseIntError},
6    rc::Rc,
7    str::ParseBoolError,
8};
9
10use derive_more::{Display, From};
11use jomini::common::{Date, PdsDate};
12
13use super::super::types::{GameId, GameString, HashMap};
14
15/// An error that can occur when converting a value from a save file.
16#[derive(Debug, From, Display)]
17pub enum ConversionError {
18    /// The value is not of the expected type.
19    #[display("failed converting {:?} to {}", _0, _1)]
20    InvalidType(SaveFileValue, &'static str),
21    ParseIntError(ParseIntError),
22    ParseFloatError(ParseFloatError),
23    ParseBoolError(ParseBoolError),
24    DateError(),
25}
26
27impl error::Error for ConversionError {
28    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
29        match self {
30            Self::ParseIntError(err) => Some(err),
31            Self::ParseBoolError(err) => Some(err),
32            Self::ParseFloatError(err) => Some(err),
33            _ => None,
34        }
35    }
36}
37
38/// A string that represents a boolean true value.
39const BOOL_TRUE: &str = "yes";
40/// A string that represents a boolean false value.
41const BOOL_FALSE: &str = "no";
42
43/// A value that comes from a save file.
44/// Matching against this enum is a bad idea, because [SaveFileValue::String] may actually contain any type.
45/// It's better to use the conversion methods like [SaveFileValue::as_string].
46#[derive(PartialEq, Clone, Debug, From)]
47#[from(forward)]
48pub enum SaveFileValue {
49    /// A simple string value, may be anything in reality.
50    String(GameString),
51    /// A complex object value.
52    Object(SaveFileObject),
53    /// A floating point value
54    Real(f64),
55    /// An integer
56    Integer(i64),
57    /// A boolean
58    Boolean(bool),
59    /// A date
60    Date(Date),
61}
62
63impl TryInto<String> for SaveFileValue {
64    type Error = ConversionError;
65    fn try_into(self) -> Result<String, Self::Error> {
66        match self {
67            SaveFileValue::Object(_) => Err(ConversionError::InvalidType(
68                self.clone(),
69                type_name::<String>(),
70            )),
71            SaveFileValue::Date(date) => Ok(date.iso_8601().to_string()),
72            SaveFileValue::Boolean(b) => Ok(b.to_string()),
73            SaveFileValue::Integer(i) => Ok(i.to_string()),
74            SaveFileValue::Real(f) => Ok(f.to_string()),
75            SaveFileValue::String(s) => Ok(s.to_string()),
76        }
77    }
78}
79
80impl From<String> for SaveFileValue {
81    fn from(value: String) -> Self {
82        value.as_str().into()
83    }
84}
85
86impl From<&str> for SaveFileValue {
87    fn from(value: &str) -> Self {
88        let dot_count = value.chars().filter(|&c| c == '.').count();
89        if dot_count == 2 {
90            let mut parts = value.split('.');
91            if let (Some(year), Some(month), Some(day)) = (parts.next(), parts.next(), parts.next())
92            {
93                if let (Ok(year), Ok(month), Ok(day)) = (year.parse(), month.parse(), day.parse()) {
94                    if let Some(date) = Date::from_ymd_opt(year, month, day) {
95                        return SaveFileValue::Date(date);
96                    }
97                }
98            }
99        } else if dot_count == 1 {
100            if let Ok(f) = value.parse() {
101                return SaveFileValue::Real(f);
102            }
103        } else if dot_count == 0 {
104            if value == BOOL_TRUE {
105                return SaveFileValue::Boolean(true);
106            } else if value == BOOL_FALSE {
107                return SaveFileValue::Boolean(false);
108            } else if let Ok(int) = value.parse() {
109                return SaveFileValue::Integer(int);
110            }
111        }
112        SaveFileValue::String(Rc::from(value))
113    }
114}
115
116impl From<i32> for SaveFileValue {
117    fn from(value: i32) -> Self {
118        SaveFileValue::Integer(value as i64)
119    }
120}
121
122impl From<u32> for SaveFileValue {
123    fn from(value: u32) -> Self {
124        SaveFileValue::Integer(value as i64)
125    }
126}
127
128impl From<u64> for SaveFileValue {
129    fn from(value: u64) -> Self {
130        SaveFileValue::Integer(value as i64)
131    }
132}
133
134impl From<[u8; 4]> for SaveFileValue {
135    fn from(value: [u8; 4]) -> Self {
136        SaveFileValue::Real(f32::from_le_bytes(value) as f64)
137    }
138}
139
140impl From<[u8; 8]> for SaveFileValue {
141    fn from(value: [u8; 8]) -> Self {
142        SaveFileValue::Real(f64::from_le_bytes(value))
143    }
144}
145
146impl SaveFileValue {
147    // this API allows for easy error collection using the ? operator.
148    /// Get the value as a string
149    pub fn as_string(&self) -> Result<GameString, ConversionError> {
150        match self {
151            SaveFileValue::String(s) => Ok(s.clone()),
152            _ => Err(ConversionError::InvalidType(
153                self.clone(),
154                type_name::<GameString>(),
155            )),
156        }
157    }
158
159    /// Get the value as a GameId
160    pub fn as_id(&self) -> Result<GameId, ConversionError> {
161        return Ok(self.as_integer()? as GameId);
162    }
163
164    /// Get the value as a GameObject
165    pub fn as_object(&self) -> Result<&SaveFileObject, ConversionError> {
166        match self {
167            SaveFileValue::Object(o) => Ok(o),
168            _ => Err(ConversionError::InvalidType(
169                self.clone(),
170                type_name::<SaveFileObject>(),
171            )),
172        }
173    }
174
175    /// Get the value as an integer. If the value is a real number, it will be
176    /// truncated.
177    pub fn as_integer(&self) -> Result<i64, ConversionError> {
178        match self {
179            SaveFileValue::Integer(i) => Ok(*i),
180            SaveFileValue::Real(r) => Ok(*r as i64),
181            SaveFileValue::String(s) => {
182                // sometimes developers make an oopsie, and make a typo. Here we try to correct it for them by getting only the digits
183                Ok(s.chars().filter_map(|c| c.to_digit(10)).sum::<u32>() as i64)
184            }
185            _ => Err(ConversionError::InvalidType(
186                self.clone(),
187                type_name::<i64>(),
188            )),
189        }
190    }
191
192    /// Get the value as a real number. If the value is an integer, it will be
193    /// converted to a real number.
194    pub fn as_real(&self) -> Result<f64, ConversionError> {
195        match self {
196            SaveFileValue::Real(r) => Ok(*r),
197            SaveFileValue::Integer(i) => Ok(*i as f64),
198            _ => Err(ConversionError::InvalidType(
199                self.clone(),
200                type_name::<f64>(),
201            )),
202        }
203    }
204
205    /// Get the value as a date
206    pub fn as_date(&self) -> Result<Date, ConversionError> {
207        match self {
208            SaveFileValue::Date(date) => Ok(*date),
209            SaveFileValue::Integer(int) => {
210                Date::from_binary(*int as i32).ok_or(ConversionError::DateError())
211            }
212            _ => Err(ConversionError::InvalidType(
213                self.clone(),
214                type_name::<(i16, u8, u8)>(),
215            )),
216        }
217    }
218
219    /// Get the value as a boolean
220    pub fn as_boolean(&self) -> Result<bool, ConversionError> {
221        match self {
222            SaveFileValue::Boolean(b) => Ok(*b),
223            _ => Err(ConversionError::InvalidType(
224                self.clone(),
225                type_name::<bool>(),
226            )),
227        }
228    }
229}
230
231// TODO well technically the key can be also an integer, and that could be a decent speedup
232
233/// A game object that stores values as a map.
234pub type GameObjectMap = HashMap<String, SaveFileValue>;
235/// A game object that stores values as an array.
236pub type GameObjectArray = Vec<SaveFileValue>;
237
238/// An object that comes from a save file.
239#[derive(PartialEq, Clone, Debug)]
240pub enum SaveFileObject {
241    /// An object that stores values as a map.
242    Map(GameObjectMap),
243    /// An object that stores values as an array.
244    Array(GameObjectArray),
245}
246
247impl SaveFileObject {
248    /// Get the value as a GameObject map
249    ///
250    /// # Panics
251    ///
252    /// Panics if the value is not a map
253    pub fn as_map(&self) -> Result<&GameObjectMap, ConversionError> {
254        match self {
255            SaveFileObject::Map(o) => Ok(o),
256            _ => Err(ConversionError::InvalidType(
257                SaveFileValue::Object(self.clone()),
258                type_name::<GameObjectMap>(),
259            )),
260        }
261    }
262
263    /// Get the value as a GameObject array
264    ///
265    /// # Panics
266    ///
267    /// Panics if the value is not an array
268    pub fn as_array(&self) -> Result<&GameObjectArray, ConversionError> {
269        match self {
270            SaveFileObject::Array(a) => Ok(a),
271            _ => Err(ConversionError::InvalidType(
272                SaveFileValue::Object(self.clone()),
273                type_name::<GameObjectArray>(),
274            )),
275        }
276    }
277
278    /// Check if the object is empty
279    pub fn is_empty(&self) -> bool {
280        match self {
281            SaveFileObject::Map(m) => m.is_empty(),
282            SaveFileObject::Array(a) => a.is_empty(),
283        }
284    }
285}
286
287#[derive(Debug, From)]
288pub enum SaveObjectError {
289    ConversionError(ConversionError),
290    KeyError(KeyError),
291}
292
293pub trait GameObjectMapping {
294    /// Get the value of a key, or return an error if the key is missing.
295    /// Essentially a different flavor of [HashMap::get].
296    /// The error is lazily initialized, so performance if the key is present is
297    /// not affected.
298    fn get_err(&self, key: &str) -> Result<&SaveFileValue, KeyError>;
299    /// Get the value of a key as a string.
300    fn get_string(&self, key: &str) -> Result<GameString, SaveObjectError>;
301    /// Get the value of a key as an object.
302    fn get_object(&self, key: &str) -> Result<&SaveFileObject, SaveObjectError>;
303    /// Get the value of a key as an integer.
304    fn get_integer(&self, key: &str) -> Result<i64, SaveObjectError>;
305    /// Get the value of a key as a real number.
306    fn get_real(&self, key: &str) -> Result<f64, SaveObjectError>;
307    /// Get the value of a key as a GameId.
308    fn get_game_id(&self, key: &str) -> Result<GameId, SaveObjectError>;
309    /// Get the value of a key as a date.
310    fn get_date(&self, key: &str) -> Result<Date, SaveObjectError>;
311}
312
313impl GameObjectMapping for GameObjectMap {
314    fn get_err(&self, key: &str) -> Result<&SaveFileValue, KeyError> {
315        self.get(key) // lazy error initialization, else we copy key and obj every time
316            .ok_or_else(|| KeyError::MissingKey(key.to_owned(), self.clone()))
317    }
318
319    fn get_string(&self, key: &str) -> Result<GameString, SaveObjectError> {
320        Ok(self.get_err(key)?.as_string()?)
321    }
322
323    fn get_object(&self, key: &str) -> Result<&SaveFileObject, SaveObjectError> {
324        Ok(self.get_err(key)?.as_object()?)
325    }
326
327    fn get_integer(&self, key: &str) -> Result<i64, SaveObjectError> {
328        Ok(self.get_err(key)?.as_integer()?)
329    }
330
331    fn get_real(&self, key: &str) -> Result<f64, SaveObjectError> {
332        Ok(self.get_err(key)?.as_real()?)
333    }
334
335    fn get_game_id(&self, key: &str) -> Result<GameId, SaveObjectError> {
336        Ok(self.get_err(key)?.as_id()?)
337    }
338
339    fn get_date(&self, key: &str) -> Result<Date, SaveObjectError> {
340        Ok(self.get_err(key)?.as_date()?)
341    }
342}
343
344pub trait GameObjectCollection {
345    fn get_index(&self, index: usize) -> Result<&SaveFileValue, KeyError>;
346}
347
348impl GameObjectCollection for GameObjectArray {
349    fn get_index(&self, index: usize) -> Result<&SaveFileValue, KeyError> {
350        self.get(index)
351            .ok_or_else(|| KeyError::IndexError(index, self.clone()))
352    }
353}
354
355impl fmt::Display for SaveObjectError {
356    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
357        match self {
358            Self::ConversionError(e) => write!(f, "conversion error: {}", e),
359            Self::KeyError(e) => write!(f, "key error: {}", e),
360        }
361    }
362}
363
364impl error::Error for SaveObjectError {
365    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
366        match self {
367            Self::ConversionError(e) => Some(e),
368            Self::KeyError(e) => Some(e),
369        }
370    }
371}
372
373#[derive(Debug)]
374pub enum KeyError {
375    MissingKey(String, GameObjectMap),
376    IndexError(usize, GameObjectArray),
377}
378
379impl fmt::Display for KeyError {
380    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
381        match self {
382            Self::MissingKey(key, obj) => write!(f, "key {} missing from object {:?}", key, obj),
383            Self::IndexError(index, obj) => write!(f, "index {} out of range for {:?}", index, obj),
384        }
385    }
386}
387
388impl error::Error for KeyError {
389    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
390        None
391    }
392}