src/save.rs 19.3 K · 591 lines · raw · history

1 use bevy::{ecs::system::SystemState, prelude::*};
2 use serde::{Deserialize, Serialize};
3
4 use crate::{
5 actions::Action,
6 animals::{Animal, AnimalAssets, AnimalKind, Tame, spawn_animal},
7 building::{Blueprints, BuildKind},
8 colonist::{Colonist, ColonistAssets, NAMES, spawn_colonist},
9 designations::Designations,
10 economy::ColonyGoals,
11 family::{Affinity, CHILD_SCALE, Child, Family, Homes, Partner},
12 food::Hunger,
13 items::{Carrying, Item, ItemAssets, spawn_stack},
14 jobs::{Tool, WorkPriorities},
15 mood::{Age, LastMeal, Losses, Mood, Sick, Unhappy},
16 needs::Rest,
17 population::{ColonyLog, Starvation},
18 rng::SimRng,
19 sim::{SimPos, SimTick, sim_pos},
20 skills::Skills,
21 social::{Personality, Relations},
22 structures::{Fuel, Structure, StructureAssets, StructureKind, spawn_structure},
23 threats::Health,
24 world::{MapLayers, TilePos, WorldMap},
25 worldgen::{WorldGenerated, WorldSeed, generate},
26 zones::{Zones, ZonesSave},
27 };
28
29 pub struct SavePlugin;
30
31 impl Plugin for SavePlugin {
32 fn build(&self, app: &mut App) {
33 app.add_systems(Update, save_game)
34 .add_systems(PreUpdate, start_load)
35 .add_systems(PostUpdate, finish_load);
36 }
37 }
38
39 const SAVE_VERSION: u32 = 1;
40
41 #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
42 pub struct ColonistSave {
43 name: String,
44 pos: Vec2,
45 tile: UVec2,
46 rest: f32,
47 hunger: f32,
48 mood: Option<f32>,
49 born: Option<i64>,
50 family: Option<u32>,
51 partner: Option<usize>,
52 child_born: Option<u64>,
53 carrying: Option<Item>,
54 priorities: WorkPriorities,
55 tool_wear: Option<u32>,
56 sick_until: Option<u64>,
57 last_meal: Option<(bool, u64)>,
58 unhappy: u32,
59 starvation: u32,
60 #[serde(default)]
61 skills: Skills,
62 #[serde(default = "full_health")]
63 health: f32,
64 #[serde(default)]
65 personality: Personality,
66 #[serde(default)]
67 opinions: Vec<(usize, f32)>,
68 }
69
70 fn full_health() -> f32 {
71 1.0
72 }
73
74 #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
75 pub struct AnimalSave {
76 kind: AnimalKind,
77 pos: Vec2,
78 tile: UVec2,
79 tame: bool,
80 }
81
82 #[derive(Serialize, Deserialize, Debug)]
83 pub struct SaveGame {
84 version: u32,
85 seed: u32,
86 tick: u64,
87 rng: SimRng,
88 map: MapLayers,
89 zones: ZonesSave,
90 blueprints: Vec<(UVec2, BuildKind)>,
91 homes: Homes,
92 designations: Designations,
93 goals: ColonyGoals,
94 log: ColonyLog,
95 losses: Losses,
96 colonists: Vec<ColonistSave>,
97 items: Vec<(UVec2, Item)>,
98 structures: Vec<(UVec2, StructureKind, Option<u32>)>,
99 animals: Vec<AnimalSave>,
100 }
101
102 #[allow(clippy::type_complexity)]
103 pub fn snapshot(world: &mut World) -> SaveGame {
104 let mut colonist_query = world.query::<(
105 Entity,
106 &Colonist,
107 &SimPos,
108 &TilePos,
109 &Rest,
110 &Hunger,
111 &Carrying,
112 &WorkPriorities,
113 (
114 Option<&Mood>,
115 Option<&Age>,
116 Option<&Family>,
117 Option<&Partner>,
118 Option<&Child>,
119 Option<&Tool>,
120 Option<&Sick>,
121 Option<&LastMeal>,
122 Option<&Unhappy>,
123 Option<&Starvation>,
124 Option<&Skills>,
125 Option<&Health>,
126 Option<&Personality>,
127 ),
128 )>();
129 let rows: Vec<_> = colonist_query.iter(world).collect();
130 let order: Vec<Entity> = rows.iter().map(|r| r.0).collect();
131 let mut colonists: Vec<ColonistSave> = rows
132 .iter()
133 .map(
134 |&(_, colonist, pos, tile, rest, hunger, carrying, priorities, extra)| {
135 let (
136 mood,
137 age,
138 family,
139 partner,
140 child,
141 tool,
142 sick,
143 meal,
144 unhappy,
145 starvation,
146 skills,
147 health,
148 personality,
149 ) = extra;
150 ColonistSave {
151 name: colonist.name.clone(),
152 pos: pos.0,
153 tile: tile.0,
154 rest: rest.0,
155 hunger: hunger.0,
156 mood: mood.map(|m| m.0),
157 born: age.map(|a| a.born),
158 family: family.map(|f| f.0),
159 partner: partner.and_then(|p| order.iter().position(|&e| e == p.0)),
160 child_born: child.map(|c| c.born),
161 carrying: carrying.0,
162 priorities: priorities.clone(),
163 tool_wear: tool.map(|t| t.wear),
164 sick_until: sick.map(|s| s.until),
165 last_meal: meal.map(|m| (m.cooked, m.tick)),
166 unhappy: unhappy.map_or(0, |u| u.0),
167 starvation: starvation.map_or(0, |s| s.0),
168 skills: skills.cloned().unwrap_or_default(),
169 health: health.map_or(1.0, |h| h.0),
170 personality: personality.copied().unwrap_or_default(),
171 opinions: Vec::new(),
172 }
173 },
174 )
175 .collect();
176 let relations = world.resource::<Relations>();
177 for (i, save) in colonists.iter_mut().enumerate() {
178 save.opinions = relations
179 .of(order[i])
180 .filter_map(|(other, v)| order.iter().position(|&o| o == other).map(|j| (j, v)))
181 .filter(|&(j, _)| j > i)
182 .collect();
183 }
184 let items = world
185 .query::<(&Item, &TilePos)>()
186 .iter(world)
187 .map(|(item, pos)| (pos.0, *item))
188 .collect();
189 let structures = world
190 .query::<(&Structure, &TilePos, Option<&Fuel>)>()
191 .iter(world)
192 .map(|(s, pos, fuel)| (pos.0, s.kind, fuel.map(|f| f.0)))
193 .collect();
194 let animals = world
195 .query::<(&Animal, &SimPos, &TilePos, Has<Tame>)>()
196 .iter(world)
197 .map(|(animal, pos, tile, tame)| AnimalSave {
198 kind: animal.kind,
199 pos: pos.0,
200 tile: tile.0,
201 tame,
202 })
203 .collect();
204 let mut blueprints: Vec<(UVec2, BuildKind)> = world
205 .resource::<Blueprints>()
206 .0
207 .iter()
208 .map(|(t, k)| (*t, *k))
209 .collect();
210 blueprints.sort_by_key(|(t, _)| (t.y, t.x));
211 SaveGame {
212 version: SAVE_VERSION,
213 seed: world.resource::<WorldSeed>().0,
214 tick: world.resource::<SimTick>().0,
215 rng: world.resource::<SimRng>().clone(),
216 map: world.resource::<WorldMap>().layers(),
217 zones: world.resource::<Zones>().save(),
218 blueprints,
219 homes: world.resource::<Homes>().clone(),
220 designations: Designations(world.resource::<Designations>().0.clone()),
221 goals: world.resource::<ColonyGoals>().clone(),
222 log: ColonyLog(world.resource::<ColonyLog>().0.clone()),
223 losses: Losses(world.resource::<Losses>().0.clone()),
224 colonists,
225 items,
226 structures,
227 animals,
228 }
229 }
230
231 pub fn restore_map(save: &SaveGame, size: UVec2) -> Result<WorldMap, String> {
232 if save.version != SAVE_VERSION {
233 return Err(format!("unsupported save version {}", save.version));
234 }
235 let mut map = generate(size, save.seed);
236 map.restore(&save.map)?;
237 Ok(map)
238 }
239
240 #[derive(Resource)]
241 struct PendingLoad(SaveGame);
242
243 fn despawn_all<T: Component>(world: &mut World) {
244 let entities: Vec<Entity> = world
245 .query_filtered::<Entity, With<T>>()
246 .iter(world)
247 .collect();
248 for entity in entities {
249 world.despawn(entity);
250 }
251 }
252
253 type SpawnParams<'w, 's> = (
254 Commands<'w, 's>,
255 Res<'w, ColonistAssets>,
256 Res<'w, ItemAssets>,
257 Res<'w, StructureAssets>,
258 Res<'w, AnimalAssets>,
259 ResMut<'w, Assets<ColorMaterial>>,
260 );
261
262 pub fn apply(world: &mut World, save: SaveGame) {
263 despawn_all::<Colonist>(world);
264 despawn_all::<Animal>(world);
265 despawn_all::<Item>(world);
266 despawn_all::<Structure>(world);
267 despawn_all::<crate::raids::Raider>(world);
268 despawn_all::<crate::trading::Trader>(world);
269 world.insert_resource(SimTick(save.tick));
270 world.insert_resource(save.rng);
271 world.insert_resource(Zones::load(save.zones));
272 world.insert_resource(Blueprints(save.blueprints.into_iter().collect()));
273 world.insert_resource(save.homes);
274 world.insert_resource(save.designations);
275 world.insert_resource(save.goals);
276 world.insert_resource(save.log);
277 world.insert_resource(save.losses);
278 world.insert_resource(Affinity::default());
279 let mut state: SystemState<SpawnParams> = SystemState::new(world);
280 let (
281 mut commands,
282 colonist_assets,
283 item_assets,
284 structure_assets,
285 animal_assets,
286 mut materials,
287 ) = state.get_mut(world).expect("assets exist");
288 for (tile, item) in save.items {
289 spawn_stack(&mut commands, &item_assets, tile, item);
290 }
291 for (tile, kind, fuel) in save.structures {
292 let entity = spawn_structure(&mut commands, &structure_assets, tile, kind);
293 if let Some(fuel) = fuel {
294 commands.entity(entity).insert(Fuel(fuel));
295 }
296 }
297 for animal in save.animals {
298 let entity = spawn_animal(&mut commands, &animal_assets, animal.kind, animal.tile);
299 let mut entity = commands.entity(entity);
300 entity.insert(sim_pos(animal.pos));
301 if animal.tame {
302 entity.insert(Tame);
303 }
304 }
305 let entities: Vec<Entity> = save
306 .colonists
307 .iter()
308 .map(|c| {
309 let index = NAMES.iter().position(|n| *n == c.name).unwrap_or(0);
310 let entity = spawn_colonist(
311 &mut commands,
312 &colonist_assets,
313 &item_assets,
314 &mut materials,
315 index,
316 c.tile,
317 );
318 let mut e = commands.entity(entity);
319 e.insert((
320 Colonist {
321 name: c.name.clone(),
322 },
323 Name::new(c.name.clone()),
324 sim_pos(c.pos),
325 Rest(c.rest),
326 Hunger(c.hunger),
327 Carrying(c.carrying),
328 c.priorities.clone(),
329 Unhappy(c.unhappy),
330 Starvation(c.starvation),
331 c.skills.clone(),
332 Health(c.health),
333 c.personality,
334 ));
335 if let Some(mood) = c.mood {
336 e.insert(Mood(mood));
337 }
338 if let Some(born) = c.born {
339 e.insert(Age { born });
340 }
341 if let Some(family) = c.family {
342 e.insert(Family(family));
343 }
344 if let Some(born) = c.child_born {
345 e.insert((
346 Child { born },
347 Transform::from_translation(c.pos.extend(1.0))
348 .with_scale(Vec3::splat(CHILD_SCALE)),
349 ));
350 }
351 if let Some(wear) = c.tool_wear {
352 e.insert(Tool { wear });
353 }
354 if let Some(until) = c.sick_until {
355 e.insert(Sick { until });
356 }
357 if let Some((cooked, tick)) = c.last_meal {
358 e.insert(LastMeal { cooked, tick });
359 }
360 entity
361 })
362 .collect();
363 let mut relations = Relations::default();
364 for (c, &entity) in save.colonists.iter().zip(&entities) {
365 if let Some(&partner) = c.partner.and_then(|i| entities.get(i)) {
366 commands.entity(entity).insert(Partner(partner));
367 }
368 for &(other, opinion) in &c.opinions {
369 if let Some(&other) = entities.get(other) {
370 relations.set(entity, other, opinion);
371 }
372 }
373 }
374 commands.insert_resource(relations);
375 state.apply(world);
376 }
377
378 const SAVE_KEY: &str = "simulator-save";
379
380 #[cfg(target_arch = "wasm32")]
381 fn storage() -> Result<web_sys::Storage, String> {
382 web_sys::window()
383 .and_then(|w| w.local_storage().ok().flatten())
384 .ok_or_else(|| "no local storage".to_string())
385 }
386
387 #[cfg(target_arch = "wasm32")]
388 fn write_save(data: &str) -> Result<(), String> {
389 storage()?
390 .set_item(SAVE_KEY, data)
391 .map_err(|_| "storage is full".to_string())
392 }
393
394 #[cfg(target_arch = "wasm32")]
395 fn read_save() -> Result<String, String> {
396 storage()?
397 .get_item(SAVE_KEY)
398 .ok()
399 .flatten()
400 .ok_or_else(|| "no saved game".to_string())
401 }
402
403 #[cfg(not(target_arch = "wasm32"))]
404 fn save_path() -> std::path::PathBuf {
405 std::path::PathBuf::from(format!("{SAVE_KEY}.json"))
406 }
407
408 #[cfg(not(target_arch = "wasm32"))]
409 fn write_save(data: &str) -> Result<(), String> {
410 std::fs::write(save_path(), data).map_err(|e| e.to_string())
411 }
412
413 #[cfg(not(target_arch = "wasm32"))]
414 fn read_save() -> Result<String, String> {
415 std::fs::read_to_string(save_path()).map_err(|_| "no saved game".to_string())
416 }
417
418 fn save_game(world: &mut World, mut reader: Local<Option<SystemState<MessageReader<Action>>>>) {
419 let state = reader.get_or_insert_with(|| SystemState::new(world));
420 let requested = state
421 .get_mut(world)
422 .is_ok_and(|mut actions| actions.read().any(|a| *a == Action::Save));
423 if !requested {
424 return;
425 }
426 let save = snapshot(world);
427 let result = serde_json::to_string(&save)
428 .map_err(|e| e.to_string())
429 .and_then(|data| write_save(&data));
430 let tick = *world.resource::<SimTick>();
431 let message = match result {
432 Ok(()) => "Game saved".to_string(),
433 Err(e) => format!("Save failed: {e}"),
434 };
435 world.resource_mut::<ColonyLog>().push(tick, message);
436 }
437
438 fn start_load(world: &mut World, mut reader: Local<Option<SystemState<MessageReader<Action>>>>) {
439 let state = reader.get_or_insert_with(|| SystemState::new(world));
440 let requested = state
441 .get_mut(world)
442 .is_ok_and(|mut actions| actions.read().any(|a| *a == Action::Load));
443 if !requested {
444 return;
445 }
446 let size = world.resource::<WorldMap>().size;
447 let loaded = read_save().and_then(|data| {
448 let save: SaveGame = serde_json::from_str(&data).map_err(|e| e.to_string())?;
449 let map = restore_map(&save, size)?;
450 Ok((save, map))
451 });
452 match loaded {
453 Ok((save, map)) => {
454 world.insert_resource(WorldSeed(save.seed));
455 world.insert_resource(map);
456 world.write_message(WorldGenerated);
457 world.insert_resource(PendingLoad(save));
458 }
459 Err(e) => {
460 let tick = *world.resource::<SimTick>();
461 world
462 .resource_mut::<ColonyLog>()
463 .push(tick, format!("Load failed: {e}"));
464 }
465 }
466 }
467
468 fn finish_load(world: &mut World) {
469 let Some(PendingLoad(save)) = world.remove_resource::<PendingLoad>() else {
470 return;
471 };
472 let tick = SimTick(save.tick);
473 apply(world, save);
474 world
475 .resource_mut::<ColonyLog>()
476 .push(tick, "Game loaded".to_string());
477 }
478
479 #[cfg(test)]
480 mod tests {
481 use super::*;
482 use crate::{items::ItemKind, world::Plant};
483
484 fn app() -> App {
485 let mut app = App::new();
486 app.insert_resource(WorldSeed(7))
487 .insert_resource(generate(UVec2::splat(64), 7))
488 .insert_resource(SimTick(12345))
489 .insert_resource(SimRng::new(3))
490 .init_resource::<Zones>()
491 .init_resource::<Blueprints>()
492 .init_resource::<Homes>()
493 .init_resource::<Designations>()
494 .init_resource::<ColonyGoals>()
495 .init_resource::<ColonyLog>()
496 .init_resource::<Losses>()
497 .init_resource::<ColonistAssets>()
498 .init_resource::<ItemAssets>()
499 .init_resource::<StructureAssets>()
500 .init_resource::<AnimalAssets>()
501 .init_resource::<Assets<ColorMaterial>>()
502 .init_resource::<Relations>();
503 app
504 }
505
506 // Jobs, paths and reservations are not saved: loaded colonists start idle and choose again,
507 // which every job already handles after an interruption. Everything else round-trips.
508 #[test]
509 fn a_saved_colony_loads_back_the_same() {
510 let mut app = app();
511 let world = app.world_mut();
512 {
513 let mut map = world.resource_mut::<WorldMap>();
514 map.set_plant(UVec2::new(3, 3), Some(Plant::Crop));
515 map.set_growth(UVec2::new(3, 3), 0.42);
516 map.set_wall(UVec2::new(4, 4), true);
517 map.set_roof(UVec2::new(5, 5), true);
518 map.set_wear(UVec2::new(6, 6), 12.5);
519 }
520 world.resource_mut::<Blueprints>().0.insert(
521 UVec2::new(9, 9),
522 BuildKind::Structure(StructureKind::Statue),
523 );
524 let ada = world
525 .spawn((
526 Colonist { name: "Ada".into() },
527 sim_pos(Vec2::new(40.0, 40.0)),
528 TilePos(UVec2::new(2, 2)),
529 Hunger(0.4),
530 Family(1),
531 Tool { wear: 99 },
532 Carrying(Some(Item::new(ItemKind::Wood, 7))),
533 ))
534 .id();
535 let bram = world
536 .spawn((
537 Colonist {
538 name: "Bram".into(),
539 },
540 Family(1),
541 Partner(ada),
542 ))
543 .id();
544 world.entity_mut(ada).insert(Partner(bram));
545 world.resource_mut::<Relations>().set(ada, bram, 0.7);
546 world.spawn((Item::new(ItemKind::Meal, 3), TilePos(UVec2::new(1, 1))));
547 world.spawn((
548 Structure {
549 kind: StructureKind::Campfire,
550 },
551 TilePos(UVec2::new(8, 8)),
552 Fuel(500),
553 ));
554 let saved = snapshot(world);
555 let json = serde_json::to_string(&saved).unwrap();
556 assert!(json.len() < 100_000, "{}", json.len());
557
558 let mut fresh = self::app();
559 let world = fresh.world_mut();
560 world.spawn(Colonist {
561 name: "Stray".into(),
562 });
563 let loaded: SaveGame = serde_json::from_str(&json).unwrap();
564 let map = restore_map(&loaded, UVec2::splat(64)).unwrap();
565 world.insert_resource(map);
566 apply(world, loaded);
567
568 let map = world.resource::<WorldMap>();
569 assert_eq!(map.growth(IVec2::new(3, 3)), 0.42);
570 assert!(map.is_wall(IVec2::new(4, 4)) && map.is_roofed(IVec2::new(5, 5)));
571 assert_eq!(map.wear(IVec2::new(6, 6)), 12.5);
572 assert_eq!(map.worn_tiles(), vec![UVec2::new(6, 6)]);
573 assert_eq!(world.resource::<SimTick>().0, 12345);
574 assert_eq!(world.resource::<Blueprints>().0.len(), 1);
575 let mut colonists = world.query::<(Entity, &Colonist, Option<&Partner>, &Hunger)>();
576 let rows: Vec<_> = colonists.iter(world).collect();
577 assert_eq!(rows.len(), 2);
578 let find = |name: &str| rows.iter().find(|r| r.1.name == name).unwrap();
579 let (ada, bram) = (find("Ada"), find("Bram"));
580 assert_eq!(ada.2.map(|p| p.0), Some(bram.0));
581 let (ada_e, bram_e) = (ada.0, bram.0);
582 assert_eq!(ada.3.0, 0.4);
583 assert_eq!(world.get::<Tool>(ada.0), Some(&Tool { wear: 99 }));
584 assert_eq!(world.get::<Carrying>(ada.0).unwrap().0.unwrap().count, 7);
585 let fuel: Vec<u32> = world.query::<&Fuel>().iter(world).map(|f| f.0).collect();
586 assert_eq!(fuel, vec![500]);
587 assert_eq!(world.query::<&Item>().iter(world).count(), 1);
588 let opinion = world.resource::<Relations>().opinion(ada_e, bram_e);
589 assert!((opinion - 0.7).abs() < 1e-6, "{opinion}");
590 }
591 }