eu4save/
melt.rs

1use crate::{file::Eu4Binary, flavor::Eu4Flavor, Eu4Date, Eu4Error, Eu4ErrorKind};
2use jomini::{
3    binary::{BinaryFlavor, FailedResolveStrategy, TokenResolver},
4    common::PdsDate,
5    BinaryTape, BinaryToken, Scalar, TextWriterBuilder,
6};
7use std::{collections::HashSet, io::Cursor};
8
9struct QuoteMode {
10    kind: QuoteKind,
11    idx: usize,
12}
13
14impl QuoteMode {
15    fn new() -> Self {
16        QuoteMode {
17            kind: QuoteKind::Inactive,
18            idx: 0,
19        }
20    }
21
22    fn clear(&mut self) {
23        self.kind = QuoteKind::Inactive;
24    }
25}
26
27#[derive(Debug, Clone, Copy)]
28enum QuoteKind {
29    // Regular quoting rules
30    Inactive,
31
32    // Unquote scalar and containers
33    UnquoteAll,
34
35    // Unquote only a scalar value
36    UnquoteScalar,
37
38    // Quote only a scalar value
39    QuoteScalar,
40
41    // Quote object keys
42    ForceQuote,
43}
44
45enum Eu4MelterKind<'a, 'b> {
46    Single(&'b BinaryTape<'a>),
47    Multiple {
48        meta: &'b Eu4Binary<'a>,
49        gamestate: &'b Eu4Binary<'a>,
50        ai: &'b Eu4Binary<'a>,
51    },
52}
53
54/// Convert a binary save to plaintext
55pub struct Eu4Melter<'a, 'b> {
56    kind: Eu4MelterKind<'a, 'b>,
57    verbatim: bool,
58    on_failed_resolve: FailedResolveStrategy,
59}
60
61impl<'a, 'b> Eu4Melter<'a, 'b> {
62    pub fn from_entries(
63        meta: &'b Eu4Binary<'a>,
64        gamestate: &'b Eu4Binary<'a>,
65        ai: &'b Eu4Binary<'a>,
66    ) -> Self {
67        Eu4Melter {
68            kind: Eu4MelterKind::Multiple {
69                meta,
70                gamestate,
71                ai,
72            },
73            verbatim: false,
74            on_failed_resolve: FailedResolveStrategy::Ignore,
75        }
76    }
77
78    pub(crate) fn new(tape: &'b BinaryTape<'a>) -> Self {
79        Eu4Melter {
80            kind: Eu4MelterKind::Single(tape),
81            verbatim: false,
82            on_failed_resolve: FailedResolveStrategy::Ignore,
83        }
84    }
85
86    pub fn verbatim(&mut self, verbatim: bool) -> &mut Self {
87        self.verbatim = verbatim;
88        self
89    }
90
91    pub fn on_failed_resolve(&mut self, strategy: FailedResolveStrategy) -> &mut Self {
92        self.on_failed_resolve = strategy;
93        self
94    }
95
96    pub(crate) fn tokens_len(&self) -> usize {
97        match &self.kind {
98            Eu4MelterKind::Single(x) => x.tokens().len(),
99            Eu4MelterKind::Multiple {
100                meta,
101                gamestate,
102                ai,
103            } => {
104                meta.tape().tokens().len()
105                    + gamestate.tape().tokens().len()
106                    + ai.tape().tokens().len()
107            }
108        }
109    }
110
111    pub(crate) fn get_token(&self, idx: usize) -> Option<&BinaryToken> {
112        match &self.kind {
113            Eu4MelterKind::Single(x) => x.tokens().get(idx),
114            Eu4MelterKind::Multiple {
115                meta,
116                gamestate,
117                ai,
118            } => {
119                let mut idx = idx;
120
121                let meta_len = meta.tape().tokens().len();
122                if idx < meta_len {
123                    return meta.tape().tokens().get(idx);
124                }
125                idx -= meta_len;
126
127                let gamestate_len = gamestate.tape().tokens().len();
128                if idx < gamestate_len {
129                    return gamestate.tape().tokens().get(idx);
130                }
131                idx -= gamestate_len;
132                ai.tape().tokens().get(idx)
133            }
134        }
135    }
136
137    pub(crate) fn skip_value_idx(&self, token_idx: usize) -> usize {
138        let offset = match &self.kind {
139            Eu4MelterKind::Single(_) => 0,
140            Eu4MelterKind::Multiple {
141                meta, gamestate, ..
142            } => {
143                let mut idx = token_idx;
144
145                let meta_len = meta.tape().tokens().len();
146                if idx < meta_len {
147                    0
148                } else {
149                    idx -= meta_len;
150                    let gamestate_len = gamestate.tape().tokens().len();
151                    if idx < gamestate_len {
152                        meta_len
153                    } else {
154                        gamestate_len
155                    }
156                }
157            }
158        };
159
160        self.get_token(token_idx + 1)
161            .map(|next_token| match next_token {
162                BinaryToken::Object(end) | BinaryToken::Array(end) => offset + end + 1,
163                _ => token_idx + 2,
164            })
165            .unwrap_or(token_idx + 1)
166    }
167
168    pub fn melt<R>(&self, resolver: &R) -> Result<MeltedDocument, Eu4Error>
169    where
170        R: TokenResolver,
171    {
172        let out = melt(self, resolver).map_err(|e| match e {
173            MelterError::Write(x) => Eu4ErrorKind::Writer(x),
174            MelterError::UnknownToken { token_id } => Eu4ErrorKind::UnknownToken { token_id },
175            MelterError::InvalidDate(x) => Eu4ErrorKind::InvalidDate(x),
176        })?;
177        Ok(out)
178    }
179}
180
181#[derive(thiserror::Error, Debug)]
182pub(crate) enum MelterError {
183    #[error("{0}")]
184    Write(#[from] jomini::Error),
185
186    #[error("")]
187    UnknownToken { token_id: u16 },
188
189    #[error("")]
190    InvalidDate(i32),
191}
192
193/// Output from melting a binary save to plaintext
194pub struct MeltedDocument {
195    data: Vec<u8>,
196    unknown_tokens: HashSet<u16>,
197}
198
199impl MeltedDocument {
200    /// The converted plaintext data
201    pub fn into_data(self) -> Vec<u8> {
202        self.data
203    }
204
205    /// The converted plaintext data
206    pub fn data(&self) -> &[u8] {
207        self.data.as_slice()
208    }
209
210    /// The list of unknown tokens that the provided resolver accumulated
211    pub fn unknown_tokens(&self) -> &HashSet<u16> {
212        &self.unknown_tokens
213    }
214}
215
216pub(crate) fn melt<R>(melter: &Eu4Melter, resolver: &R) -> Result<MeltedDocument, MelterError>
217where
218    R: TokenResolver,
219{
220    // let tokens = melter.tape.tokens();
221    let mut out = Vec::with_capacity(melter.tokens_len() * 10);
222    out.extend_from_slice(b"EU4txt\n");
223    let mut unknown_tokens = HashSet::new();
224    let pos = out.len() as u64;
225    let mut writer = Cursor::new(out);
226    writer.set_position(pos);
227
228    let mut wtr = TextWriterBuilder::new()
229        .indent_char(b'\t')
230        .indent_factor(1)
231        .from_writer(writer);
232    let mut token_idx = 0;
233    let mut known_number = false;
234    let mut known_date = false;
235    let mut quote_mode = QuoteMode::new();
236    let mut long_format = false;
237    let mut depth = 0;
238    let mut queued_checksum: Option<Scalar> = None;
239    let flavor = Eu4Flavor::new();
240
241    while let Some(token) = melter.get_token(token_idx) {
242        match token {
243            BinaryToken::Object(_) => {
244                depth += 1;
245                wtr.write_object_start()?;
246            }
247            BinaryToken::Array(_) => {
248                depth += 1;
249                wtr.write_array_start()?;
250            }
251            BinaryToken::End(x) => {
252                depth -= 1;
253
254                if *x == quote_mode.idx {
255                    quote_mode.clear();
256                }
257
258                wtr.write_end()?;
259            }
260            BinaryToken::Bool(x) => wtr.write_bool(*x)?,
261            BinaryToken::U32(x) => wtr.write_u32(*x)?,
262            BinaryToken::U64(x) => wtr.write_u64(*x)?,
263            BinaryToken::I32(x) => {
264                if known_number {
265                    wtr.write_i32(*x)?;
266                    known_number = false;
267                } else if known_date {
268                    if let Some(date) = Eu4Date::from_binary(*x) {
269                        wtr.write_date(date.game_fmt())?;
270                    } else if melter.on_failed_resolve != FailedResolveStrategy::Error {
271                        wtr.write_i32(*x)?;
272                    } else {
273                        return Err(MelterError::InvalidDate(*x));
274                    }
275                    known_date = false;
276                } else if let Some(date) = Eu4Date::from_binary_heuristic(*x) {
277                    wtr.write_date(date.game_fmt())?;
278                } else {
279                    wtr.write_i32(*x)?;
280                }
281            }
282            BinaryToken::Quoted(x) => {
283                match quote_mode.kind {
284                    QuoteKind::Inactive if wtr.expecting_key() => {
285                        wtr.write_unquoted(x.as_bytes())?
286                    }
287                    QuoteKind::Inactive => wtr.write_quoted(x.as_bytes())?,
288                    QuoteKind::ForceQuote => wtr.write_quoted(x.as_bytes())?,
289                    QuoteKind::UnquoteAll => wtr.write_unquoted(x.as_bytes())?,
290                    QuoteKind::UnquoteScalar if token_idx == quote_mode.idx => {
291                        wtr.write_unquoted(x.as_bytes())?
292                    }
293                    QuoteKind::UnquoteScalar => wtr.write_quoted(x.as_bytes())?,
294                    QuoteKind::QuoteScalar if token_idx == quote_mode.idx => {
295                        wtr.write_quoted(x.as_bytes())?
296                    }
297                    QuoteKind::QuoteScalar => wtr.write_unquoted(x.as_bytes())?,
298                };
299
300                // Clear quote mode after encountering a scalar value
301                if token_idx == quote_mode.idx {
302                    quote_mode.clear();
303                }
304            }
305            BinaryToken::Unquoted(x) => {
306                wtr.write_unquoted(x.as_bytes())?;
307            }
308            BinaryToken::F32(x) => {
309                let val = flavor.visit_f32(*x);
310                if long_format {
311                    write!(&mut wtr, "{:.6}", val)?;
312                } else {
313                    write!(&mut wtr, "{:.3}", val)?;
314                }
315            }
316            BinaryToken::F64(x) => write!(&mut wtr, "{:.5}", flavor.visit_f64(*x))?,
317            BinaryToken::Token(x) => match resolver.resolve(*x) {
318                Some(id) => {
319                    let skip = (id == "is_ironman" && !melter.verbatim) || id == "checksum";
320                    if skip && wtr.expecting_key() {
321                        let next = melter.get_token(token_idx + 1);
322                        if id == "checksum" {
323                            if let Some(BinaryToken::Quoted(s)) = next {
324                                queued_checksum = Some(*s);
325                            }
326                        };
327
328                        token_idx = melter.skip_value_idx(token_idx);
329                        continue;
330                    }
331
332                    // There are certain tokens that we know are integers and will dupe the date heuristic
333                    known_number = id == "random" || id.ends_with("seed") || id == "id";
334                    known_date = id == "date_built";
335
336                    match id {
337                        "friend"
338                        | "production_leader_tag"
339                        | "dynamic_countries"
340                        | "electors"
341                        | "cores"
342                        | "named_unrest"
343                        | "claims"
344                        | "country_of_origin"
345                        | "granted_privileges"
346                        | "attackers"
347                        | "defenders"
348                        | "persistent_attackers"
349                        | "persistent_defenders"
350                        | "mission_slot"
351                        | "votes"
352                        | "ruler_flags"
353                        | "neighbours"
354                        | "home_neighbours"
355                        | "core_neighbours"
356                        | "call_to_arms_friends"
357                        | "allies"
358                        | "extended_allies"
359                        | "trade_embargoed_by"
360                        | "trade_embargoes"
361                        | "transfer_trade_power_from"
362                        | "friend_tags"
363                        | "hidden_flags"
364                        | "members"
365                        | "colony_claim"
366                        | "harsh"
367                        | "concilatory"
368                        | "current_at_war_with"
369                        | "current_war_allies"
370                        | "participating_countries"
371                        | "subjects"
372                        | "support_independence"
373                        | "transfer_trade_power_to"
374                        | "guarantees"
375                        | "warnings"
376                        | "flags" => {
377                            quote_mode = QuoteMode {
378                                kind: QuoteKind::UnquoteAll,
379                                idx: token_idx + 1,
380                            };
381                        }
382                        _ => {}
383                    }
384
385                    if depth == 2
386                        && matches!(id, "discovered_by" | "tribal_owner" | "active_disaster")
387                    {
388                        quote_mode = QuoteMode {
389                            kind: QuoteKind::UnquoteAll,
390                            idx: token_idx + 1,
391                        };
392                    }
393
394                    match id {
395                        "culture_group" | "saved_names" | "tech_level_dates"
396                        | "incident_variables" => {
397                            quote_mode = QuoteMode {
398                                kind: QuoteKind::ForceQuote,
399                                idx: token_idx + 1,
400                            };
401                        }
402                        "subjects" => {
403                            quote_mode = QuoteMode {
404                                kind: QuoteKind::QuoteScalar,
405                                idx: token_idx + 1,
406                            };
407                        }
408                        _ => {}
409                    }
410
411                    if depth == 4 && id == "leader" {
412                        quote_mode = QuoteMode {
413                            kind: QuoteKind::UnquoteScalar,
414                            idx: token_idx + 1,
415                        }
416                    }
417
418                    if depth == 0 && id == "ai" {
419                        long_format = true;
420                    }
421
422                    wtr.write_unquoted(id.as_bytes())?;
423                }
424                None => match melter.on_failed_resolve {
425                    FailedResolveStrategy::Error => {
426                        return Err(MelterError::UnknownToken { token_id: *x });
427                    }
428                    FailedResolveStrategy::Ignore if wtr.expecting_key() => {
429                        token_idx = melter.skip_value_idx(token_idx);
430                        continue;
431                    }
432                    _ => {
433                        unknown_tokens.insert(*x);
434                        write!(wtr, "__unknown_0x{:x}", x)?;
435                    }
436                },
437            },
438
439            // Tokens below are not found in EU4 saves, but we handle them anyways.
440            BinaryToken::MixedContainer => {
441                wtr.start_mixed_mode();
442            }
443            BinaryToken::Equal => {
444                wtr.write_operator(jomini::text::Operator::Equal)?;
445            }
446            BinaryToken::Rgb(color) => {
447                wtr.write_header(b"rgb")?;
448                wtr.write_array_start()?;
449                wtr.write_u32(color.r)?;
450                wtr.write_u32(color.g)?;
451                wtr.write_u32(color.b)?;
452                wtr.write_end()?;
453            }
454        }
455
456        token_idx += 1;
457    }
458
459    if let Some(checksum) = queued_checksum.take() {
460        wtr.write_unquoted(b"checksum")?;
461        wtr.write_quoted(checksum.as_bytes())?;
462    }
463
464    Ok(MeltedDocument {
465        data: wtr.into_inner().into_inner(),
466        unknown_tokens,
467    })
468}
469
470#[cfg(all(test, ironman))]
471mod tests {
472    use super::*;
473    use crate::{EnvTokens, Eu4File};
474
475    #[test]
476    fn test_short_input_regression() {
477        // Make sure it doesn't crash
478        let tape = BinaryTape::from_slice(&[]).unwrap();
479        let _ = Eu4Melter::new(&tape)
480            .on_failed_resolve(FailedResolveStrategy::Error)
481            .melt(&EnvTokens)
482            .unwrap();
483    }
484
485    #[test]
486    fn test_rgb_regression() {
487        let data = [
488            45, 2, 1, 0, 1, 137, 1, 45, 1, 0, 67, 2, 0, 255, 255, 255, 255, 226, 2, 1, 0, 1, 137,
489            1, 45, 1, 56, 226, 1, 255, 255, 255, 255, 255,
490        ];
491        let tape = BinaryTape::from_slice(&data).unwrap();
492        let _ = Eu4Melter::new(&tape)
493            .on_failed_resolve(FailedResolveStrategy::Ignore)
494            .melt(&EnvTokens)
495            .unwrap();
496    }
497
498    #[test]
499    fn test_ironman_nonscalar() {
500        let data = [137, 53, 3, 0, 4, 0];
501        let tape = BinaryTape::from_slice(&data).unwrap();
502        let expected = b"EU4txt\n";
503        let out = Eu4Melter::new(&tape)
504            .on_failed_resolve(FailedResolveStrategy::Error)
505            .melt(&EnvTokens)
506            .unwrap();
507        assert_eq!(out.data(), &expected[..]);
508    }
509
510    #[test]
511    fn test_melt_meta() {
512        let meta = include_bytes!("../tests/it/fixtures/meta.bin");
513        let expected = include_bytes!("../tests/it/fixtures/meta.bin.melted");
514        let file = Eu4File::from_slice(&meta[..]).unwrap();
515        let mut zip_sink = Vec::new();
516        let parsed_file = file.parse(&mut zip_sink).unwrap();
517        let binary = parsed_file.as_binary().unwrap();
518        let out = binary
519            .melter()
520            .on_failed_resolve(FailedResolveStrategy::Error)
521            .melt(&EnvTokens)
522            .unwrap();
523        assert_eq!(out.data(), &expected[..]);
524    }
525
526    #[test]
527    fn test_melt_skip_ironman() {
528        let data = [
529            0x45, 0x55, 0x34, 0x62, 0x69, 0x6e, 0x4d, 0x28, 0x01, 0x00, 0x0c, 0x00, 0x70, 0x98,
530            0x8d, 0x03, 0x89, 0x35, 0x01, 0x00, 0x0e, 0x00, 0x01, 0x38, 0x2a, 0x01, 0x00, 0x0f,
531            0x00, 0x03, 0x00, 0x42, 0x48, 0x41,
532        ];
533
534        let expected = b"EU4txt\ndate=1804.12.9\nplayer=\"BHA\"";
535        let file = Eu4File::from_slice(&data).unwrap();
536        let mut zip_sink = Vec::new();
537        let parsed_file = file.parse(&mut zip_sink).unwrap();
538        let binary = parsed_file.as_binary().unwrap();
539        let out = binary
540            .melter()
541            .on_failed_resolve(FailedResolveStrategy::Error)
542            .melt(&EnvTokens)
543            .unwrap();
544
545        assert_eq!(out.data(), &expected[..]);
546    }
547
548    #[test]
549    fn test_melt_skip_ironman_in_object() {
550        let data = [
551            0x45, 0x55, 0x34, 0x62, 0x69, 0x6e, 0x4d, 0x28, 0x01, 0x00, 0x0c, 0x00, 0x70, 0x98,
552            0x8d, 0x03, 0x23, 0x2d, 0x01, 0x00, 0x03, 0x00, 0x89, 0x35, 0x01, 0x00, 0x0e, 0x00,
553            0x01, 0x04, 0x00, 0x38, 0x2a, 0x01, 0x00, 0x0f, 0x00, 0x03, 0x00, 0x42, 0x48, 0x41,
554        ];
555
556        let expected = "EU4txt\ndate=1804.12.9\nimpassable={ }\nplayer=\"BHA\"";
557        let file = Eu4File::from_slice(&data).unwrap();
558        let mut zip_sink = Vec::new();
559        let parsed_file = file.parse(&mut zip_sink).unwrap();
560        let binary = parsed_file.as_binary().unwrap();
561        let out = binary
562            .melter()
563            .on_failed_resolve(FailedResolveStrategy::Error)
564            .melt(&EnvTokens)
565            .unwrap();
566
567        assert_eq!(std::str::from_utf8(&out.data()).unwrap(), &expected[..]);
568    }
569
570    #[test]
571    fn test_skip_quoting_flags() {
572        let mut data = vec![];
573        data.extend_from_slice(b"EU4bin");
574        data.extend_from_slice(&[0xcc, 0x29, 0x01, 0x00, 0x03, 0x00, 0x0f, 0x00, 0x11, 0x00]);
575        data.extend_from_slice(b"schools_initiated");
576        data.extend_from_slice(&[0x01, 0x00, 0x0f, 0x00, 0x0b, 0x00]);
577        data.extend_from_slice(b"1444.11.11\n");
578        data.extend_from_slice(&0x0004u16.to_le_bytes());
579
580        let expected = "EU4txt\nflags={\n\tschools_initiated=1444.11.11\n\n}";
581
582        let file = Eu4File::from_slice(&data).unwrap();
583        let mut zip_sink = Vec::new();
584        let parsed_file = file.parse(&mut zip_sink).unwrap();
585        let binary = parsed_file.as_binary().unwrap();
586        let out = binary
587            .melter()
588            .on_failed_resolve(FailedResolveStrategy::Error)
589            .melt(&EnvTokens)
590            .unwrap();
591
592        assert_eq!(std::str::from_utf8(out.data()).unwrap(), &expected[..]);
593    }
594
595    #[test]
596    fn test_melt_skip_unknown_key() {
597        let data = [
598            0x45, 0x55, 0x34, 0x62, 0x69, 0x6e, 0xff, 0xff, 0x01, 0x00, 0x0c, 0x00, 0x70, 0x98,
599            0x8d, 0x03, 0x89, 0x35, 0x01, 0x00, 0x0e, 0x00, 0x01, 0x38, 0x2a, 0x01, 0x00, 0x0f,
600            0x00, 0x03, 0x00, 0x42, 0x48, 0x41,
601        ];
602
603        let expected = "EU4txt\nplayer=\"BHA\"";
604        let file = Eu4File::from_slice(&data).unwrap();
605        let mut zip_sink = Vec::new();
606        let parsed_file = file.parse(&mut zip_sink).unwrap();
607        let binary = parsed_file.as_binary().unwrap();
608        let out = binary
609            .melter()
610            .on_failed_resolve(FailedResolveStrategy::Ignore)
611            .melt(&EnvTokens)
612            .unwrap();
613        assert_eq!(std::str::from_utf8(out.data()).unwrap(), &expected[..]);
614    }
615
616    #[test]
617    fn test_melt_skip_unknown_value() {
618        let data = [
619            0x45, 0x55, 0x34, 0x62, 0x69, 0x6e, 0x4d, 0x28, 0x01, 0x00, 0xff, 0xff, 0x89, 0x35,
620            0x01, 0x00, 0x0e, 0x00, 0x01, 0x38, 0x2a, 0x01, 0x00, 0x0f, 0x00, 0x03, 0x00, 0x42,
621            0x48, 0x41,
622        ];
623
624        let expected = "EU4txt\ndate=__unknown_0xffff\nplayer=\"BHA\"";
625        let file = Eu4File::from_slice(&data).unwrap();
626        let mut zip_sink = Vec::new();
627        let parsed_file = file.parse(&mut zip_sink).unwrap();
628        let binary = parsed_file.as_binary().unwrap();
629        let out = binary
630            .melter()
631            .on_failed_resolve(FailedResolveStrategy::Ignore)
632            .melt(&EnvTokens)
633            .unwrap();
634        assert_eq!(std::str::from_utf8(out.data()).unwrap(), &expected[..]);
635    }
636
637    #[test]
638    fn test_melt_from_entries() {
639        let data = [
640            0x45, 0x55, 0x34, 0x62, 0x69, 0x6e, 0x89, 0x35, 0x01, 0x00, 0x0e, 0x00, 0x01, 0x98,
641            0x35, 0x01, 0x00, 0x0e, 0x00, 0x01, 0x79, 0x01, 0x01, 0x00, 0x0f, 0x00, 0x03, 0x00,
642            0x42, 0x48, 0x41,
643        ];
644
645        let meta = Eu4Binary::from_slice(&data).unwrap();
646
647        let mut gdata = data.to_vec();
648        gdata[13] = 0x3a;
649        gdata[14] = 0x29;
650
651        let gamestate = Eu4Binary::from_slice(&gdata).unwrap();
652
653        let mut adata = data.to_vec();
654        adata[13] = 0x76;
655        adata[14] = 0x29;
656        let ai = Eu4Binary::from_slice(&adata).unwrap();
657
658        let out = Eu4Melter::from_entries(&meta, &gamestate, &ai)
659            .melt(&EnvTokens)
660            .unwrap();
661
662        assert_eq!(
663            std::str::from_utf8(out.data()).unwrap(),
664            "EU4txt\ncan_be_annexed=yes\nis_emperor=yes\nis_bankrupt=yes\nchecksum=\"BHA\""
665        );
666    }
667}