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 Inactive,
31
32 UnquoteAll,
34
35 UnquoteScalar,
37
38 QuoteScalar,
40
41 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
54pub 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
193pub struct MeltedDocument {
195 data: Vec<u8>,
196 unknown_tokens: HashSet<u16>,
197}
198
199impl MeltedDocument {
200 pub fn into_data(self) -> Vec<u8> {
202 self.data
203 }
204
205 pub fn data(&self) -> &[u8] {
207 self.data.as_slice()
208 }
209
210 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 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 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 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 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 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}