diff --git a/src/components.rs b/src/components.rs index 0b2e047..0d08bb3 100644 --- a/src/components.rs +++ b/src/components.rs @@ -1,15 +1,17 @@ use rltk::RGB; use specs::prelude::*; use specs_derive::*; +use specs::saveload::{Marker, ConvertSaveload}; +use serde::{Serialize, Deserialize}; +use specs::error::NoError; - -#[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, @@ -17,30 +19,30 @@ pub struct Renderable { pub render_order: i32, } -#[derive(Component, Debug)] +#[derive(Component, Serialize, Deserialize, Debug, Clone)] pub struct Player {} -#[derive(Component)] +#[derive(Component, ConvertSaveload, Clone)] pub struct Viewshed { pub visible_tiles: Vec, pub range: i32, pub dirty: bool, } -#[derive(Component, Debug)] +#[derive(Component, Serialize, Deserialize, Debug, Clone)] pub struct Monster {} -#[derive(Component, Debug)] +#[derive(Component, ConvertSaveload, Clone, Debug)] pub struct Name { pub name: String, } -#[derive(Component, Debug)] +#[derive(Component, Serialize, Deserialize, Debug, Clone)] pub struct BlocksTile {} -#[derive(Component, Debug)] +#[derive(Component, ConvertSaveload, Clone, Debug)] pub struct CombatStats { pub max_hp: i32, pub hp: i32, @@ -48,12 +50,12 @@ pub struct CombatStats { pub power: i32, } -#[derive(Component, Debug, Clone)] +#[derive(Component, ConvertSaveload, Clone, Debug)] pub struct WantsToMelee { pub target: Entity, } -#[derive(Component, Debug)] +#[derive(Component, ConvertSaveload, Clone, Debug)] pub struct SufferDamage { pub amount: Vec, } @@ -71,56 +73,61 @@ impl SufferDamage { } } -#[derive(Component, Debug)] +#[derive(Component, Serialize, Deserialize, Debug, Clone)] pub struct Item {} -#[derive(Component, Debug)] +#[derive(Component, ConvertSaveload, Clone, Debug)] pub struct ProvidesHealing { pub heal_amount: i32, } -#[derive(Component, Debug)] +#[derive(Component, ConvertSaveload, Debug)] pub struct InBackpack { pub owner: Entity, } -#[derive(Component, Debug, Clone)] +#[derive(Component, ConvertSaveload, Debug, Clone)] pub struct WantsToPickupItem { pub collected_by: Entity, pub item: Entity, } -#[derive(Component, Debug)] +#[derive(Component, ConvertSaveload, Debug)] pub struct WantsToUseItem { pub item: Entity, pub target: Option } -#[derive(Component, Debug, Clone)] +#[derive(Component, ConvertSaveload, Debug, Clone)] pub struct WantsToDropItem { pub item: Entity, } -#[derive(Component, Debug)] +#[derive(Component, Serialize, Deserialize, Clone, Debug)] pub struct Consumable {} -#[derive(Component, Debug)] +#[derive(Component, ConvertSaveload, Clone, Debug)] pub struct Ranged { pub range : i32 } -#[derive(Component, Debug)] +#[derive(Component, ConvertSaveload, Clone, Debug)] pub struct InflictsDamage { pub damage : i32 } -#[derive(Component, Debug)] +#[derive(Component, ConvertSaveload, Clone, Debug)] pub struct AreaOfEffect { pub radius : i32 } -#[derive(Component, Debug)] +#[derive(Component, ConvertSaveload, Clone, Debug)] pub struct Confusion { pub turns : i32 } pub struct SerializeMe; + +#[derive(Component, Serialize, Deserialize, Clone)] +pub struct SerializationHelper { + pub map : super::map::Map +} \ No newline at end of file diff --git a/src/gui.rs b/src/gui.rs index 0fa4ee9..303312a 100644 --- a/src/gui.rs +++ b/src/gui.rs @@ -273,6 +273,7 @@ pub fn ranged_target(gs: &mut State, ctx : &mut Rltk, range: i32) -> (ItemMenuRe pub fn main_menu(gs: &mut State, ctx : &mut Rltk) -> MainMenuResult { + let save_exists = super::saveload_system::does_save_exist(); let runstate = gs.ecs.fetch::(); ctx.print_color_centered(15, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), "Rust Roguelike Tutorial"); @@ -284,11 +285,14 @@ pub fn main_menu(gs: &mut State, ctx : &mut Rltk) -> MainMenuResult { ctx.print_color_centered(24, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK), "Start a new game"); } - if selection == MainMenuSelection::LoadGame { - ctx.print_color_centered(25, RGB::named(rltk::MAGENTA), RGB::named(rltk::BLACK), "Load a save"); - } else { - ctx.print_color_centered(25, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK), "Load a save"); + if save_exists { + if selection == MainMenuSelection::LoadGame { + ctx.print_color_centered(25, RGB::named(rltk::MAGENTA), RGB::named(rltk::BLACK), "Load a save"); + } else { + ctx.print_color_centered(25, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK), "Load a save"); + } } + if selection == MainMenuSelection::Quit { ctx.print_color_centered(26, RGB::named(rltk::MAGENTA), RGB::named(rltk::BLACK), "Quit the game"); @@ -302,21 +306,27 @@ pub fn main_menu(gs: &mut State, ctx : &mut Rltk) -> MainMenuResult { match key { VirtualKeyCode::Escape => {return MainMenuResult::NoSelection { selected: MainMenuSelection::Quit }}, VirtualKeyCode::Up => { - let newselection; + let mut newselection; match selection { MainMenuSelection::NewGame => newselection = MainMenuSelection::Quit, MainMenuSelection::LoadGame => newselection = MainMenuSelection::NewGame, MainMenuSelection::Quit => newselection = MainMenuSelection::LoadGame } + if newselection == MainMenuSelection::LoadGame && !save_exists { + newselection = MainMenuSelection::NewGame; + } return MainMenuResult::NoSelection { selected: newselection } } VirtualKeyCode::Down => { - let newselection; + let mut newselection; match selection { MainMenuSelection::NewGame => newselection = MainMenuSelection::LoadGame, MainMenuSelection::LoadGame => newselection = MainMenuSelection::Quit, MainMenuSelection::Quit => newselection = MainMenuSelection::NewGame } + if newselection == MainMenuSelection::LoadGame && !save_exists { + newselection = MainMenuSelection::Quit; + } return MainMenuResult::NoSelection { selected: newselection } } VirtualKeyCode::Return => {return MainMenuResult::Selected { selected: selection }} diff --git a/src/main.rs b/src/main.rs index 927665a..2640848 100644 --- a/src/main.rs +++ b/src/main.rs @@ -42,6 +42,8 @@ mod spawner; mod inventory_system; use inventory_system::*; +mod saveload_system; + #[derive(PartialEq, Copy, Clone)] pub enum RunState { AwaitingInput, @@ -227,17 +229,18 @@ impl GameState for State { } gui::MainMenuResult::Selected { selected } => match selected { gui::MainMenuSelection::NewGame => newrunstate = RunState::PreRun, - gui::MainMenuSelection::LoadGame => newrunstate = RunState::PreRun, + gui::MainMenuSelection::LoadGame => { + saveload_system::load_game(&mut self.ecs); + newrunstate = RunState::AwaitingInput; + saveload_system::delete_save(); + } gui::MainMenuSelection::Quit => ::std::process::exit(0), }, } } RunState::SaveGame => { - let data = serde_json::to_string(&*self.ecs.fetch::()).unwrap(); - println!("{}", data); - newrunstate = RunState::MainMenu { - menu_selection: gui::MainMenuSelection::LoadGame, - }; + saveload_system::save_game(&mut self.ecs); + newrunstate = RunState::MainMenu { menu_selection: gui::MainMenuSelection::LoadGame }; } } @@ -279,10 +282,11 @@ fn main() -> rltk::BError { gs.ecs.register::(); gs.ecs.register::(); gs.ecs.register::>(); + gs.ecs.register::(); gs.ecs.insert(SimpleMarkerAllocator::::new()); - let map = Map::new_map_rooms_and_corridors(); + let map = Map::new_map_rooms_and_corridors(1); let (player_x, player_y) = map.rooms[0].center(); gs.ecs.insert(Point::new(player_x, player_y)); diff --git a/src/map.rs b/src/map.rs index cd62d97..eac9051 100644 --- a/src/map.rs +++ b/src/map.rs @@ -23,6 +23,7 @@ pub struct Map { pub revealed_tiles: Vec, pub visible_tiles: Vec, pub blocked : Vec, + pub depth : i32, #[serde(skip_serializing)] #[serde(skip_deserializing)] @@ -61,7 +62,7 @@ impl Map { } } - pub fn new_map_rooms_and_corridors() -> Map { + pub fn new_map_rooms_and_corridors(new_depth : i32) -> Map { let mut map = Map { tiles: vec![TileType::Wall; MAPCOUNT], rooms: Vec::new(), @@ -70,7 +71,8 @@ impl Map { revealed_tiles: vec![false; MAPCOUNT], visible_tiles: vec![false; MAPCOUNT], blocked: vec![false; MAPCOUNT], - tile_content: vec![Vec::new(); MAPCOUNT] + tile_content: vec![Vec::new(); MAPCOUNT], + depth: new_depth }; const MAX_ROOMS: i32 = 30; diff --git a/src/saveload_system.rs b/src/saveload_system.rs new file mode 100644 index 0000000..517e818 --- /dev/null +++ b/src/saveload_system.rs @@ -0,0 +1,122 @@ +use specs::prelude::*; +use specs::saveload::{SimpleMarker, SimpleMarkerAllocator, SerializeComponents, DeserializeComponents, MarkedBuilder}; +use specs::error::NoError; +use super::components::*; +use std::fs::File; +use std::path::Path; +use std::fs; + + +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(); + )* + }; +} + +macro_rules! deserialize_individually { + ($ecs:expr, $de:expr, $data:expr, $( $type:ty),*) => { + $( + DeserializeComponents::::deserialize( + &mut ( &mut $ecs.write_storage::<$type>(), ), + &mut $data.0, // entities + &mut $data.1, // marker + &mut $data.2, // allocater + &mut $de, + ) + .unwrap(); + )* + }; +} + +#[cfg(target_arch = "wasm32")] +pub fn save_game(_ecs : &mut World) { +} + +#[cfg(not(target_arch = "wasm32"))] +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("Crash on cleanup"); +} + +pub fn does_save_exist() -> bool { + Path::new("./savegame.json").exists() +} + +pub fn load_game(ecs : &mut World) { + { + // Delete everything + let mut to_delete = Vec::new(); + for e in ecs.entities().join() { + to_delete.push(e); + } + for del in to_delete.iter() { + ecs.delete_entity(*del).expect("Deletion failed"); + } + } + + let data = fs::read_to_string("./savegame.json").unwrap(); + let mut de = serde_json::Deserializer::from_str(&data); + + { + let mut d = (&mut ecs.entities(), &mut ecs.write_storage::>(), &mut ecs.write_resource::>()); + + deserialize_individually!(ecs, de, d, Position, Renderable, Player, Viewshed, Monster, + Name, BlocksTile, CombatStats, SufferDamage, WantsToMelee, Item, Consumable, Ranged, InflictsDamage, + AreaOfEffect, Confusion, ProvidesHealing, InBackpack, WantsToPickupItem, WantsToUseItem, + WantsToDropItem, SerializationHelper + ); + } + + let mut deleteme : Option = None; + { + let entities = ecs.entities(); + let helper = ecs.read_storage::(); + let player = ecs.read_storage::(); + let position = ecs.read_storage::(); + for (e,h) in (&entities, &helper).join() { + let mut worldmap = ecs.write_resource::(); + *worldmap = h.map.clone(); + worldmap.tile_content = vec![Vec::new(); super::map::MAPCOUNT]; + deleteme = Some(e); + } + for (e,_p,pos) in (&entities, &player, &position).join() { + let mut ppos = ecs.write_resource::(); + *ppos = rltk::Point::new(pos.x, pos.y); + let mut player_resource = ecs.write_resource::(); + *player_resource = e; + } + } + ecs.delete_entity(deleteme.unwrap()).expect("Unable to delete helper"); +} + +pub fn delete_save() { + if Path::new("./savegame.json").exists() { std::fs::remove_file("./savegame.json").expect("Unable to delete file"); } +} \ No newline at end of file