Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,430 changes: 889 additions & 541 deletions launcher/pnpm-lock.yaml

Large diffs are not rendered by default.

26 changes: 25 additions & 1 deletion launcher/src-tauri/src/commands.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
use crate::settings::LauncherSettings;
use crate::storage;

use serde::{Deserialize, Serialize};
use std::collections::VecDeque;
use std::process::Stdio;
Expand Down Expand Up @@ -250,7 +253,7 @@ pub async fn launch_game(
debug_enabled: Option<bool>,
) -> Result<String, String> {
let exe = find_client_binary()?;
let assets = crate::downloader::assets_dir();
let assets = storage::assets_dir();

let account = uuid.as_deref().and_then(crate::auth::try_restore);

Expand Down Expand Up @@ -411,3 +414,24 @@ fn find_client_binary() -> Result<std::path::PathBuf, String> {

Err("POMC client not found. It will be bundled in future releases.".into())
}

#[tauri::command]
pub async fn load_launcher_settings() -> LauncherSettings {
let settings = LauncherSettings::get().await;
settings.clone()
}

#[tauri::command]
pub async fn set_launcher_language(language: String) -> Result<(), String> {
LauncherSettings::update(|s| s.language = language).await
}

#[tauri::command]
pub async fn set_keep_launcher_open(keep: bool) -> Result<(), String> {
LauncherSettings::update(|s| s.keep_launcher_open = keep).await
}

#[tauri::command]
pub async fn set_launch_with_console(launch: bool) -> Result<(), String> {
LauncherSettings::update(|s| s.launch_with_console = launch).await
}
48 changes: 14 additions & 34 deletions launcher/src-tauri/src/downloader.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use crate::storage;

use serde::Deserialize;
use std::path::{Path, PathBuf};
use std::path::Path;
use tauri::{AppHandle, Emitter};

const VERSION_MANIFEST_URL: &str =
Expand Down Expand Up @@ -59,41 +61,20 @@ pub struct DownloadProgress {
pub status: String,
}

fn data_dir() -> PathBuf {
directories::ProjectDirs::from("", "", ".pomc")
.map(|d| d.data_dir().to_path_buf())
.unwrap_or_else(|| PathBuf::from(".pomc"))
}

pub fn assets_dir() -> PathBuf {
data_dir().join("assets")
}

fn indexes_dir() -> PathBuf {
assets_dir().join("indexes")
}

fn objects_dir() -> PathBuf {
assets_dir().join("objects")
}

fn versions_dir() -> PathBuf {
data_dir().join("versions")
}

pub fn needs_download(version: &str) -> bool {
let no_index = !indexes_dir().join(format!("{version}.json")).exists();
let no_jar = !assets_dir().join("jar").join(".extracted").exists();
let no_index = !storage::indexes_dir()
.join(format!("{version}.json"))
.exists();
let no_jar = !storage::assets_dir()
.join("jar")
.join(".extracted")
.exists();
no_index || no_jar
}

pub async fn download(app: &AppHandle, version: &str) -> Result<(), String> {
let _ = std::fs::create_dir_all(indexes_dir());
let _ = std::fs::create_dir_all(objects_dir());
let _ = std::fs::create_dir_all(versions_dir());

let client = reqwest::Client::new();
let index_path = indexes_dir().join(format!("{version}.json"));
let index_path = storage::indexes_dir().join(format!("{version}.json"));

let (asset_index, version_json) = if index_path.exists() {
let content = std::fs::read_to_string(&index_path).map_err(|e| e.to_string())?;
Expand Down Expand Up @@ -154,7 +135,7 @@ async fn download_objects(
let total = index.objects.len() as u32;
let mut downloaded = 0u32;
let mut skipped = 0u32;
let objects = objects_dir();
let objects = storage::objects_dir();

for (name, obj) in &index.objects {
let prefix = &obj.hash[..2];
Expand Down Expand Up @@ -202,7 +183,7 @@ async fn download_jar(
version: &str,
cached_vj: Option<&VersionJson>,
) -> Result<(), String> {
let jar_assets = assets_dir().join("jar");
let jar_assets = storage::assets_dir().join("jar");
let marker = jar_assets.join(".extracted");
if marker.exists() {
return Ok(());
Expand Down Expand Up @@ -238,7 +219,7 @@ async fn download_jar(
&fetched
};

let jar_path = versions_dir().join(format!("{version}.jar"));
let jar_path = storage::versions_dir().join(format!("{version}.jar"));
if !jar_path.exists() || std::fs::metadata(&jar_path).map(|m| m.len()).unwrap_or(0) != dl.size {
emit_progress(app, 0, 1, "Downloading client JAR...");
let bytes = client
Expand All @@ -255,7 +236,6 @@ async fn download_jar(
log::warn!("JAR hash mismatch: expected {}, got {actual_hash}", dl.sha1);
}

let _ = std::fs::create_dir_all(versions_dir());
std::fs::write(&jar_path, &bytes).map_err(|e| e.to_string())?;
}

Expand Down
10 changes: 9 additions & 1 deletion launcher/src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ use tokio::sync::Mutex;
mod auth;
mod commands;
mod downloader;
mod settings;
mod storage;

use std::collections::VecDeque;

#[derive(Default)]
Expand All @@ -21,6 +24,7 @@ fn main() {

tauri::Builder::default()
.setup(|app| {
storage::ensure_dirs();
app.manage(Mutex::new(AppState::default()));
Ok(())
})
Expand All @@ -37,7 +41,11 @@ fn main() {
commands::get_patch_notes,
commands::get_patch_content,
commands::launch_game,
commands::get_client_logs
commands::get_client_logs,
commands::load_launcher_settings,
commands::set_launcher_language,
commands::set_keep_launcher_open,
commands::set_launch_with_console,
])
.run(tauri::generate_context!())
.expect("failed to run POMC launcher");
Expand Down
75 changes: 75 additions & 0 deletions launcher/src-tauri/src/settings.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
use crate::storage;
use serde::{Deserialize, Serialize};
use std::sync::LazyLock;
use tokio::sync::{RwLock, RwLockReadGuard};

static LAUNCHER_SETTINGS: LazyLock<RwLock<LauncherSettings>> =
LazyLock::new(|| RwLock::new(LauncherSettings::load()));

#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct LauncherSettings {
pub language: String,
pub keep_launcher_open: bool,
pub launch_with_console: bool,
}

impl Default for LauncherSettings {
fn default() -> Self {
LauncherSettings {
language: "English".into(),
keep_launcher_open: true,
launch_with_console: false,
}
}
}

impl LauncherSettings {
async fn save(&self) -> Result<(), String> {
let path = storage::settings_file();
let json = serde_json::to_string_pretty(self).map_err(|e| e.to_string())?;
tokio::fs::write(path, json)
.await
.map_err(|e| e.to_string())?;
Ok(())
}

fn load() -> Self {
let path = storage::settings_file();

match std::fs::read_to_string(&path) {
Ok(content) => match serde_json::from_str::<LauncherSettings>(&content) {
Ok(cfg) => return cfg,
Err(err) => {
log::warn!("Settings file invalid ({}), using defaults", err);
}
},
Err(_) => {
log::info!("Settings file not found, creating default settings");
}
}

let default = LauncherSettings::default();
if let Ok(json) = serde_json::to_string_pretty(&default) {
let _ = std::fs::write(&path, json);
}
default
}

pub async fn get() -> RwLockReadGuard<'static, LauncherSettings> {
LAUNCHER_SETTINGS.read().await
}

pub async fn update<F>(f: F) -> Result<(), String>
where
F: FnOnce(&mut LauncherSettings),
{
let cloned = {
let mut settings = LAUNCHER_SETTINGS.write().await;
f(&mut settings);
settings.clone()
};

cloned.save().await
}
}
60 changes: 60 additions & 0 deletions launcher/src-tauri/src/storage.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
use std::path::{Path, PathBuf};
use std::sync::LazyLock;

static DATA_DIR: LazyLock<PathBuf> = {
LazyLock::new(|| {
directories::ProjectDirs::from("", "", ".pomc")
.map(|d| d.data_dir().to_path_buf())
.unwrap_or_else(|| PathBuf::from(".pomc"))
})
};

pub fn data_dir() -> &'static Path {
&DATA_DIR
}

fn ensure_file(path: &Path, default: &str) {
if !path.exists() {
let _ = std::fs::write(path, default);
}
}

pub fn ensure_dirs() {
let _ = std::fs::create_dir_all(assets_dir());
let _ = std::fs::create_dir_all(versions_dir());
let _ = std::fs::create_dir_all(installations_dir());

let _ = std::fs::create_dir_all(indexes_dir());
let _ = std::fs::create_dir_all(objects_dir());

ensure_file(&settings_file(), "{}");
ensure_file(&accounts_file(), "[]");
ensure_file(&installations_file(), "[]");
}

pub fn assets_dir() -> PathBuf {
data_dir().join("assets")
}
pub fn versions_dir() -> PathBuf {
data_dir().join("versions")
}
pub fn installations_dir() -> PathBuf {
data_dir().join("installations")
}

pub fn indexes_dir() -> PathBuf {
assets_dir().join("indexes")
}
pub fn objects_dir() -> PathBuf {
assets_dir().join("objects")
}

pub fn settings_file() -> PathBuf {
data_dir().join("settings.json")
}
pub fn accounts_file() -> PathBuf {
data_dir().join("accounts.json")
}
pub fn installations_file() -> PathBuf {
data_dir().join("installations.json")
}
7 changes: 3 additions & 4 deletions launcher/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
setActiveIndex,
setAccountDropdownOpen,
server,

setInstallations,
editingInstall,
setEditingInstall,
Expand All @@ -42,7 +41,7 @@
setSkinUrl,
setSelectedNote,
username,
useConsole,
launcherSettings,
} = useAppStateContext();

const openPatchNote = useCallback(async (note: PatchNote) => {
Expand All @@ -55,13 +54,13 @@
} catch (e) {
console.error("Failed to fetch content:", e);
}
}, []);

Check warning on line 57 in launcher/src/App.tsx

View workflow job for this annotation

GitHub Actions / launcher-frontend

React Hook useCallback has missing dependencies: 'setPage' and 'setSelectedNote'. Either include them or remove the dependency array

const loadSkin = useCallback((uuid: string) => {
invoke<string>("get_skin_url", { uuid })
.then(setSkinUrl)
.catch(() => setSkinUrl(null));
}, []);

Check warning on line 63 in launcher/src/App.tsx

View workflow job for this annotation

GitHub Actions / launcher-frontend

React Hook useCallback has a missing dependency: 'setSkinUrl'. Either include it or remove the dependency array

useEffect(() => {
invoke<AuthAccount[]>("get_all_accounts").then((accs) => {
Expand All @@ -77,7 +76,7 @@
invoke<GameVersion[]>("get_versions", { showSnapshots: false })
.then(setVersions)
.catch((e) => console.error("Failed to fetch versions:", e));
}, [loadSkin]);

Check warning on line 79 in launcher/src/App.tsx

View workflow job for this annotation

GitHub Actions / launcher-frontend

React Hook useEffect has missing dependencies: 'setAccounts', 'setActiveIndex', 'setNews', and 'setVersions'. Either include them or remove the dependency array

const startAddAccount = useCallback(async () => {
setAccountDropdownOpen(false);
Expand All @@ -96,7 +95,7 @@
setStatus(`Auth failed: ${e}`);
}
setAuthLoading(false);
}, [accounts, loadSkin]);

Check warning on line 98 in launcher/src/App.tsx

View workflow job for this annotation

GitHub Actions / launcher-frontend

React Hook useCallback has missing dependencies: 'setAccountDropdownOpen', 'setAccounts', 'setActiveIndex', 'setAuthLoading', and 'setStatus'. Either include them or remove the dependency array

const switchAccount = useCallback(
(index: number) => {
Expand All @@ -106,7 +105,7 @@
loadSkin(accounts[index].uuid);
}
},
[accounts, loadSkin],

Check warning on line 108 in launcher/src/App.tsx

View workflow job for this annotation

GitHub Actions / launcher-frontend

React Hook useCallback has missing dependencies: 'setAccountDropdownOpen' and 'setActiveIndex'. Either include them or remove the dependency array
);

const removeAccount = useCallback((uuid: string) => {
Expand All @@ -115,7 +114,7 @@
setActiveIndex(0);
setAccountDropdownOpen(false);
setSkinUrl(null);
}, []);

Check warning on line 117 in launcher/src/App.tsx

View workflow job for this annotation

GitHub Actions / launcher-frontend

React Hook useCallback has missing dependencies: 'setAccountDropdownOpen', 'setAccounts', 'setActiveIndex', and 'setSkinUrl'. Either include them or remove the dependency array

const handleLaunch = useCallback(async () => {
setLaunching(true);
Expand All @@ -126,7 +125,7 @@
const result = await invoke<string>("launch_game", {
uuid: account?.uuid || null,
server: server || null,
debugEnabled: useConsole || null,
debugEnabled: launcherSettings.launchWithConsole || null,
});
setStatus(result);
} catch (e) {
Expand All @@ -136,7 +135,7 @@
setLaunching(false);
setStatus("");
}, 3000);
}, [username, server, selectedVersion, useConsole]);
}, [username, server, selectedVersion, launcherSettings.launchWithConsole]);

Check warning on line 138 in launcher/src/App.tsx

View workflow job for this annotation

GitHub Actions / launcher-frontend

React Hook useCallback has missing dependencies: 'account?.uuid', 'setLaunching', and 'setStatus'. Either include them or remove the dependency array

return (
<div className="app">
Expand Down
Loading
Loading