diff --git a/src-tauri/src/commands/binaries.rs b/src-tauri/src/commands/binaries.rs index b06c12ff..153dd7a3 100644 --- a/src-tauri/src/commands/binaries.rs +++ b/src-tauri/src/commands/binaries.rs @@ -4,16 +4,19 @@ use std::{ collections::HashMap, path::{Path, PathBuf}, process::Command, + time::Instant, }; use log::{info, warn}; use semver::Version; use serde::{Deserialize, Serialize}; use serde_json::Value; +use tauri::Manager; use crate::{ config::LauncherConfig, util::file::{create_dir, overwrite_dir, read_last_lines_from_file}, + TAURI_APP, }; use super::CommandError; @@ -731,7 +734,7 @@ pub async fn launch_game( let config_info = common_prelude(&config_lock)?; let exec_info = get_exec_location(&config_info, "gk")?; - let args = generate_launch_game_string(&config_info, game_name, in_debug)?; + let args = generate_launch_game_string(&config_info, game_name.clone(), in_debug)?; log::info!( "Launching game version {:?} -> {:?} with args: {:?}", @@ -740,8 +743,9 @@ pub async fn launch_game( args ); - // TODO - log rotation here would be nice too, and for it to be game specific let log_file = create_log_file(&app_handle, "game.log", false)?; + + // TODO - log rotation here would be nice too, and for it to be game specific let mut command = Command::new(exec_info.executable_path); command .args(args) @@ -752,6 +756,54 @@ pub async fn launch_game( { command.creation_flags(0x08000000); } - command.spawn()?; + // Start the process here so if there is an error, we can return immediately + let mut child = command.spawn()?; + // if all goes well, we await the child to exit in the background (separate thread) + tokio::spawn(async move { + let start_time = Instant::now(); // get the start time of the game + // start waiting for the game to exit + if let Err(err) = child.wait() { + log::error!("Error occured when waiting for game to exit: {}", err); + return; + } + // once the game exits pass the time the game started to the track_playtine function + if let Err(err) = track_playtime(start_time, game_name).await { + log::error!("Error occured when tracking playtime: {}", err); + return; + } + }); + Ok(()) +} + +async fn track_playtime( + start_time: std::time::Instant, + game_name: String, +) -> Result<(), CommandError> { + let app_handle = TAURI_APP + .get() + .ok_or_else(|| { + CommandError::BinaryExecution("Cannot access global app state to persist playtime".to_owned()) + })? + .app_handle(); + let config = app_handle.state::>(); + let mut config_lock = config.lock().await; + + // get the playtime of the session + let elapsed_time = start_time.elapsed().as_secs(); + log::info!("elapsed time: {}", elapsed_time); + + config_lock + .update_game_seconds_played(&game_name, elapsed_time) + .map_err(|_| CommandError::Configuration("Unable to persist time played".to_owned()))?; + + // send an event to the front end so that it can refresh the playtime on screen + if let Err(err) = app_handle.emit_all("playtimeUpdated", ()) { + log::error!("Failed to emit playtimeUpdated event: {}", err); + return Err(CommandError::BinaryExecution(format!( + "Failed to emit playtimeUpdated event: {}", + err + ))); + } + Ok(()) } diff --git a/src-tauri/src/commands/config.rs b/src-tauri/src/commands/config.rs index 9ff74861..31f3eb76 100644 --- a/src-tauri/src/commands/config.rs +++ b/src-tauri/src/commands/config.rs @@ -414,3 +414,18 @@ pub async fn does_active_tooling_version_support_game( _ => Ok(false), } } + +#[tauri::command] +pub async fn get_playtime( + config: tauri::State<'_, tokio::sync::Mutex>, + game_name: String, +) -> Result { + let mut config_lock = config.lock().await; + match config_lock.get_game_seconds_played(&game_name) { + Ok(playtime) => Ok(playtime), + Err(err) => Err(CommandError::Configuration(format!( + "Error occurred when getting game playtime: {}", + err + ))), + } +} diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs index bf26d364..a58b8e21 100644 --- a/src-tauri/src/config.rs +++ b/src-tauri/src/config.rs @@ -116,6 +116,7 @@ pub struct GameConfig { pub version: Option, pub version_folder: Option, pub features: Option, + pub seconds_played: Option, } impl GameConfig { @@ -125,6 +126,7 @@ impl GameConfig { version: None, version_folder: None, features: Some(GameFeatureConfig::default()), + seconds_played: Some(0), } } } @@ -559,4 +561,27 @@ impl LauncherConfig { self.save_config()?; Ok(()) } + + pub fn update_game_seconds_played( + &mut self, + game_name: &String, + additional_seconds: u64, + ) -> Result<(), ConfigError> { + let game_config = self.get_supported_game_config_mut(game_name)?; + match game_config.seconds_played { + Some(seconds) => { + game_config.seconds_played = Some(seconds + additional_seconds); + } + None => { + game_config.seconds_played = Some(additional_seconds); + } + } + self.save_config()?; + Ok(()) + } + + pub fn get_game_seconds_played(&mut self, game_name: &String) -> Result { + let game_config = self.get_supported_game_config_mut(&game_name)?; + Ok(game_config.seconds_played.unwrap_or(0)) + } } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index a0976205..96c7d384 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -6,6 +6,7 @@ use directories::UserDirs; use fern::colors::{Color, ColoredLevelConfig}; use tauri::{Manager, RunEvent}; +use tokio::sync::OnceCell; use util::file::create_dir; use backtrace::Backtrace; @@ -44,6 +45,8 @@ fn panic_hook(info: &std::panic::PanicInfo) { log_crash(Some(info), None); } +static TAURI_APP: OnceCell = OnceCell::const_new(); + fn main() { // In the event that some catastrophic happens, atleast log it out // the panic_hook will log to a file in the folder of the executable @@ -51,6 +54,8 @@ fn main() { let tauri_setup = tauri::Builder::default() .setup(|app| { + TAURI_APP.set(app.app_handle()); + // Setup Logging let log_path = app .path_resolver() @@ -141,6 +146,7 @@ fn main() { commands::config::cleanup_enabled_texture_packs, commands::config::delete_old_data_directory, commands::config::does_active_tooling_version_support_game, + commands::config::get_playtime, commands::config::finalize_installation, commands::config::get_active_tooling_version_folder, commands::config::get_active_tooling_version, diff --git a/src/assets/translations/en-US.json b/src/assets/translations/en-US.json index 8e46e8a6..e5ef9f84 100644 --- a/src/assets/translations/en-US.json +++ b/src/assets/translations/en-US.json @@ -36,6 +36,11 @@ "gameControls_noToolingSet_button_setVersion": "Set Version", "gameControls_noToolingSet_header": "No Tooling Version Configured!", "gameControls_noToolingSet_subheader": "Head over to the following settings page to download the latest release", + "gameControls_timePlayed_label": "Played For", + "gameControls_timePlayed_hour": "hour", + "gameControls_timePlayed_hours": "hours", + "gameControls_timePlayed_minute": "minute", + "gameControls_timePlayed_minutes": "minutes", "gameJob_applyTexturePacks": "Applying Packs", "gameJob_deleteTexturePacks": "Deleting Packs", "gameJob_enablingTexturePacks": "Enabling Packs", diff --git a/src/components/games/GameControls.svelte b/src/components/games/GameControls.svelte index b95af7a6..f788209a 100644 --- a/src/components/games/GameControls.svelte +++ b/src/components/games/GameControls.svelte @@ -16,8 +16,10 @@ import { resetGameSettings, uninstallGame } from "$lib/rpc/game"; import { platform } from "@tauri-apps/api/os"; import { getLaunchGameString, launchGame, openREPL } from "$lib/rpc/binaries"; + import { getPlaytime } from "$lib/rpc/config"; import { _ } from "svelte-i18n"; import { navigate } from "svelte-navigator"; + import { listen } from "@tauri-apps/api/event"; import { toastStore } from "$lib/stores/ToastStore"; export let activeGame: SupportedGame; @@ -26,6 +28,7 @@ let settingsDir = undefined; let savesDir = undefined; let isLinux = false; + let playtime = ""; onMount(async () => { isLinux = (await platform()) === "linux"; @@ -42,15 +45,70 @@ "saves", ); }); + + // format the time from the settings file which is stored as seconds + function formatPlaytime(playtimeRaw: number) { + // calculate the number of hours and minutes + const hours = Math.floor(playtimeRaw / 3600); + const minutes = Math.floor((playtimeRaw % 3600) / 60); + + // initialize the formatted playtime string + let formattedPlaytime = ""; + + // add the hours to the formatted playtime string + if (hours > 0) { + if (hours > 1) { + formattedPlaytime += `${hours} ${$_(`gameControls_timePlayed_hours`)}`; + } else { + formattedPlaytime += `${hours} ${$_(`gameControls_timePlayed_hour`)}`; + } + } + + // add the minutes to the formatted playtime string + if (minutes > 0) { + // add a comma if there are already hours in the formatted playtime string + if (formattedPlaytime.length > 0) { + formattedPlaytime += ", "; + } + if (minutes > 1) { + formattedPlaytime += `${minutes} ${$_( + `gameControls_timePlayed_minutes`, + )}`; + } else { + formattedPlaytime += `${minutes} ${$_( + `gameControls_timePlayed_minute`, + )}`; + } + } + + // return the formatted playtime string + return formattedPlaytime; + } + + // get the playtime from the backend, format it, and assign it to the playtime variable when the page first loads + getPlaytime(getInternalName(activeGame)).then((result) => { + playtime = formatPlaytime(result); + }); + + // listen for the custom playtiemUpdated event from the backend and then refresh the playtime on screen + listen("playtimeUpdated", (event) => { + getPlaytime(getInternalName(activeGame)).then((result) => { + playtime = formatPlaytime(result); + }); + });
-

{$_(`gameName_${getInternalName(activeGame)}`)}

+ {#if playtime} +

+ {`${$_(`gameControls_timePlayed_label`)} ${playtime}`} +

+ {/if}