From 583ece9c12a55a4f3654b8ce9b407eb0737b76ef Mon Sep 17 00:00:00 2001
From: Evan Pratten <ewpratten@gmail.com>
Date: Sun, 3 Oct 2021 12:12:06 -0400
Subject: [PATCH] Loading and saving time data

Co-authored-by: Luna <LuS404@users.noreply.github.com>
---
 game/Cargo.toml                     |  4 +--
 game/src/context.rs                 |  6 ++--
 game/src/lib.rs                     | 12 ++++++++
 game/src/progress.rs                | 44 +++++++++++++++++++++++++++++
 game/src/scenes/ingame_scene/mod.rs | 13 ++++++++-
 savegame.json                       |  1 +
 6 files changed, 75 insertions(+), 5 deletions(-)
 create mode 100644 game/src/progress.rs
 create mode 100644 savegame.json

diff --git a/game/Cargo.toml b/game/Cargo.toml
index 4538ee0..e1da054 100644
--- a/game/Cargo.toml
+++ b/game/Cargo.toml
@@ -14,7 +14,7 @@ tracing = { version = "0.1", features = ["log"] }
 serde = { version = "1.0.126", features = ["derive"] }
 serde_json = "1.0.64"
 thiserror = "1.0"
-chrono = "0.4"
+chrono = { version = "0.4", features = ["serde"] }
 rust-embed = "6.2.0"
 raylib = { version = "3.5", git = "https://github.com/ewpratten/raylib-rs", rev = "2ae949cb3488dd1bb052ece71d61021c8dd6e910", features = [
     "serde"
@@ -31,7 +31,7 @@ pkg-version = "1.0"
 cfg-if = "1.0"
 num-derive = "0.3"
 num = "0.4"
-tiled = { version  ="0.9.5", default-features = false }
+tiled = { version = "0.9.5", default-features = false }
 async-trait = "0.1.51"
 webbrowser = "0.5"
 
diff --git a/game/src/context.rs b/game/src/context.rs
index 10957df..a27eb26 100644
--- a/game/src/context.rs
+++ b/game/src/context.rs
@@ -3,19 +3,21 @@ use std::{cell::RefCell, sync::mpsc::Sender};
 use chrono::{DateTime, Utc};
 use discord_sdk::activity::ActivityBuilder;
 
-use crate::{utilities::non_ref_raylib::HackedRaylibHandle, GameConfig};
+use crate::{progress::ProgressData, utilities::non_ref_raylib::HackedRaylibHandle, GameConfig};
 
 #[derive(Debug)]
 pub enum ControlFlag {
     Quit,
     SwitchLevel(usize),
-    UpdateLevelStart(DateTime<Utc>)
+    UpdateLevelStart(DateTime<Utc>),
+    SaveProgress
 }
 
 #[derive(Debug)]
 pub struct GameContext {
     pub renderer: RefCell<HackedRaylibHandle>,
     pub config: GameConfig,
+    pub player_progress: ProgressData,
     pub current_level: usize,
     pub level_start_time: DateTime<Utc>,
     pub discord_rpc_send: Sender<Option<ActivityBuilder>>,
diff --git a/game/src/lib.rs b/game/src/lib.rs
index 3b4c965..f1cb87f 100644
--- a/game/src/lib.rs
+++ b/game/src/lib.rs
@@ -81,6 +81,7 @@ use utilities::discord::DiscordConfig;
 use crate::{
     context::GameContext,
     discord_rpc::{maybe_set_discord_presence, try_connect_to_local_discord},
+    progress::ProgressData,
     scenes::{build_screen_state_machine, Scenes},
     utilities::{
         game_config::FinalShaderConfig,
@@ -108,6 +109,7 @@ mod scenes;
 mod utilities;
 pub use utilities::{datastore::StaticGameData, game_config::GameConfig};
 mod character;
+mod progress;
 
 /// The game entrypoint
 pub async fn game_begin(game_config: &mut GameConfig) -> Result<(), Box<dyn std::error::Error>> {
@@ -148,6 +150,9 @@ pub async fn game_begin(game_config: &mut GameConfig) -> Result<(), Box<dyn std:
     // Build an MPSC for signaling the control thread
     let (send_control_signal, recv_control_signal) = std::sync::mpsc::channel();
 
+    // Load the savefile
+    let mut save_file = ProgressData::load_from_file();
+
     let mut context;
     let raylib_thread;
     {
@@ -172,6 +177,7 @@ pub async fn game_begin(game_config: &mut GameConfig) -> Result<(), Box<dyn std:
             renderer: RefCell::new(rl.into()),
             config: game_config.clone(),
             current_level: 0,
+            player_progress: save_file,
             level_start_time: Utc::now(),
             discord_rpc_send: send_discord_rpc,
             flag_send: send_control_signal,
@@ -320,9 +326,14 @@ pub async fn game_begin(game_config: &mut GameConfig) -> Result<(), Box<dyn std:
                         context::ControlFlag::Quit => break,
                         context::ControlFlag::SwitchLevel(level) => {
                             context.as_mut().current_level = level;
+                            context.as_mut().player_progress.save();
                         }
                         context::ControlFlag::UpdateLevelStart(time) => {
                             context.as_mut().level_start_time = time;
+                            context.as_mut().player_progress.save();
+                        }
+                        context::ControlFlag::SaveProgress => {
+                            context.as_mut().player_progress.save();
                         }
                     }
                 }
@@ -333,5 +344,6 @@ pub async fn game_begin(game_config: &mut GameConfig) -> Result<(), Box<dyn std:
             }
         }
     }
+    context.as_mut().player_progress.save();
     Ok(())
 }
diff --git a/game/src/progress.rs b/game/src/progress.rs
new file mode 100644
index 0000000..9e7f3f9
--- /dev/null
+++ b/game/src/progress.rs
@@ -0,0 +1,44 @@
+use std::collections::HashMap;
+
+use chrono::Duration;
+use tracing::info;
+
+#[derive(Debug, Deserialize, Serialize, Default)]
+pub struct ProgressData {
+    pub level_best_times: HashMap<usize, i64>,
+}
+
+impl ProgressData {
+    pub fn get_level_best_time(&self, level: usize) -> Option<Duration> {
+        let level_best_time = self.level_best_times.get(&level);
+        match level_best_time {
+            Some(time) => Some(Duration::seconds(*time)),
+            None => None,
+        }
+    }
+
+    pub fn maybe_write_new_time(&mut self, level: usize, time: &Duration) {
+        let time_in_seconds = time.num_seconds();
+        if let Some(best_time) = self.get_level_best_time(level) {
+            if best_time.num_seconds() > time_in_seconds {
+                self.level_best_times.insert(level, time_in_seconds);
+            }
+        } else {
+            self.level_best_times.insert(level, time_in_seconds);
+        }
+    }
+
+    pub fn load_from_file() -> Self {
+        info!("Loading progress data from file");
+        serde_json::from_str(
+            &std::fs::read_to_string("./savegame.json")
+                .unwrap_or("{\"level_best_times\":{}}".to_string()),
+        )
+        .unwrap_or(Self::default())
+    }
+
+    pub fn save(&self) {
+        info!("Saving progress data to file");
+        std::fs::write("./savegame.json", serde_json::to_string(self).unwrap()).unwrap()
+    }
+}
diff --git a/game/src/scenes/ingame_scene/mod.rs b/game/src/scenes/ingame_scene/mod.rs
index 798a098..3645217 100644
--- a/game/src/scenes/ingame_scene/mod.rs
+++ b/game/src/scenes/ingame_scene/mod.rs
@@ -104,7 +104,12 @@ impl Action<Scenes, ScreenError, GameContext> for InGameScreen {
         if self.current_level_idx != context.current_level {
             self.current_level_idx = context.current_level;
             self.level_switch_timestamp = Utc::now();
-            context.flag_send.send(Some(ControlFlag::UpdateLevelStart(self.level_switch_timestamp))).unwrap();
+            context
+                .flag_send
+                .send(Some(ControlFlag::UpdateLevelStart(
+                    self.level_switch_timestamp,
+                )))
+                .unwrap();
         }
 
         // Grab exclusive access to the renderer
@@ -131,6 +136,12 @@ impl Action<Scenes, ScreenError, GameContext> for InGameScreen {
         // Check if the player won
         let cur_level = self.levels.get(context.current_level).unwrap();
         if self.player.position.x > cur_level.zones.win.x {
+            // Save the progress
+            context
+                .flag_send
+                .send(Some(ControlFlag::SaveProgress))
+                .unwrap();
+
             // If this is the last level, win the game
             if self.current_level_idx >= self.levels.len() - 1 {
                 return Ok(ActionFlag::SwitchState(Scenes::WinScreen));
diff --git a/savegame.json b/savegame.json
new file mode 100644
index 0000000..cdcc103
--- /dev/null
+++ b/savegame.json
@@ -0,0 +1 @@
+{"level_best_times":{}}
\ No newline at end of file