mirror of
https://github.com/open-goal/launcher.git
synced 2024-10-19 14:47:36 -04:00
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:
parent
52e10be247
commit
41e3581826
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue