From 48609aee1f4c2ba3ca29dceae8e2f92927a840ea Mon Sep 17 00:00:00 2001 From: Timothy Warren Date: Mon, 8 Nov 2021 13:58:40 -0500 Subject: [PATCH] Implement basic game saving --- Cargo.toml | 6 ++-- src/components.rs | 82 +++++++++++++++++++++++------------------- src/main.rs | 15 ++++++++ src/map.rs | 7 +++- src/player.rs | 3 ++ src/rect.rs | 3 ++ src/saveload_system.rs | 79 ++++++++++++++++++++++++++++++++++++++++ src/spawner.rs | 8 ++++- 8 files changed, 163 insertions(+), 40 deletions(-) create mode 100644 src/saveload_system.rs diff --git a/Cargo.toml b/Cargo.toml index 5cb0d18..7e6bfa9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,8 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -rltk = { version = "0.8.0" } -specs = "0.16.1" +rltk = { version = "0.8.0", features=["serde"] } +specs = {version = "0.16.1", features=["serde"] } specs-derive = "0.4.1" +serde= { version="1.0.93", features = ["derive"]} +serde_json = "1.0.39" \ No newline at end of file diff --git a/src/components.rs b/src/components.rs index 2ff5660..3f774f7 100644 --- a/src/components.rs +++ b/src/components.rs @@ -1,14 +1,17 @@ use rltk::RGB; +use serde::{Deserialize, Serialize}; +use specs::error::NoError; use specs::prelude::*; +use specs::saveload::{ConvertSaveload, Marker}; use specs_derive::*; -#[derive(Component)] +#[derive(Component, ConvertSaveload, Clone)] pub struct Position { pub x: i32, pub y: i32, } -#[derive(Component)] +#[derive(Component, ConvertSaveload, Clone)] pub struct Renderable { pub glyph: rltk::FontCharType, pub fg: RGB, @@ -16,10 +19,10 @@ pub struct Renderable { pub render_order: i32, } -#[derive(Component, Debug, Default)] +#[derive(Component, Debug, Clone, Serialize, Deserialize, Default)] pub struct Player {} -#[derive(Component)] +#[derive(Component, ConvertSaveload, Clone)] pub struct Viewshed { pub visible_tiles: Vec, pub range: i32, @@ -36,10 +39,10 @@ impl Default for Viewshed { } } -#[derive(Component, Debug, Default)] +#[derive(Component, Debug, Serialize, Deserialize, Clone, Default)] pub struct Monster {} -#[derive(Component, Debug)] +#[derive(Component, Debug, ConvertSaveload, Clone)] pub struct Name { pub name: String, } @@ -52,10 +55,10 @@ impl Name { } } -#[derive(Component, Debug, Default)] +#[derive(Component, Debug, Serialize, Deserialize, Clone, Default)] pub struct BlocksTile {} -#[derive(Component, Debug)] +#[derive(Component, Debug, ConvertSaveload, Clone)] pub struct CombatStats { pub max_hp: i32, pub hp: i32, @@ -63,12 +66,12 @@ pub struct CombatStats { pub power: i32, } -#[derive(Component, Debug, Clone)] +#[derive(Component, Debug, ConvertSaveload, Clone)] pub struct WantsToMelee { pub target: Entity, } -#[derive(Component, Debug)] +#[derive(Component, Debug, ConvertSaveload, Clone)] pub struct SufferDamage { pub amount: Vec, } @@ -86,55 +89,62 @@ impl SufferDamage { } } -#[derive(Component, Debug, Default)] +#[derive(Component, Debug, Serialize, Deserialize, Clone, Default)] pub struct Item {} -#[derive(Component, Debug)] +#[derive(Component, Debug, Serialize, Deserialize, Clone, Default)] +pub struct Consumable {} + +#[derive(Component, Debug, ConvertSaveload, Clone)] +pub struct Ranged { + pub range: i32, +} + +#[derive(Component, Debug, ConvertSaveload, Clone)] +pub struct InflictsDamage { + pub damage: i32, +} + +#[derive(Component, Debug, ConvertSaveload, Clone)] +pub struct AreaOfEffect { + pub radius: i32, +} + +#[derive(Component, Debug, ConvertSaveload, Clone)] +pub struct Confusion { + pub turns: i32, +} + +#[derive(Component, Debug, ConvertSaveload, Clone)] pub struct ProvidesHealing { pub heal_amount: i32, } -#[derive(Component, Debug, Clone)] +#[derive(Component, Debug, ConvertSaveload)] pub struct InBackpack { pub owner: Entity, } -#[derive(Component, Debug, Clone)] +#[derive(Component, Debug, ConvertSaveload)] pub struct WantsToPickupItem { pub collected_by: Entity, pub item: Entity, } -#[derive(Component, Debug)] +#[derive(Component, Debug, ConvertSaveload)] pub struct WantsToUseItem { pub item: Entity, pub target: Option, } -#[derive(Component, Debug, Clone)] +#[derive(Component, Debug, ConvertSaveload)] pub struct WantsToDropItem { pub item: Entity, } -#[derive(Component, Debug, Default)] -pub struct Consumable {} +pub struct SerializeMe; -#[derive(Component, Debug)] -pub struct Ranged { - pub range: i32, -} - -#[derive(Component, Debug)] -pub struct InflictsDamage { - pub damage: i32, -} - -#[derive(Component, Debug)] -pub struct AreaOfEffect { - pub radius: i32, -} - -#[derive(Component, Debug)] -pub struct Confusion { - pub turns: i32, +#[derive(Component, Serialize, Deserialize, Clone)] +pub struct SerializationHelper { + pub map: crate::map::Map, } diff --git a/src/main.rs b/src/main.rs index abea450..2251538 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,7 @@ use rltk::{GameState, Point, Rltk}; use specs::prelude::*; +use specs::saveload::{SimpleMarker, SimpleMarkerAllocator}; +extern crate serde; mod components; mod damage_system; @@ -12,6 +14,7 @@ mod melee_combat_system; mod monster_ai_system; mod player; mod rect; +pub mod saveload_system; mod spawner; mod visibility_system; @@ -55,6 +58,7 @@ pub enum RunState { MainMenu { menu_selection: gui::MainMenuSelection, }, + SaveGame, } pub struct State { @@ -240,6 +244,13 @@ impl GameState for State { }, } } + RunState::SaveGame => { + saveload_system::save_game(&mut self.ecs); + + newrunstate = RunState::MainMenu { + menu_selection: gui::MainMenuSelection::LoadGame, + } + } } { @@ -283,8 +294,12 @@ fn main() -> rltk::BError { InflictsDamage, AreaOfEffect, Confusion, + SimpleMarker, + SerializationHelper, ); + gs.ecs.insert(SimpleMarkerAllocator::::new()); + let map = Map::new_map_rooms_and_corridors(); let (player_x, player_y) = map.rooms[0].center(); diff --git a/src/map.rs b/src/map.rs index 6bdf698..1a1d230 100644 --- a/src/map.rs +++ b/src/map.rs @@ -1,5 +1,6 @@ use crate::Rect; use rltk::{Algorithm2D, BaseMap, Point, RandomNumberGenerator, Rltk, SmallVec, RGB}; +use serde::{Deserialize, Serialize}; use specs::prelude::*; use std::cmp::{max, min}; @@ -7,12 +8,13 @@ pub const MAP_WIDTH: usize = 80; pub const MAP_HEIGHT: usize = 43; pub const MAP_COUNT: usize = MAP_HEIGHT * MAP_WIDTH; -#[derive(PartialEq, Copy, Clone)] +#[derive(PartialEq, Copy, Clone, Serialize, Deserialize)] pub enum TileType { Wall, Floor, } +#[derive(Default, Serialize, Deserialize, Clone)] pub struct Map { pub tiles: Vec, pub rooms: Vec, @@ -21,6 +23,9 @@ pub struct Map { pub revealed_tiles: Vec, pub visible_tiles: Vec, pub blocked: Vec, + + #[serde(skip_serializing)] + #[serde(skip_deserializing)] pub tile_content: Vec>, } diff --git a/src/player.rs b/src/player.rs index 535fc73..6ce3ca4 100644 --- a/src/player.rs +++ b/src/player.rs @@ -104,6 +104,9 @@ pub fn player_input(gs: &mut State, ctx: &mut Rltk) -> RunState { // Show item drop screen VirtualKeyCode::D => return RunState::ShowDropItem, + // Save and Quit + VirtualKeyCode::Escape => return RunState::SaveGame, + _ => return RunState::AwaitingInput, }, } diff --git a/src/rect.rs b/src/rect.rs index 0b567ee..416178e 100644 --- a/src/rect.rs +++ b/src/rect.rs @@ -1,3 +1,6 @@ +use serde::{Deserialize, Serialize}; + +#[derive(PartialEq, Copy, Clone, Serialize, Deserialize)] pub struct Rect { pub x1: i32, pub x2: i32, diff --git a/src/saveload_system.rs b/src/saveload_system.rs new file mode 100644 index 0000000..b73dfee --- /dev/null +++ b/src/saveload_system.rs @@ -0,0 +1,79 @@ +use crate::components::*; +use specs::error::NoError; +use specs::prelude::*; +use specs::saveload::{ + DeserializeComponents, MarkedBuilder, SerializeComponents, SimpleMarker, SimpleMarkerAllocator, +}; +use std::fs; +use std::fs::File; +use std::path::Path; + +macro_rules! serialize_individually { + ($ecs:expr, $ser:expr, $data:expr, $( $type:ty),*) => { + $( + SerializeComponents::>::serialize( + &( $ecs.read_storage::<$type>(), ), + &$data.0, + &$data.1, + &mut $ser, + ) + .unwrap(); + )* + }; +} + +pub fn save_game(ecs: &mut World) { + // Create helper + let mapcopy = ecs.get_mut::().unwrap().clone(); + let savehelper = ecs + .create_entity() + .with(SerializationHelper { map: mapcopy }) + .marked::>() + .build(); + + // Actually serialize + { + let data = ( + ecs.entities(), + ecs.read_storage::>(), + ); + + let writer = File::create("./savegame.json").unwrap(); + let mut serializer = serde_json::Serializer::new(writer); + serialize_individually!( + ecs, + serializer, + data, + Position, + Renderable, + Player, + Viewshed, + Monster, + Name, + BlocksTile, + CombatStats, + SufferDamage, + WantsToMelee, + Item, + Consumable, + Ranged, + InflictsDamage, + AreaOfEffect, + Confusion, + ProvidesHealing, + InBackpack, + WantsToPickupItem, + WantsToUseItem, + WantsToDropItem, + SerializationHelper + ); + } + + // Clean up + ecs.delete_entity(savehelper) + .expect("Failed to clean up savehelper component"); +} + +pub fn does_save_exist() -> bool { + Path::new("./savegame.json").exists() +} diff --git a/src/spawner.rs b/src/spawner.rs index 9e3a661..ddf83a7 100644 --- a/src/spawner.rs +++ b/src/spawner.rs @@ -1,10 +1,11 @@ use crate::components::{ AreaOfEffect, BlocksTile, CombatStats, Confusion, Consumable, InflictsDamage, Item, Monster, - Name, Player, Position, ProvidesHealing, Ranged, Renderable, Viewshed, + Name, Player, Position, ProvidesHealing, Ranged, Renderable, SerializeMe, Viewshed, }; use crate::{Rect, MAP_WIDTH}; use rltk::{RandomNumberGenerator, RGB}; use specs::prelude::*; +use specs::saveload::{MarkedBuilder, SimpleMarker}; const MAX_MONSTERS: i32 = 4; const MAX_ITEMS: i32 = 2; @@ -31,6 +32,7 @@ pub fn player(ecs: &mut World, player_x: i32, player_y: i32) -> Entity { defense: 2, power: 5, }) + .marked::>() .build() } @@ -76,6 +78,7 @@ fn monster(ecs: &mut World, x: i32, y: i32, glyph: rltk::FontCharTy defense: 1, power: 4, }) + .marked::>() .build(); } @@ -166,6 +169,7 @@ fn magic_missile_scroll(ecs: &mut World, x: i32, y: i32) { .with(Consumable {}) .with(Ranged { range: 6 }) .with(InflictsDamage { damage: 8 }) + .marked::>() .build(); } @@ -199,6 +203,7 @@ fn fireball_scroll(ecs: &mut World, x: i32, y: i32) { .with(Ranged { range: 6 }) .with(InflictsDamage { damage: 20 }) .with(AreaOfEffect { radius: 3 }) + .marked::>() .build(); } @@ -216,5 +221,6 @@ fn confusion_scroll(ecs: &mut World, x: i32, y: i32) { .with(Consumable {}) .with(Ranged { range: 6 }) .with(Confusion { turns: 4 }) + .marked::>() .build(); }