Skip to main content
Verticopolis engineGitHub

verticopolis_engine/
load.rs

1//! Saves: writing a game to a `.vctower` file and reading one back.
2//!
3//! A `.vctower` file is the web game's save format: a first line naming the
4//! format ([`TOWER_FILE_MAGIC`]), then the save JSON, compressed. A save from
5//! any older version loads; the loader upgrades it first. A save from a newer
6//! version is refused.
7//!
8//! ```
9//! use verticopolis_engine::clock::GameMode;
10//! use verticopolis_engine::load::{
11//!     decode_vctower, deserialize, encode_simulation_vctower, mark_founder_from_loaded_file,
12//! };
13//! use verticopolis_engine::sim::Simulation;
14//!
15//! let sim = Simulation::new_game(5, GameMode::Classic);
16//! let bytes = encode_simulation_vctower(&sim, "my-host 1.0.0", 1_791_849_600_000.0).unwrap();
17//!
18//! let text = String::from_utf8(bytes).unwrap();
19//! let save = decode_vctower(&text).unwrap();
20//! let mut loaded = deserialize(&save).unwrap();
21//! mark_founder_from_loaded_file(&mut loaded, &save);
22//! assert_eq!(loaded.money, sim.money);
23//! ```
24//!
25//! A save holds the tower and leaves out the people walking around and the
26//! elevators' dispatch state, so a loaded game restarts both and does not
27//! track the game that wrote it. Two loads of the same save do track each
28//! other exactly.
29//!
30//! The loader repairs: [`deserialize`] corrects damaged or edited fields the
31//! way the web game's loader does, and refuses only a file it cannot read.
32//! The JSON inside the file is what
33//! [`Simulation::serialize`](crate::sim::Simulation::serialize) returns.
34
35use std::collections::{HashMap, HashSet};
36
37use base64::Engine;
38use indexmap::IndexMap;
39use serde_json::{json, Map, Value};
40
41use crate::clock::{CalendarKind, Clock, GameMode};
42use crate::econ::{classic_ladder, rent_config};
43use crate::events::PendingChoice;
44use crate::facilities::*;
45use crate::jsmath::{number_to_string, round};
46use crate::ledger::Ledger;
47use crate::rent::snap_to_ladder;
48use crate::rng::Rng;
49use crate::schedule::Schedule;
50use crate::sim::{LogEntry, LogKind, Simulation, LOG_RING_CAP, SAVE_VERSION};
51use crate::sim_loop::weather_for;
52use crate::tower::{unit_list_headroom, Transport, Unit, UnitState};
53
54const SOLD_CONDO_MIN_PRICE: f64 = 60_000.0;
55const SOLD_CONDO_MAX_PRICE: f64 = 240_000.0;
56const VIP_VISITS_CAP: f64 = 1_000_000.0;
57const LOG_TEXT_CAP: usize = 400;
58const LEGACY_CONDO_DEFAULT_PRICE: f64 = 120_000.0;
59/// `Number.MAX_SAFE_INTEGER`: saved whole numbers clamp to it in both loaders,
60/// so a value past `i64` reads the same in both. Without the clamp `as i64`
61/// saturates here to 2^63 - 1, which a double cannot hold exactly (#858).
62const MAX_SAFE_INTEGER: f64 = 9_007_199_254_740_991.0;
63pub(crate) const OLDEST_SAVE_VERSION: i64 = 1;
64/// `MAX_SAVE_INFLATED_BYTES` (saveCompression.ts).
65const MAX_SAVE_INFLATED_BYTES: u64 = 32 * 1024 * 1024;
66pub(crate) const UNIT_CAP: usize =
67    2 * (LOT_WIDTH as usize) * ((MAX_FLOOR - MIN_FLOOR + 1) as usize);
68const LOT: i64 = LOT_WIDTH;
69
70mod assemble;
71mod coerce;
72mod container;
73mod founder;
74mod guards;
75mod helpers;
76mod migrate;
77mod migrate_v1;
78mod migrate_v4;
79mod migrate_v5;
80mod reader;
81
82pub(crate) use assemble::*;
83use coerce::*;
84pub use container::*;
85pub use founder::*;
86use guards::*;
87pub(crate) use helpers::*;
88pub use migrate::*;
89pub use migrate_v1::*;
90pub use migrate_v4::*;
91pub use migrate_v5::*;
92pub use reader::*;
93
94#[cfg(test)]
95#[path = "load_assemble_tests.rs"]
96mod assemble_tests;
97#[cfg(test)]
98mod loader_cases;
99#[cfg(test)]
100mod save_json_tests;
101#[cfg(test)]
102mod text_tests;
103#[cfg(test)]
104mod vctower_encode_tests;