Track playtime and display it in the launcher (#336)

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Tyler Wilding <xTVaser@users.noreply.github.com>
Co-authored-by: OpenGOALBot <OpenGOALBot@users.noreply.github.com>
Co-authored-by: Tyler Wilding <xtvaser@gmail.com>
This commit is contained in:
NeoFoxxo 2023-11-06 05:18:00 +00:00 committed by GitHub
parent 52e10be247
commit 41e3581826
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 169 additions and 4 deletions

View file

@ -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::<tokio::sync::Mutex<LauncherConfig>>();
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(())
}

View file

@ -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<LauncherConfig>>,
game_name: String,
) -> Result<u64, CommandError> {
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
))),
}
}

View file

@ -116,6 +116,7 @@ pub struct GameConfig {
pub version: Option<String>,
pub version_folder: Option<String>,
pub features: Option<GameFeatureConfig>,
pub seconds_played: Option<u64>,
}
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<u64, ConfigError> {
let game_config = self.get_supported_game_config_mut(&game_name)?;
Ok(game_config.seconds_played.unwrap_or(0))
}
}

View file

@ -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<tauri::AppHandle> = 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,

View file

@ -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",

View file

@ -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<string>("playtimeUpdated", (event) => {
getPlaytime(getInternalName(activeGame)).then((result) => {
playtime = formatPlaytime(result);
});
});
</script>
<div class="flex flex-col justify-end items-end mt-auto">
<!-- TOOO - time played -->
<h1
class="tracking-tighter text-2xl font-bold pb-3 text-orange-500 text-outline pointer-events-none"
>
{$_(`gameName_${getInternalName(activeGame)}`)}
</h1>
{#if playtime}
<h1 class="pb-4 text-xl text-outline tracking-tighter font-extrabold">
{`${$_(`gameControls_timePlayed_label`)} ${playtime}`}
</h1>
{/if}
<div class="flex flex-row gap-2">
<Button
class="border-solid border-2 border-slate-900 rounded bg-slate-900 hover:bg-slate-800 text-sm text-white font-semibold px-5 py-2"

View file

@ -256,3 +256,7 @@ export async function doesActiveToolingVersionSupportGame(
() => false,
);
}
export async function getPlaytime(gameName: string): Promise<number> {
return await invoke_rpc("get_playtime", { gameName: gameName }, () => 0);
}