verticopolis_engine/load/
reader.rs1use super::*;
4use crate::economy::visitors::census_follows_the_day;
5use crate::tower::Extras;
6
7#[doc(hidden)]
8pub const NEWER_SAVE_REFUSAL: &str =
10 "This save was made by a newer version of Verticopolis. Update the game to load it.";
11
12pub fn deserialize(raw: &Value) -> Result<Simulation, String> {
18 deserialize_owned(raw.clone())
19}
20
21#[doc(hidden)]
25pub fn deserialize_tdt(raw: &Value) -> Result<Simulation, String> {
26 let mut sim = deserialize(raw)?;
27 if sim.mode != GameMode::Classic {
28 return Ok(sim);
29 }
30 let Some(source) = raw.get("transports").and_then(Value::as_array) else {
31 return Ok(sim);
32 };
33 let mut changed = false;
34 for shaft in &mut sim.tower.transports {
35 if shaft.kind != Kind::ElevatorExpress || shaft.bottom >= 0 {
36 continue;
37 }
38 let original = source.iter().find(|t| {
39 t.get("kind").and_then(Value::as_str) == Some("elevatorExpress")
40 && t.get("x").and_then(Value::as_i64) == Some(shaft.x)
41 && t.get("bottom").and_then(Value::as_i64) == Some(shaft.bottom)
42 && t.get("top").and_then(Value::as_i64) == Some(shaft.top)
43 });
44 let Some(authored) = original
45 .and_then(|t| t.get("skipFloors"))
46 .and_then(Value::as_array)
47 else {
48 continue;
49 };
50 let saved_skips: Vec<f64> = authored
51 .iter()
52 .filter_map(Value::as_f64)
53 .filter(|v| v.is_finite())
54 .collect();
55 let Some(skips) = shaft.skip_floors.as_mut() else {
56 continue;
57 };
58 shaft.preserve_basement_stops = true;
61 let before = skips.len();
62 skips.retain(|floor| *floor > 0.0 || saved_skips.contains(floor));
65 changed |= skips.len() != before;
66 }
67 if changed {
68 sim.tower.revision += 1;
69 }
70 Ok(sim)
71}
72
73pub fn deserialize_owned(raw: Value) -> Result<Simulation, String> {
77 if raw.is_null() {
80 return Err("not a tower save (the save is null)".into());
81 }
82 if let Some(version) = raw.get("version").and_then(Value::as_f64) {
86 if version.is_finite() && version > SAVE_VERSION as f64 {
87 return Err(NEWER_SAVE_REFUSAL.into());
88 }
89 }
90 let written = founder_keys(&raw);
93 let mut data = migrate_save_owned(raw);
94 let modern = data.get("mode").and_then(Value::as_str) == Some("modern");
95 let listed = match data.get_mut("units") {
96 Some(Value::Array(units)) => std::mem::take(units),
97 _ => Vec::new(),
98 };
99 let kept = listed
103 .iter()
104 .filter(|u| kind_of(u).is_some())
105 .take(UNIT_CAP + 1)
106 .count();
107 if kept > UNIT_CAP {
108 return Err(format!(
109 "This save lists more than {UNIT_CAP} units, more than the whole lot can hold, so it cannot be a real tower."
110 ));
111 }
112 let mut units = Vec::with_capacity(kept + unit_list_headroom(kept));
113 let mut ids = Vec::with_capacity(kept);
114 for entry in listed {
115 if let Some((unit, id)) = unit_from_value(&entry, modern) {
116 units.push(unit);
117 ids.push(id);
118 }
119 }
120 debug_assert_eq!(units.len(), kept, "unit_from_value skipped a counted entry");
121 Ok(assemble(&data, units, ids, &written))
122}
123
124pub(crate) fn founder_keys(raw: &Value) -> Value {
126 let mut keys = Map::new();
127 for key in ["founder", "appVersion"] {
128 if let Some(v) = raw.get(key) {
129 keys.insert(key.to_string(), v.clone());
130 }
131 }
132 Value::Object(keys)
133}
134
135pub(crate) fn unit_from_value(u: &Value, modern: bool) -> Option<(Unit, Option<i64>)> {
141 let kind = kind_of(u)?;
142 let cat = kind.facility().width;
143 let floor =
146 (round(num(u.get("floor"), 1.0)) as i64).clamp(MIN_FLOOR, kind.highest_base_floor());
147 let x = (round(num(u.get("x"), 0.0)) as i64).clamp(0, LOT - 1);
148 let cap = if kind.is_structural() { cat } else { LOT - x };
149 let width = (round(num(u.get("width"), cat as f64)) as i64)
150 .min(cap)
151 .min(LOT - x)
152 .max(1);
153 let state = if kind.is_structural() {
154 UnitState::Empty
155 } else {
156 parse_state(u.get("state")).unwrap_or(UnitState::Empty)
157 };
158 let not_owned = matches!(
159 state,
160 UnitState::Empty | UnitState::Construction | UnitState::Gutted
161 );
162 let ever_occupied =
163 u.get("everOccupied") == Some(&Value::Bool(true)) && !(not_owned && !kind.is_hotel());
164 let sold_condo = kind == Kind::Condo && ever_occupied;
165 let keep_household = ever_occupied && kind.has_household();
166 let ladder_priced = !modern && classic_ladder(kind).is_some();
167 let mut rent: Option<f64> = match u.get("rent") {
168 None => None,
169 Some(r) if ladder_priced => Some(r.as_f64().unwrap_or(f64::NAN)),
170 Some(r) => Some(num(
171 Some(r),
172 rent_config(kind).map(|c| c.default).unwrap_or(0.0),
173 )),
174 };
175 if let Some(r) = rent {
176 if kind == Kind::Condo {
177 let ladder = if modern {
178 None
179 } else {
180 classic_ladder(Kind::Condo)
181 };
182 if sold_condo {
183 rent = Some(match ladder {
184 Some(l) => js_max(l[0], js_min(l[3], r)),
185 None => js_max(SOLD_CONDO_MIN_PRICE, js_min(SOLD_CONDO_MAX_PRICE, r)),
186 });
187 } else if ladder.is_none() {
188 let band = rent_config(Kind::Condo).unwrap();
189 rent = Some(js_max(band.min, js_min(band.max, r)));
190 }
191 }
192 }
193 let residents = if keep_household && modern {
194 match u.get("residents") {
195 None => None,
196 Some(r) => {
197 let v = if r.as_f64().is_some_and(|x| x.is_finite()) {
198 r.as_f64().unwrap()
199 } else {
200 3.0
201 };
202 Some((round(v) as i64).clamp(2, 5))
203 }
204 }
205 } else {
206 None
207 };
208 let film_policy: Option<&'static str> = match u.get("filmPolicy").and_then(Value::as_str) {
209 Some("feature") => Some("feature"),
210 Some("blockbuster") => Some("blockbuster"),
211 Some("auto") => Some("auto"),
212 Some("revival") => Some("revival"),
213 Some("off") => Some("off"),
214 _ => None,
215 };
216 let subtype: Option<&'static str> = match (
217 u.get("subtype").and_then(Value::as_str),
218 kind.subtype_list(),
219 ) {
220 (Some(s), Some(list)) => list.iter().copied().find(|c| *c == s),
221 _ => None,
222 };
223 let retail = kind.subtype_list().is_some();
224 let retail_field = |key: &str| -> Option<f64> {
225 if !retail {
226 return None;
227 }
228 let v = u.get(key)?;
229 Some(num(Some(v), 0.0).max(0.0))
230 };
231 let no_rate =
232 rent_config(kind).is_some() && !modern && u.get("noRate") == Some(&Value::Bool(true));
233 let unit = Unit {
234 id: 0,
235 kind,
236 floor,
237 x,
238 width,
239 state,
240 satisfaction: if kind.is_structural() {
241 1.0
242 } else {
243 num(u.get("satisfaction"), 1.0).min(1.0).max(0.0)
244 },
245 occupants: if kind.attendance_cap().is_some() || kind.is_structural() {
246 0
247 } else {
248 num(u.get("occupants"), 0.0).clamp(0.0, MAX_SAFE_INTEGER) as i64
249 },
250 customers_in: None,
251 hotel_customers_in: None,
252 out_for_meal: None,
253 residents,
254 visitors_yest: if census_follows_the_day(kind) {
255 u.get("visitorsYest")
256 .map(|v| num(Some(v), 0.0).clamp(0.0, f64::from(u8::MAX)).floor() as u8)
257 } else {
258 None
259 },
260 ever_occupied,
261 pending_income: num(u.get("pendingIncome"), 0.0),
262 rent,
263 no_rate,
264 label: u
265 .get("label")
266 .and_then(Value::as_str)
267 .map(|s| s.to_string())
268 .unwrap_or_else(|| kind.facility().name.to_string()),
269 subtype,
270 extras: Extras {
271 vacate_reason: vacate_reason(u.get("vacateReason")),
272 vacate_at: u.get("vacateAt").map(|v| num(Some(v), 0.0)),
273 film_policy,
274 film_days: film_days(kind, u.get("filmDays")),
275 film_classic: cinema_flag(kind, u.get("filmClassic")),
276 film_blockbuster: modern && cinema_flag(kind, u.get("filmBlockbuster")),
278 patronage_today: retail_field("patronageToday"),
279 patronage_yest: retail_field("patronageYest"),
280 profit_today: retail_field("profitToday"),
281 profit_yest: retail_field("profitYest"),
282 complete_at: u.get("completeAt").and_then(Value::as_f64),
283 complete_at_raw: match u.get("completeAt") {
284 Some(v) if !v.is_number() => Some(v.clone()),
285 _ => None,
286 },
287 dirty_days: coerce_dirty_days(state, kind, u.get("dirtyDays")),
288 }
289 .boxed(),
290 };
291 Some((unit, sane_id(u.get("id"))))
292}