Skip to main content

banished_formats/
save.rs

1//! Banished save files (`.sav`): container, game-state stream, and the fields needed to check
2//! world generation. Layout notes are in docs/SAVE-FORMAT-AND-RESTORE.md.
3
4use crate::crs::{Reader, Writer};
5use crate::error::{format_err, Error, Result};
6use crate::stream;
7
8/// A save file: its two payloads, expanded. See the [module documentation](self).
9pub struct Save {
10    /// Expanded metadata stream (the first payload; see [`SaveMetadata`]).
11    pub metadata: Vec<u8>,
12    /// Expanded game-state stream (the second payload).
13    pub state: Vec<u8>,
14}
15
16/// The first payload (`SaveLoad::SaveMetaData`): what the load dialog shows. One block at version
17/// 3.
18///
19/// Original: Runtime `0x1800c3d00`.
20#[derive(Debug, Clone, PartialEq)]
21pub struct SaveMetadata {
22    /// The town's name, as the Load dialog shows it.
23    pub name: String,
24    /// The preview picture's width in pixels.
25    pub width: i32,
26    /// The preview picture's height in pixels.
27    pub height: i32,
28    /// `width × height` 4-byte texels (blue, green, red, unused) of the preview.
29    pub image: Vec<u8>,
30    /// From version 3: an unnamed value (2 in the samples), then the active mods.
31    pub value: i32,
32    /// The mods that were on when the town was saved.
33    pub mods: Vec<SavedMod>,
34    /// The strings were UTF-16 (2015 builds).
35    pub wide: bool,
36}
37
38/// A mod active when the game was saved: its name, its `.pkm` file and three values.
39#[derive(Debug, Clone, PartialEq)]
40pub struct SavedMod {
41    /// The mod's name.
42    pub name: String,
43    /// The mod's file name.
44    pub file: String,
45    /// Three values saved with the mod (not identified yet).
46    pub values: [u32; 3],
47}
48
49impl SaveMetadata {
50    /// Read a [`SaveMetadata`] from the bytes of its resource.
51    pub fn parse(data: &[u8]) -> Result<SaveMetadata> {
52        let mut r = Reader::new(data);
53        let mut meta = r.block(|r, version| {
54            let name = r.string()?;
55            let width = r.i32()?;
56            let height = r.i32()?;
57            let image = r
58                .bytes(width.max(0) as usize * height.max(0) as usize * 4)?
59                .to_vec();
60            let (value, mods) = if version > 2 {
61                let value = r.i32()?;
62                let mods = (0..r.count()?)
63                    .map(|_| {
64                        Ok(SavedMod {
65                            name: r.string()?,
66                            file: r.string()?,
67                            values: [r.u32()?, r.u32()?, r.u32()?],
68                        })
69                    })
70                    .collect::<Result<_>>()?;
71                (value, mods)
72            } else {
73                (0, Vec::new())
74            };
75            Ok(SaveMetadata {
76                name,
77                width,
78                height,
79                image,
80                value,
81                mods,
82                wide: false,
83            })
84        })?;
85        meta.wide = r.wide_seen;
86        Ok(meta)
87    }
88
89    /// The block at version 3.
90    pub fn write(&self) -> Vec<u8> {
91        let mut w = Writer::new();
92        w.wide_strings = self.wide;
93        let at = w.begin_block(3);
94        w.string(&self.name);
95        w.i32(self.width);
96        w.i32(self.height);
97        w.bytes(&self.image);
98        w.i32(self.value);
99        w.count(self.mods.len());
100        for m in &self.mods {
101            w.string(&m.name);
102            w.string(&m.file);
103            m.values.iter().for_each(|&v| w.u32(v));
104        }
105        w.end_block(at);
106        w.data
107    }
108}
109
110/// The file header's check value (`IO::Persistence`'s writer, Runtime around `0x1800b9500`):
111/// CRC-32 steps from `0xffffffff` over the block table, complemented, then over each block in
112/// 32 KiB pieces, complemented after each piece.
113fn header_check(table: &[u8], blocks: &[&[u8]]) -> u32 {
114    let step = |c: u32, data: &[u8]| !crate::crc::crc32_raw(c, data);
115    let mut c = step(!0, table);
116    for b in blocks {
117        for piece in b.chunks(stream::BLOCK_SIZE) {
118            c = step(c, piece);
119        }
120    }
121    c
122}
123
124/// Fields the game loop writes right after the reference table and settings block.
125///
126/// Original: `FUN_14009af60`.
127#[derive(Debug, Clone)]
128pub struct LoopHeader {
129    /// The climate profile's reference.
130    pub climate: String,
131    /// The start condition's reference.
132    pub start_condition: String,
133    /// The terrain profile's reference.
134    pub terrain: String,
135    /// The map seed.
136    pub seed: u32,
137    /// The map size: 0 small, 1 medium, 2 large.
138    pub size_index: i32,
139}
140
141/// The map component's terrain grid.
142///
143/// Original: `FUN_1400b59a0`.
144#[derive(Debug, Clone)]
145pub struct MapHeights {
146    /// Samples per side (`2n + 1`).
147    pub width: usize,
148    /// The heights, row by row: height × 256.
149    pub heights: Vec<i16>,
150}
151
152/// One cell of the gameplay map, as a save stores it (the cell is 48 bytes in memory).
153///
154/// Original: `FUN_14001b570`.
155#[derive(Debug, Clone, Copy, PartialEq, Eq)]
156pub struct MapCell {
157    // +0x00; +0x08
158    /// Whether the two object pointers are set.
159    pub objects: [bool; 2],
160    // +0x10
161    /// External reference id at (`u32::MAX` when unset).
162    pub external: u32,
163    // +0x1c..+0x25
164    /// Bytes.
165    pub flags: [u8; 9],
166    // +0x26
167    /// Not identified yet.
168    pub value: u16,
169    // +0x29; +0x28..+0x2d
170    /// Bytes; `[1]` is the cell type.
171    pub bytes: [u8; 6],
172}
173
174impl MapCell {
175    /// The cell's type (see `banished_terrain::CellType`).
176    pub fn cell_type(&self) -> u8 {
177        self.bytes[1]
178    }
179}
180
181/// The gameplay map (serializer version 4): `size × size` cells covering the centre of the terrain
182/// (the terrain grid is twice as wide).
183///
184/// Original: `FUN_1400a45d0`.
185#[derive(Debug, Clone)]
186pub struct MapCells {
187    /// The number of cells on each side.
188    pub size: usize,
189    /// The cells, row by row.
190    pub cells: Vec<MapCell>,
191    // +0x90
192    /// The nine values that end the map block.
193    pub trailer: [i16; 9],
194}
195
196fn u32_at(b: &[u8], at: usize) -> Result<u32> {
197    b.get(at..at + 4)
198        .map(|s| u32::from_le_bytes(s.try_into().unwrap()))
199        .ok_or(Error::Truncated { at, wanted: 4 })
200}
201
202// Game +0x138
203/// One saved event-log entry (`Game`, 0x28 bytes in memory).
204#[derive(Debug, Clone, PartialEq)]
205pub struct SavedEvent {
206    /// The colour of the entry, `0xAARRGGBB`.
207    pub color: u32,
208    /// The text.
209    pub text: String,
210    /// The kind of event.
211    pub kind: i32,
212    /// How serious the event is.
213    pub severity: i32,
214    /// The map cell it happened at (-1 for none).
215    pub cell: i32,
216    /// The category the log filters by.
217    pub category: i32,
218    /// The icon.
219    pub icon: i32,
220}
221
222// +0x10..+0x15
223/// The event log block: six notify flags, then the entries.
224#[derive(Debug, Clone)]
225pub struct SavedEventLog {
226    /// Which categories show a notification.
227    pub notify: [bool; 6],
228    /// The entries, oldest first.
229    pub entries: Vec<SavedEvent>,
230}
231
232impl Save {
233    /// Read a [`Save`] from the bytes of its resource.
234    pub fn parse(data: &[u8]) -> Result<Save> {
235        let (meta, state) = Save::payloads(data)?;
236        Ok(Save {
237            metadata: stream::decode(meta)?,
238            state: stream::decode(state)?,
239        })
240    }
241
242    /// Only the metadata, as the save list reads it: the game state is not expanded.
243    ///
244    /// Original: `Persistence::BeginGetSaveGameInfo`, then `SaveLoad::LoadMetaData`.
245    ///
246    /// Original: Runtime `0x1800c43a0`.
247    pub fn read_metadata(data: &[u8]) -> Result<SaveMetadata> {
248        SaveMetadata::parse(&stream::decode(Save::payloads(data)?.0)?)
249    }
250
251    /// The compressed metadata and game-state payloads, checked against the header.
252    fn payloads(data: &[u8]) -> Result<(&[u8], &[u8])> {
253        if data.len() < 2 {
254            return Err(Error::Truncated { at: 0, wanted: 2 });
255        }
256        let version = u16::from_le_bytes([data[0], data[1]]);
257        // Version 1: the first payload's offset and size at 0x08, the second's id, offset and
258        // size at 0x10. Version 2: `u16 2, u16 count, 0x80808080, u32 check`, then `{id,
259        // offset, size}` per payload from 0x0c.
260        let (first, second) = match version {
261            1 => ((0x08, 0x0c), (0x14, 0x18)),
262            2 => ((0x10, 0x14), (0x1c, 0x20)),
263            v => return Err(Error::Unsupported(format!("save container version {v}"))),
264        };
265        let payload = |(o, s): (usize, usize)| -> Result<&[u8]> {
266            let (off, size) = (u32_at(data, o)? as usize, u32_at(data, s)? as usize);
267            data.get(off..off + size).ok_or(Error::Truncated {
268                at: off,
269                wanted: size,
270            })
271        };
272        let (meta, state) = (payload(first)?, payload(second)?);
273        if state.as_ptr_range().end != data.as_ptr_range().end {
274            return format_err("game-state payload does not end at EOF");
275        }
276        if version == 2 && u32_at(data, 4)? == 0x8080_8080 {
277            let count = u16::from_le_bytes([data[2], data[3]]) as usize;
278            let table = data.get(12..12 + 12 * count).ok_or(Error::Truncated {
279                at: 12,
280                wanted: 12 * count,
281            })?;
282            if header_check(table, &[meta, state]) != u32_at(data, 8)? {
283                return format_err("save check value does not match");
284            }
285        }
286        Ok((meta, state))
287    }
288
289    /// The metadata: the town's name, the preview picture and the mods.
290    pub fn metadata(&self) -> Result<SaveMetadata> {
291        SaveMetadata::parse(&self.metadata)
292    }
293
294    /// A version 2 save file: the metadata as payload `0x24` and the game state as payload 1,
295    /// each compressed (`IO::CompressedStream`), behind the checked header.
296    pub fn write(metadata: &SaveMetadata, state: &[u8]) -> Vec<u8> {
297        let meta = stream::encode(&metadata.write());
298        let state = stream::encode(state);
299        const HEADER: usize = 12 + 2 * 12;
300        let mut table = Vec::new();
301        for (id, off, size) in [
302            (0, HEADER, meta.len()),
303            (1, HEADER + meta.len(), state.len()),
304        ] {
305            for v in [id as u32, off as u32, size as u32] {
306                table.extend_from_slice(&v.to_le_bytes());
307            }
308        }
309        let mut out = Vec::with_capacity(HEADER + meta.len() + state.len());
310        out.extend_from_slice(&2u16.to_le_bytes());
311        out.extend_from_slice(&2u16.to_le_bytes());
312        out.extend_from_slice(&0x8080_8080u32.to_le_bytes());
313        out.extend_from_slice(&header_check(&table, &[&meta, &state]).to_le_bytes());
314        out.extend_from_slice(&table);
315        out.extend_from_slice(&meta);
316        out.extend_from_slice(&state);
317        out
318    }
319
320    /// Find the event-log block (version 1: six notify bytes, a count and entries of colour,
321    /// text, type, severity, cell, category, icon). Located by scanning for a block whose
322    /// contents parse as exactly that and fill the block; the game-state order before it is
323    /// not walked.
324    pub fn event_log(&self) -> Option<SavedEventLog> {
325        let d = &self.state;
326        let marker = 0xaaaa_aaaau32.to_le_bytes();
327        let mut at = 0;
328        while let Some(p) = d[at..].windows(4).position(|w| w == marker) {
329            let start = at + p;
330            at = start + 4;
331            if let Some(log) = parse_event_block(&d[start..]) {
332                return Some(log);
333            }
334        }
335        None
336    }
337
338    /// External reference table: (id, path) pairs.
339    pub fn references(&self) -> Result<Vec<(u32, String)>> {
340        let mut r = Reader::new(&self.state);
341        r.block(|r, _| {
342            r.block(|r, _| {
343                let n = r.count()?;
344                (0..n).map(|_| Ok((r.u32()?, r.string()?))).collect()
345            })
346            .map(|refs| {
347                // Skip the rest of the outer block; only the table is needed.
348                skip_rest(r);
349                refs
350            })
351        })
352    }
353
354    /// The game loop's header: the seed, the map size, and the terrain, climate and start condition.
355    pub fn loop_header(&self) -> Result<LoopHeader> {
356        let refs = self.references()?;
357        let name = |id: u32| {
358            refs.iter()
359                .find(|(i, _)| *i == id)
360                .map(|(_, p)| p.clone())
361                .unwrap_or_default()
362        };
363        let mut r = Reader::new(&self.state);
364        r.block(|r, _| {
365            r.skip_block()?; // reference table
366            r.skip_block()?; // UI settings
367            let (climate, start, terrain) = (r.u32()?, r.u32()?, r.u32()?);
368            let seed = r.u32()?;
369            let size_index = r.i32()?;
370            skip_rest(r);
371            Ok(LoopHeader {
372                climate: name(climate),
373                start_condition: name(start),
374                terrain: name(terrain),
375                seed,
376                size_index,
377            })
378        })
379    }
380
381    /// Locate the map block by its dimension header (`2n, n, 32, 33, 2n+1, 33^2`).
382    pub fn map_heights(&self) -> Result<MapHeights> {
383        for n in [256i32, 384, 512] {
384            let pat: Vec<u8> = [2 * n, n, 32, 33, 2 * n + 1, 33 * 33]
385                .iter()
386                .flat_map(|v| v.to_le_bytes())
387                .collect();
388            if let Some(at) = self
389                .state
390                .windows(pat.len())
391                .position(|w| w == pat.as_slice())
392            {
393                let count = u32_at(&self.state, at + 24)? as usize;
394                let width = (2 * n + 1) as usize;
395                if count != width * width {
396                    return format_err("map height count does not match its dimensions");
397                }
398                let start = at + 28;
399                let bytes = self
400                    .state
401                    .get(start..start + 2 * count)
402                    .ok_or(Error::Truncated {
403                        at: start,
404                        wanted: 2 * count,
405                    })?;
406                let heights = bytes
407                    .chunks_exact(2)
408                    .map(|c| i16::from_le_bytes([c[0], c[1]]))
409                    .collect();
410                return Ok(MapHeights { width, heights });
411            }
412        }
413        format_err("no map block found")
414    }
415}
416
417impl Save {
418    /// Locate the gameplay map block (`0xaaaaaaaa, size, 4, n, n²`) and read its cells.
419    /// `n` is half the terrain size (see `map_heights`).
420    pub fn map_cells(&self) -> Result<MapCells> {
421        let n = (self.map_heights()?.width - 1) / 2;
422        let pat: Vec<u8> = [0xaaaa_aaaau32, 0, 4, n as u32, (n * n) as u32]
423            .iter()
424            .flat_map(|v| v.to_le_bytes())
425            .collect();
426        let at = self
427            .state
428            .windows(pat.len())
429            .position(|w| w[..4] == pat[..4] && w[8..] == pat[8..])
430            .ok_or_else(|| Error::Format("no map cell block found".into()))?;
431        let mut r = Reader::new(&self.state[at + 20..]);
432        let ptr = |r: &mut Reader| -> Result<bool> {
433            let set = r.u8()? != 0;
434            if set {
435                r.bytes(12)?;
436            }
437            Ok(set)
438        };
439        let mut cells = Vec::with_capacity(n * n);
440        for _ in 0..n * n {
441            let objects = [ptr(&mut r)?, ptr(&mut r)?];
442            let external = r.u32()?;
443            let flags: [u8; 9] = r.bytes(9)?.try_into().unwrap();
444            let value = u16::from_le_bytes(r.bytes(2)?.try_into().unwrap());
445            let bytes: [u8; 6] = r.bytes(6)?.try_into().unwrap();
446            cells.push(MapCell {
447                objects,
448                external,
449                flags,
450                value,
451                bytes,
452            });
453        }
454        let mut trailer = [0i16; 9];
455        for t in &mut trailer {
456            *t = i16::from_le_bytes(r.bytes(2)?.try_into().unwrap());
457        }
458        if r.u32()? != 0xbbbb_bbbb {
459            return format_err("map cell block does not end with its terminator");
460        }
461        Ok(MapCells {
462            size: n,
463            cells,
464            trailer,
465        })
466    }
467}
468
469fn skip_rest(r: &mut Reader) {
470    let rest = r.remaining();
471    let _ = r.bytes(rest);
472}
473
474fn parse_event_block(b: &[u8]) -> Option<SavedEventLog> {
475    let u32_at = |o: usize| {
476        b.get(o..o + 4)
477            .map(|s| u32::from_le_bytes(s.try_into().unwrap()))
478    };
479    // `aaaaaaaa | size | body | bbbbbbbb`; the body starts with the version.
480    let size = u32_at(4)? as usize;
481    if size < 14 || u32_at(8 + size)? != 0xbbbb_bbbb || u32_at(8)? != 1 {
482        return None;
483    }
484    let body = &b[8..8 + size];
485    if body[4..10].iter().any(|&v| v > 1) {
486        return None;
487    }
488    let notify: [bool; 6] = std::array::from_fn(|i| body[4 + i] == 1);
489    let mut o = 10;
490    let rd = |o: &mut usize| -> Option<u32> {
491        let v = body.get(*o..*o + 4)?;
492        *o += 4;
493        Some(u32::from_le_bytes(v.try_into().unwrap()))
494    };
495    let count = rd(&mut o)? as usize;
496    if count > 100_000 {
497        return None;
498    }
499    let mut entries = Vec::with_capacity(count);
500    for _ in 0..count {
501        let color = rd(&mut o)?;
502        let len = rd(&mut o)?;
503        // The length counts the terminating NUL; without the 8-bit flag the text is UTF-16
504        // (2015 builds).
505        let text = if len & 0x8000_0000 != 0 {
506            let n = (len & 0x7fff_ffff) as usize;
507            let text = body.get(o..o + n.checked_sub(1)?)?;
508            o += n;
509            String::from_utf8_lossy(text).into_owned()
510        } else {
511            let n = len as usize;
512            let units: Vec<u16> = body
513                .get(o..o + 2 * n.checked_sub(1)?)?
514                .chunks_exact(2)
515                .map(|c| u16::from_le_bytes([c[0], c[1]]))
516                .collect();
517            o += 2 * n;
518            String::from_utf16_lossy(&units)
519        };
520        let mut i = || rd(&mut o).map(|v| v as i32);
521        entries.push(SavedEvent {
522            color,
523            text,
524            kind: i()?,
525            severity: i()?,
526            cell: i()?,
527            category: i()?,
528            icon: i()?,
529        });
530    }
531    (o == body.len()).then_some(SavedEventLog { notify, entries })
532}