feat: persist launcher memory settings

This commit is contained in:
emil28092005
2026-09-06 00:45:29 +03:00
parent dbc38b6017
commit d8a06bcee6
4 changed files with 149 additions and 2 deletions
+1
View File
@@ -6,6 +6,7 @@
- интерактивный интерфейс профилей и настроек;
- Tauri 2 native shell с безопасной командой `native_host`;
- сохранение памяти профиля в локальном каталоге данных приложения;
- адаптивное окно для Windows, Linux и macOS;
- базовая структура, в которую будут добавлены манифесты, проверка файлов,
Java 21 и запуск Minecraft.
+33 -1
View File
@@ -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<profile::
.map_err(|error| error.to_string())
}
#[tauri::command]
async fn load_settings(app: AppHandle) -> Result<settings::LauncherSettings, String> {
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<settings::LauncherSettings, String> {
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");
}
+99
View File
@@ -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<LauncherSettings, SettingsError> {
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<LauncherSettings, SettingsError> {
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());
}
}
+16 -1
View File
@@ -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<NativeHost>('native_host').then(setNativeHost).catch(() => setNativeHost(null))
invoke<NativeSettings>('load_settings')
.then((settings) => setRam(settings.memoryMb / 1024))
.catch(() => undefined)
}, [])
const updateRam = (memoryGb: number) => {
setRam(memoryGb)
if ('__TAURI_INTERNALS__' in window) {
invoke<NativeSettings>('save_settings', { settings: { memoryMb: memoryGb * 1024 } })
.catch(() => undefined)
}
}
const repair = () => {
if (selected.disabled) return
setReady(false)
@@ -230,7 +245,7 @@ function App() {
</div>
<label className="range-setting">
<span><strong>Оперативная память</strong><b>{ram} ГБ</b></span>
<input type="range" min="3" max="12" value={ram} onChange={(e) => setRam(Number(e.target.value))} />
<input type="range" min="3" max="12" value={ram} onChange={(e) => updateRam(Number(e.target.value))} />
<small>Для Aeronautics рекомендуется 6 ГБ</small>
</label>
<button className="setting-row"><span><FolderOpen />Папка игры</span><ChevronRight /></button>