From d8a06bcee6670c111cef9d22afe8e17e6cd0b678 Mon Sep 17 00:00:00 2001 From: emil28092005 Date: Sun, 6 Sep 2026 00:45:29 +0300 Subject: [PATCH] feat: persist launcher memory settings --- README.md | 1 + src-tauri/src/lib.rs | 34 +++++++++++++- src-tauri/src/settings.rs | 99 +++++++++++++++++++++++++++++++++++++++ src/main.tsx | 17 ++++++- 4 files changed, 149 insertions(+), 2 deletions(-) create mode 100644 src-tauri/src/settings.rs diff --git a/README.md b/README.md index ca47608..3f12eb6 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ - интерактивный интерфейс профилей и настроек; - Tauri 2 native shell с безопасной командой `native_host`; +- сохранение памяти профиля в локальном каталоге данных приложения; - адаптивное окно для Windows, Linux и macOS; - базовая структура, в которую будут добавлены манифесты, проверка файлов, Java 21 и запуск Minecraft. diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 2eaf8a7..62b0817 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,5 +1,6 @@ mod manifest; mod profile; +mod settings; use serde::Serialize; use tauri::{AppHandle, Manager}; @@ -68,9 +69,40 @@ async fn sync_profile(app: AppHandle, manifest_json: String) -> Result Result { + let data_dir = app + .path() + .app_data_dir() + .map_err(|error| format!("Cannot resolve launcher data directory: {error}"))?; + tauri::async_runtime::spawn_blocking(move || settings::load(&data_dir)) + .await + .map_err(|error| format!("Settings task failed: {error}"))? + .map_err(|error| error.to_string()) +} + +#[tauri::command] +async fn save_settings(app: AppHandle, settings: settings::LauncherSettings) -> Result { + let data_dir = app + .path() + .app_data_dir() + .map_err(|error| format!("Cannot resolve launcher data directory: {error}"))?; + tauri::async_runtime::spawn_blocking(move || settings::save(&data_dir, settings)) + .await + .map_err(|error| format!("Settings task failed: {error}"))? + .map_err(|error| error.to_string()) +} + pub fn run() { tauri::Builder::default() - .invoke_handler(tauri::generate_handler![native_host, validate_manifest, inspect_profile, sync_profile]) + .invoke_handler(tauri::generate_handler![ + native_host, + validate_manifest, + inspect_profile, + sync_profile, + load_settings, + save_settings + ]) .run(tauri::generate_context!()) .expect("error while running ShaCraft Launcher"); } diff --git a/src-tauri/src/settings.rs b/src-tauri/src/settings.rs new file mode 100644 index 0000000..e943d7a --- /dev/null +++ b/src-tauri/src/settings.rs @@ -0,0 +1,99 @@ +use serde::{Deserialize, Serialize}; +use std::{fmt, fs, io, path::Path}; + +const SETTINGS_FILE: &str = "settings.json"; +const MIN_MEMORY_MB: u16 = 3 * 1024; +const MAX_MEMORY_MB: u16 = 12 * 1024; +const DEFAULT_MEMORY_MB: u16 = 6 * 1024; + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct LauncherSettings { + pub memory_mb: u16, +} + +impl Default for LauncherSettings { + fn default() -> Self { + Self { memory_mb: DEFAULT_MEMORY_MB } + } +} + +#[derive(Debug)] +pub enum SettingsError { + Io(io::Error), + InvalidJson(serde_json::Error), + InvalidMemory, +} + +impl fmt::Display for SettingsError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Io(error) => write!(formatter, "Cannot access launcher settings: {error}"), + Self::InvalidJson(error) => write!(formatter, "Cannot read launcher settings: {error}"), + Self::InvalidMemory => write!(formatter, "Memory allocation must be between 3 and 12 GiB"), + } + } +} + +pub fn load(data_dir: &Path) -> Result { + let path = data_dir.join(SETTINGS_FILE); + let source = match fs::read_to_string(path) { + Ok(source) => source, + Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(LauncherSettings::default()), + Err(error) => return Err(SettingsError::Io(error)), + }; + let settings = serde_json::from_str(&source).map_err(SettingsError::InvalidJson)?; + validate(&settings)?; + Ok(settings) +} + +pub fn save(data_dir: &Path, settings: LauncherSettings) -> Result { + validate(&settings)?; + fs::create_dir_all(data_dir).map_err(SettingsError::Io)?; + + let target = data_dir.join(SETTINGS_FILE); + let temporary = data_dir.join(".settings.json.shacraft.part"); + let contents = serde_json::to_vec_pretty(&settings).expect("LauncherSettings is serializable"); + fs::write(&temporary, contents).map_err(SettingsError::Io)?; + fs::rename(temporary, target).map_err(SettingsError::Io)?; + Ok(settings) +} + +fn validate(settings: &LauncherSettings) -> Result<(), SettingsError> { + if !(MIN_MEMORY_MB..=MAX_MEMORY_MB).contains(&settings.memory_mb) || settings.memory_mb % 1024 != 0 { + return Err(SettingsError::InvalidMemory); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::{load, save, LauncherSettings}; + use std::{fs, process, time::{SystemTime, UNIX_EPOCH}}; + + fn temporary_directory() -> std::path::PathBuf { + std::env::temp_dir().join(format!( + "shacraft-settings-test-{}-{}", + process::id(), + SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos() + )) + } + + #[test] + fn defaults_then_persists_memory() { + let directory = temporary_directory(); + assert_eq!(load(&directory).unwrap().memory_mb, 6 * 1024); + + let saved = save(&directory, LauncherSettings { memory_mb: 8 * 1024 }).unwrap(); + assert_eq!(saved.memory_mb, 8 * 1024); + assert_eq!(load(&directory).unwrap().memory_mb, 8 * 1024); + + fs::remove_dir_all(directory).unwrap(); + } + + #[test] + fn rejects_unsafe_memory_values() { + let directory = temporary_directory(); + assert!(save(&directory, LauncherSettings { memory_mb: 512 }).is_err()); + } +} diff --git a/src/main.tsx b/src/main.tsx index 892cf34..1529162 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -41,6 +41,10 @@ type NativeHost = { launcherVersion: string } +type NativeSettings = { + memoryMb: number +} + const servers: Server[] = [ { id: 'aoc', @@ -89,8 +93,19 @@ function App() { useEffect(() => { if (!('__TAURI_INTERNALS__' in window)) return invoke('native_host').then(setNativeHost).catch(() => setNativeHost(null)) + invoke('load_settings') + .then((settings) => setRam(settings.memoryMb / 1024)) + .catch(() => undefined) }, []) + const updateRam = (memoryGb: number) => { + setRam(memoryGb) + if ('__TAURI_INTERNALS__' in window) { + invoke('save_settings', { settings: { memoryMb: memoryGb * 1024 } }) + .catch(() => undefined) + } + } + const repair = () => { if (selected.disabled) return setReady(false) @@ -230,7 +245,7 @@ function App() {