src/save.rs 19.2 K · 590 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 world.insert_resource(SimTick(save.tick));
269 world.insert_resource(save.rng);
270 world.insert_resource(Zones::load(save.zones));
271 world.insert_resource(Blueprints(save.blueprints.into_iter().collect()));
272 world.insert_resource(save.homes);
273 world.insert_resource(save.designations);
274 world.insert_resource(save.goals);
275 world.insert_resource(save.log);
276 world.insert_resource(save.losses);
277 world.insert_resource(Affinity::default());
278 let mut state: SystemState<SpawnParams> = SystemState::new(world);
279 let (
280 mut commands,
281 colonist_assets,
282 item_assets,
283 structure_assets,
284 animal_assets,
285 mut materials,
286 ) = state.get_mut(world).expect("assets exist");
287 for (tile, item) in save.items {
288 spawn_stack(&mut commands, &item_assets, tile, item);
289 }
290 for (tile, kind, fuel) in save.structures {
291 let entity = spawn_structure(&mut commands, &structure_assets, tile, kind);
292 if let Some(fuel) = fuel {
293 commands.entity(entity).insert(Fuel(fuel));
294 }
295 }
296 for animal in save.animals {
297 let entity = spawn_animal(&mut commands, &animal_assets, animal.kind, animal.tile);
298 let mut entity = commands.entity(entity);
299 entity.insert(sim_pos(animal.pos));
300 if animal.tame {
301 entity.insert(Tame);
302 }
303 }
304 let entities: Vec<Entity> = save
305 .colonists
306 .iter()
307 .map(|c| {
308 let index = NAMES.iter().position(|n| *n == c.name).unwrap_or(0);
309 let entity = spawn_colonist(
310 &mut commands,
311 &colonist_assets,
312 &item_assets,
313 &mut materials,
314 index,
315 c.tile,
316 );
317 let mut e = commands.entity(entity);
318 e.insert((
319 Colonist {
320 name: c.name.clone(),
321 },
322 Name::new(c.name.clone()),
323 sim_pos(c.pos),
324 Rest(c.rest),
325 Hunger(c.hunger),
326 Carrying(c.carrying),
327 c.priorities.clone(),
328 Unhappy(c.unhappy),
329 Starvation(c.starvation),
330 c.skills.clone(),
331 Health(c.health),
332 c.personality,
333 ));
334 if let Some(mood) = c.mood {
335 e.insert(Mood(mood));
336 }
337 if let Some(born) = c.born {
338 e.insert(Age { born });
339 }
340 if let Some(family) = c.family {
341 e.insert(Family(family));
342 }
343 if let Some(born) = c.child_born {
344 e.insert((
345 Child { born },
346 Transform::from_translation(c.pos.extend(1.0))
347 .with_scale(Vec3::splat(CHILD_SCALE)),
348 ));
349 }
350 if let Some(wear) = c.tool_wear {
351 e.insert(Tool { wear });
352 }
353 if let Some(until) = c.sick_until {
354 e.insert(Sick { until });
355 }
356 if let Some((cooked, tick)) = c.last_meal {
357 e.insert(LastMeal { cooked, tick });
358 }
359 entity
360 })
361 .collect();
362 let mut relations = Relations::default();
363 for (c, &entity) in save.colonists.iter().zip(&entities) {
364 if let Some(&partner) = c.partner.and_then(|i| entities.get(i)) {
365 commands.entity(entity).insert(Partner(partner));
366 }
367 for &(other, opinion) in &c.opinions {
368 if let Some(&other) = entities.get(other) {
369 relations.set(entity, other, opinion);
370 }
371 }
372 }
373 commands.insert_resource(relations);
374 state.apply(world);
375 }
376
377 const SAVE_KEY: &str = "simulator-save";
378
379 #[cfg(target_arch = "wasm32")]
380 fn storage() -> Result<web_sys::Storage, String> {
381 web_sys::window()
382 .and_then(|w| w.local_storage().ok().flatten())
383 .ok_or_else(|| "no local storage".to_string())
384 }
385
386 #[cfg(target_arch = "wasm32")]
387 fn write_save(data: &str) -> Result<(), String> {
388 storage()?
389 .set_item(SAVE_KEY, data)
390 .map_err(|_| "storage is full".to_string())
391 }
392
393 #[cfg(target_arch = "wasm32")]
394 fn read_save() -> Result<String, String> {
395 storage()?
396 .get_item(SAVE_KEY)
397 .ok()
398 .flatten()
399 .ok_or_else(|| "no saved game".to_string())
400 }
401
402 #[cfg(not(target_arch = "wasm32"))]
403 fn save_path() -> std::path::PathBuf {
404 std::path::PathBuf::from(format!("{SAVE_KEY}.json"))
405 }
406
407 #[cfg(not(target_arch = "wasm32"))]
408 fn write_save(data: &str) -> Result<(), String> {
409 std::fs::write(save_path(), data).map_err(|e| e.to_string())
410 }
411
412 #[cfg(not(target_arch = "wasm32"))]
413 fn read_save() -> Result<String, String> {
414 std::fs::read_to_string(save_path()).map_err(|_| "no saved game".to_string())
415 }
416
417 fn save_game(world: &mut World, mut reader: Local<Option<SystemState<MessageReader<Action>>>>) {
418 let state = reader.get_or_insert_with(|| SystemState::new(world));
419 let requested = state
420 .get_mut(world)
421 .is_ok_and(|mut actions| actions.read().any(|a| *a == Action::Save));
422 if !requested {
423 return;
424 }
425 let save = snapshot(world);
426 let result = serde_json::to_string(&save)
427 .map_err(|e| e.to_string())
428 .and_then(|data| write_save(&data));
429 let tick = *world.resource::<SimTick>();
430 let message = match result {
431 Ok(()) => "Game saved".to_string(),
432 Err(e) => format!("Save failed: {e}"),
433 };
434 world.resource_mut::<ColonyLog>().push(tick, message);
435 }
436
437 fn start_load(world: &mut World, mut reader: Local<Option<SystemState<MessageReader<Action>>>>) {
438 let state = reader.get_or_insert_with(|| SystemState::new(world));
439 let requested = state
440 .get_mut(world)
441 .is_ok_and(|mut actions| actions.read().any(|a| *a == Action::Load));
442 if !requested {
443 return;
444 }
445 let size = world.resource::<WorldMap>().size;
446 let loaded = read_save().and_then(|data| {
447 let save: SaveGame = serde_json::from_str(&data).map_err(|e| e.to_string())?;
448 let map = restore_map(&save, size)?;
449 Ok((save, map))
450 });
451 match loaded {
452 Ok((save, map)) => {
453 world.insert_resource(WorldSeed(save.seed));
454 world.insert_resource(map);
455 world.write_message(WorldGenerated);
456 world.insert_resource(PendingLoad(save));
457 }
458 Err(e) => {
459 let tick = *world.resource::<SimTick>();
460 world
461 .resource_mut::<ColonyLog>()
462 .push(tick, format!("Load failed: {e}"));
463 }
464 }
465 }
466
467 fn finish_load(world: &mut World) {
468 let Some(PendingLoad(save)) = world.remove_resource::<PendingLoad>() else {
469 return;
470 };
471 let tick = SimTick(save.tick);
472 apply(world, save);
473 world
474 .resource_mut::<ColonyLog>()
475 .push(tick, "Game loaded".to_string());
476 }
477
478 #[cfg(test)]
479 mod tests {
480 use super::*;
481 use crate::{items::ItemKind, world::Plant};
482
483 fn app() -> App {
484 let mut app = App::new();
485 app.insert_resource(WorldSeed(7))
486 .insert_resource(generate(UVec2::splat(64), 7))
487 .insert_resource(SimTick(12345))
488 .insert_resource(SimRng::new(3))
489 .init_resource::<Zones>()
490 .init_resource::<Blueprints>()
491 .init_resource::<Homes>()
492 .init_resource::<Designations>()
493 .init_resource::<ColonyGoals>()
494 .init_resource::<ColonyLog>()
495 .init_resource::<Losses>()
496 .init_resource::<ColonistAssets>()
497 .init_resource::<ItemAssets>()
498 .init_resource::<StructureAssets>()
499 .init_resource::<AnimalAssets>()
500 .init_resource::<Assets<ColorMaterial>>()
501 .init_resource::<Relations>();
502 app
503 }
504
505 // Jobs, paths and reservations are not saved: loaded colonists start idle and choose again,
506 // which every job already handles after an interruption. Everything else round-trips.
507 #[test]
508 fn a_saved_colony_loads_back_the_same() {
509 let mut app = app();
510 let world = app.world_mut();
511 {
512 let mut map = world.resource_mut::<WorldMap>();
513 map.set_plant(UVec2::new(3, 3), Some(Plant::Crop));
514 map.set_growth(UVec2::new(3, 3), 0.42);
515 map.set_wall(UVec2::new(4, 4), true);
516 map.set_roof(UVec2::new(5, 5), true);
517 map.set_wear(UVec2::new(6, 6), 12.5);
518 }
519 world.resource_mut::<Blueprints>().0.insert(
520 UVec2::new(9, 9),
521 BuildKind::Structure(StructureKind::Statue),
522 );
523 let ada = world
524 .spawn((
525 Colonist { name: "Ada".into() },
526 sim_pos(Vec2::new(40.0, 40.0)),
527 TilePos(UVec2::new(2, 2)),
528 Hunger(0.4),
529 Family(1),
530 Tool { wear: 99 },
531 Carrying(Some(Item::new(ItemKind::Wood, 7))),
532 ))
533 .id();
534 let bram = world
535 .spawn((
536 Colonist {
537 name: "Bram".into(),
538 },
539 Family(1),
540 Partner(ada),
541 ))
542 .id();
543 world.entity_mut(ada).insert(Partner(bram));
544 world.resource_mut::<Relations>().set(ada, bram, 0.7);
545 world.spawn((Item::new(ItemKind::Meal, 3), TilePos(UVec2::new(1, 1))));
546 world.spawn((
547 Structure {
548 kind: StructureKind::Campfire,
549 },
550 TilePos(UVec2::new(8, 8)),
551 Fuel(500),
552 ));
553 let saved = snapshot(world);
554 let json = serde_json::to_string(&saved).unwrap();
555 assert!(json.len() < 100_000, "{}", json.len());
556
557 let mut fresh = self::app();
558 let world = fresh.world_mut();
559 world.spawn(Colonist {
560 name: "Stray".into(),
561 });
562 let loaded: SaveGame = serde_json::from_str(&json).unwrap();
563 let map = restore_map(&loaded, UVec2::splat(64)).unwrap();
564 world.insert_resource(map);
565 apply(world, loaded);
566
567 let map = world.resource::<WorldMap>();
568 assert_eq!(map.growth(IVec2::new(3, 3)), 0.42);
569 assert!(map.is_wall(IVec2::new(4, 4)) && map.is_roofed(IVec2::new(5, 5)));
570 assert_eq!(map.wear(IVec2::new(6, 6)), 12.5);
571 assert_eq!(map.worn_tiles(), vec![UVec2::new(6, 6)]);
572 assert_eq!(world.resource::<SimTick>().0, 12345);
573 assert_eq!(world.resource::<Blueprints>().0.len(), 1);
574 let mut colonists = world.query::<(Entity, &Colonist, Option<&Partner>, &Hunger)>();
575 let rows: Vec<_> = colonists.iter(world).collect();
576 assert_eq!(rows.len(), 2);
577 let find = |name: &str| rows.iter().find(|r| r.1.name == name).unwrap();
578 let (ada, bram) = (find("Ada"), find("Bram"));
579 assert_eq!(ada.2.map(|p| p.0), Some(bram.0));
580 let (ada_e, bram_e) = (ada.0, bram.0);
581 assert_eq!(ada.3.0, 0.4);
582 assert_eq!(world.get::<Tool>(ada.0), Some(&Tool { wear: 99 }));
583 assert_eq!(world.get::<Carrying>(ada.0).unwrap().0.unwrap().count, 7);
584 let fuel: Vec<u32> = world.query::<&Fuel>().iter(world).map(|f| f.0).collect();
585 assert_eq!(fuel, vec![500]);
586 assert_eq!(world.query::<&Item>().iter(world).count(), 1);
587 let opinion = world.resource::<Relations>().opinion(ada_e, bram_e);
588 assert!((opinion - 0.7).abs() < 1e-6, "{opinion}");
589 }
590 }