From 9bafd981d7ffd8b7b45b967a405e23dcf4d0a143 Mon Sep 17 00:00:00 2001 From: ddidderr Date: Thu, 21 May 2026 22:24:59 +0200 Subject: [PATCH] feat(install): write launcher language marker files Some games include a language.txt marker in the unpacked local tree, similar in spirit to account_name.txt. Installs and updates now carry the launcher language alongside the account name so those game-provided marker files are rewritten before staged files are promoted into local/. The Tauri command boundary keeps the UI setting vocabulary as de/en, then maps it to the file vocabulary expected by games: german or english. Unknown values continue through the existing DEFAULT_LANGUAGE path, so the marker file falls back to english just like script launch arguments fall back to en. The transaction layer deliberately reuses the same first-match traversal helper for both marker files. The searches stay independent, so games may place account_name.txt and language.txt in different directories if their archive layout requires that. Test Plan: - just fmt - just test - just frontend-test - just clippy - deno task build - git diff --check Refs: none --- crates/lanspread-peer-cli/src/main.rs | 2 + crates/lanspread-peer/ARCHITECTURE.md | 3 + crates/lanspread-peer/src/handlers.rs | 29 ++++--- .../lanspread-peer/src/install/transaction.rs | 82 ++++++++++++++----- crates/lanspread-peer/src/lib.rs | 15 +++- .../src-tauri/src/lib.rs | 81 +++++++++++++----- .../src/hooks/useGameActions.ts | 6 +- 7 files changed, 162 insertions(+), 56 deletions(-) diff --git a/crates/lanspread-peer-cli/src/main.rs b/crates/lanspread-peer-cli/src/main.rs index 411b863..e966947 100644 --- a/crates/lanspread-peer-cli/src/main.rs +++ b/crates/lanspread-peer-cli/src/main.rs @@ -241,6 +241,7 @@ async fn handle_command( file_descriptions: files, install_after_download: *install_after_download, account_name: None, + language: None, })?; Ok(json!({"queued": true, "game_id": game_id, "install": install_after_download})) } @@ -250,6 +251,7 @@ async fn handle_command( sender.send(PeerCommand::InstallGame { id: game_id.clone(), account_name: None, + language: None, })?; Ok(json!({"queued": true, "game_id": game_id})) } diff --git a/crates/lanspread-peer/ARCHITECTURE.md b/crates/lanspread-peer/ARCHITECTURE.md index e7e8dda..0f8c080 100644 --- a/crates/lanspread-peer/ARCHITECTURE.md +++ b/crates/lanspread-peer/ARCHITECTURE.md @@ -116,6 +116,9 @@ Downloaded and installed are independent predicates: - `installed` is true when `/local/` is a directory. The contents of `local/` are user-owned and are skipped by manifests, fingerprints, and file serving. +- Install and update transactions unpack into staging, then overwrite the first + discovered game-provided `account_name.txt` and `language.txt` files under + the staged tree from launcher settings before promoting it to `local/`. Reserved per-game paths: diff --git a/crates/lanspread-peer/src/handlers.rs b/crates/lanspread-peer/src/handlers.rs index 699c7e8..59e6e0f 100644 --- a/crates/lanspread-peer/src/handlers.rs +++ b/crates/lanspread-peer/src/handlers.rs @@ -200,6 +200,7 @@ pub async fn handle_download_game_files_command( file_descriptions: Vec, install_after_download: bool, account_name: Option, + language: Option, ) { log::info!("Got PeerCommand::DownloadGameFiles"); if !catalog_contains(ctx, &id).await { @@ -277,7 +278,7 @@ pub async fn handle_download_game_files_command( log::error!("Failed to send DownloadGameFilesFinished event: {e}"); } if install_after_download { - spawn_install_operation(ctx, tx_notify_ui, id.clone(), account_name); + spawn_install_operation(ctx, tx_notify_ui, id.clone(), account_name, language); } } else { log::error!("No trusted peers available after majority validation for game {id}"); @@ -364,6 +365,7 @@ pub async fn handle_download_game_files_command( download_id, prepared, account_name, + language, ) .await; } else { @@ -422,8 +424,9 @@ pub async fn handle_install_game_command( tx_notify_ui: &UnboundedSender, id: String, account_name: Option, + language: Option, ) { - spawn_install_operation(ctx, tx_notify_ui, id, account_name); + spawn_install_operation(ctx, tx_notify_ui, id, account_name, language); } /// Handles the `UninstallGame` command. @@ -471,11 +474,12 @@ fn spawn_install_operation( tx_notify_ui: &UnboundedSender, id: String, account_name: Option, + language: Option, ) { let ctx = ctx.clone(); let tx_notify_ui = tx_notify_ui.clone(); ctx.task_tracker.clone().spawn(async move { - run_install_operation(&ctx, &tx_notify_ui, id, account_name).await; + run_install_operation(&ctx, &tx_notify_ui, id, account_name, language).await; }); } @@ -484,6 +488,7 @@ async fn run_install_operation( tx_notify_ui: &UnboundedSender, id: String, account_name: Option, + language: Option, ) { let Some(prepared) = prepare_install_operation(ctx, tx_notify_ui, &id).await else { return; @@ -494,7 +499,7 @@ async fn run_install_operation( return; } - run_started_install_operation(ctx, tx_notify_ui, id, prepared, account_name).await; + run_started_install_operation(ctx, tx_notify_ui, id, prepared, account_name, language).await; } struct PreparedInstallOperation { @@ -547,6 +552,7 @@ async fn run_started_install_operation( id: String, prepared: PreparedInstallOperation, account_name: Option, + language: Option, ) { let PreparedInstallOperation { game_root, @@ -577,6 +583,7 @@ async fn run_started_install_operation( &id, ctx.unpacker.clone(), account_name.as_deref(), + language.as_deref(), ) .await } @@ -587,6 +594,7 @@ async fn run_started_install_operation( &id, ctx.unpacker.clone(), account_name.as_deref(), + language.as_deref(), ) .await } @@ -1503,7 +1511,7 @@ mod tests { update_and_announce_games(&ctx, &tx, scan).await; assert_local_update(recv_event(&mut rx).await, true, true); - run_install_operation(&ctx, &tx, "game".to_string(), None).await; + run_install_operation(&ctx, &tx, "game".to_string(), None, None).await; assert_active_update( recv_event(&mut rx).await, @@ -1534,7 +1542,7 @@ mod tests { let ctx = test_ctx(temp.path().to_path_buf()); let (tx, mut rx) = mpsc::unbounded_channel(); - run_install_operation(&ctx, &tx, "game".to_string(), None).await; + run_install_operation(&ctx, &tx, "game".to_string(), None, None).await; assert_active_update( recv_event(&mut rx).await, @@ -1588,7 +1596,8 @@ mod tests { .await ); clear_active_download(&ctx, "game").await; - run_started_install_operation(&ctx, &tx, "game".to_string(), prepared, None).await; + run_started_install_operation(&ctx, &tx, "game".to_string(), prepared, None, None) + .await; } }); @@ -1655,7 +1664,7 @@ mod tests { let ctx = test_ctx(temp.path().to_path_buf()); let (tx, mut rx) = mpsc::unbounded_channel(); - run_install_operation(&ctx, &tx, "game".to_string(), None).await; + run_install_operation(&ctx, &tx, "game".to_string(), None, None).await; assert_active_update( recv_event(&mut rx).await, @@ -1687,7 +1696,7 @@ mod tests { let ctx = test_ctx(temp.path().to_path_buf()); let (tx, mut rx) = mpsc::unbounded_channel(); - run_install_operation(&ctx, &tx, "game".to_string(), None).await; + run_install_operation(&ctx, &tx, "game".to_string(), None, None).await; assert_active_update( recv_event(&mut rx).await, active_update("game", ActiveOperationKind::Installing), @@ -1710,7 +1719,7 @@ mod tests { write_file(&root.join("version.ini"), b"20250101"); write_file(&root.join("game.eti"), b"new archive"); - run_install_operation(&ctx, &tx, "game".to_string(), None).await; + run_install_operation(&ctx, &tx, "game".to_string(), None, None).await; assert_active_update( recv_event(&mut rx).await, active_update("game", ActiveOperationKind::Updating), diff --git a/crates/lanspread-peer/src/install/transaction.rs b/crates/lanspread-peer/src/install/transaction.rs index 0b33596..57a8643 100644 --- a/crates/lanspread-peer/src/install/transaction.rs +++ b/crates/lanspread-peer/src/install/transaction.rs @@ -21,6 +21,7 @@ const OWNED_MARKER: &str = ".lanspread_owned"; const VERSION_TMP_FILE: &str = ".version.ini.tmp"; const VERSION_DISCARDED_FILE: &str = ".version.ini.discarded"; const ACCOUNT_NAME_FILE: &str = "account_name.txt"; +const LANGUAGE_FILE: &str = "language.txt"; #[derive(Clone, Copy, Debug, Eq, PartialEq)] enum FsEntryState { @@ -41,6 +42,7 @@ pub async fn install( id: &str, unpacker: Arc, account_name: Option<&str>, + language: Option<&str>, ) -> eyre::Result<()> { let eti_version = read_downloaded_version(game_root).await; write_intent( @@ -50,7 +52,7 @@ pub async fn install( ) .await?; - let result = install_inner(game_root, id, unpacker, account_name).await; + let result = install_inner(game_root, id, unpacker, account_name, language).await; match result { Ok(()) => { write_intent(state_dir, id, &InstallIntent::none(id, eti_version)).await?; @@ -75,6 +77,7 @@ pub async fn update( id: &str, unpacker: Arc, account_name: Option<&str>, + language: Option<&str>, ) -> eyre::Result<()> { let eti_version = read_downloaded_version(game_root).await; write_intent( @@ -84,7 +87,7 @@ pub async fn update( ) .await?; - let result = update_inner(game_root, id, unpacker, account_name).await; + let result = update_inner(game_root, id, unpacker, account_name, language).await; match result { Ok(()) => { write_intent(state_dir, id, &InstallIntent::none(id, eti_version)).await?; @@ -193,6 +196,7 @@ async fn install_inner( id: &str, unpacker: Arc, account_name: Option<&str>, + language: Option<&str>, ) -> eyre::Result<()> { let local = local_dir(game_root); if path_is_dir(&local).await { @@ -202,7 +206,8 @@ async fn install_inner( let staging = installing_dir(game_root); prepare_owned_empty_dir(&staging).await?; unpack_archives(game_root, &staging, unpacker).await?; - write_account_name_if_present(&staging, account_name).await?; + write_install_setting_if_present(&staging, ACCOUNT_NAME_FILE, account_name).await?; + write_install_setting_if_present(&staging, LANGUAGE_FILE, language).await?; tokio::fs::rename(&staging, &local) .await .wrap_err_with(|| format!("failed to promote install for {id}"))?; @@ -214,6 +219,7 @@ async fn update_inner( id: &str, unpacker: Arc, account_name: Option<&str>, + language: Option<&str>, ) -> eyre::Result<()> { let local = local_dir(game_root); let backup = backup_dir(game_root); @@ -230,7 +236,8 @@ async fn update_inner( prepare_owned_empty_dir(&staging).await?; unpack_archives(game_root, &staging, unpacker).await?; - write_account_name_if_present(&staging, account_name).await?; + write_install_setting_if_present(&staging, ACCOUNT_NAME_FILE, account_name).await?; + write_install_setting_if_present(&staging, LANGUAGE_FILE, language).await?; tokio::fs::rename(&staging, &local) .await .wrap_err_with(|| format!("failed to promote update for {id}"))?; @@ -284,25 +291,26 @@ async fn root_eti_archives(game_root: &Path) -> eyre::Result> { Ok(archives) } -async fn write_account_name_if_present( +async fn write_install_setting_if_present( install_root: &Path, - account_name: Option<&str>, + file_name: &str, + value: Option<&str>, ) -> eyre::Result<()> { - let Some(account_name) = account_name else { + let Some(value) = value else { return Ok(()); }; - let Some(path) = find_account_name_file(install_root).await? else { + let Some(path) = find_install_setting_file(install_root, file_name).await? else { return Ok(()); }; - tokio::fs::write(&path, account_name) + tokio::fs::write(&path, value) .await .wrap_err_with(|| format!("failed to write {}", path.display()))?; Ok(()) } -async fn find_account_name_file(root: &Path) -> eyre::Result> { +async fn find_install_setting_file(root: &Path, file_name: &str) -> eyre::Result> { let mut pending_dirs = vec![root.to_path_buf()]; while let Some(dir) = pending_dirs.pop() { let mut entries = tokio::fs::read_dir(&dir).await?; @@ -310,7 +318,7 @@ async fn find_account_name_file(root: &Path) -> eyre::Result> { while let Some(entry) = entries.next_entry().await? { let file_type = entry.file_type().await?; let path = entry.path(); - if entry.file_name() == OsStr::new(ACCOUNT_NAME_FILE) && file_type.is_file() { + if entry.file_name() == OsStr::new(file_name) && file_type.is_file() { return Ok(Some(path)); } if file_type.is_dir() { @@ -630,9 +638,16 @@ mod tests { write_file(&root.join("game.eti"), b"archive"); write_file(&root.join("version.ini"), b"20250101"); - install(&root, state.path(), "game", successful_unpacker(), None) - .await - .expect("install should succeed"); + install( + &root, + state.path(), + "game", + successful_unpacker(), + None, + None, + ) + .await + .expect("install should succeed"); assert!(root.join("local").join("payload.txt").is_file()); assert!(!root.join(".local.installing").exists()); @@ -654,6 +669,7 @@ mod tests { "game", successful_unpacker(), Some("Alice"), + None, ) .await .expect("install should succeed without account file"); @@ -672,7 +688,7 @@ mod tests { write_file(&root.join("version.ini"), b"20250101"); let unpacker = Arc::new(FakeUnpacker::default()); - install(&root, state.path(), "game", unpacker.clone(), None) + install(&root, state.path(), "game", unpacker.clone(), None, None) .await .expect("install should succeed"); @@ -687,16 +703,18 @@ mod tests { } #[tokio::test] - async fn install_overwrites_first_account_name_file() { - struct AccountNameUnpacker; + async fn install_overwrites_first_install_setting_files() { + struct InstallSettingsUnpacker; - impl Unpacker for AccountNameUnpacker { + impl Unpacker for InstallSettingsUnpacker { fn unpack<'a>(&'a self, _archive: &'a Path, dest: &'a Path) -> UnpackFuture<'a> { Box::pin(async move { tokio::fs::create_dir_all(dest.join("a")).await?; tokio::fs::create_dir_all(dest.join("z")).await?; tokio::fs::write(dest.join("a").join(ACCOUNT_NAME_FILE), b"old-a").await?; tokio::fs::write(dest.join("z").join(ACCOUNT_NAME_FILE), b"old-z").await?; + tokio::fs::write(dest.join("a").join(LANGUAGE_FILE), b"old-a").await?; + tokio::fs::write(dest.join("z").join(LANGUAGE_FILE), b"old-z").await?; Ok(()) }) } @@ -712,8 +730,9 @@ mod tests { &root, state.path(), "game", - Arc::new(AccountNameUnpacker), + Arc::new(InstallSettingsUnpacker), Some("Alice"), + Some("german"), ) .await .expect("install should succeed"); @@ -728,6 +747,16 @@ mod tests { .expect("second account file should be readable"), "old-z" ); + assert_eq!( + std::fs::read_to_string(root.join("local").join("a").join(LANGUAGE_FILE)) + .expect("first language file should be readable"), + "german" + ); + assert_eq!( + std::fs::read_to_string(root.join("local").join("z").join(LANGUAGE_FILE)) + .expect("second language file should be readable"), + "old-z" + ); } #[tokio::test] @@ -745,6 +774,7 @@ mod tests { "game", Arc::new(FakeUnpacker::failing()), None, + None, ) .await .expect_err("update should fail"); @@ -772,6 +802,7 @@ mod tests { "game", Arc::new(FakeUnpacker::commit_conflict()), None, + None, ) .await .expect_err("update should fail at commit rename"); @@ -801,9 +832,16 @@ mod tests { write_file(&root.join("version.ini"), b"20250101"); write_file(&root.join("local").join("old.txt"), b"old"); - update(&root, state.path(), "game", successful_unpacker(), None) - .await - .expect("update should succeed"); + update( + &root, + state.path(), + "game", + successful_unpacker(), + None, + None, + ) + .await + .expect("update should succeed"); assert!(root.join("local").join("payload.txt").is_file()); assert!(!root.join("local").join("old.txt").exists()); diff --git a/crates/lanspread-peer/src/lib.rs b/crates/lanspread-peer/src/lib.rs index edebf9b..8f19bbd 100644 --- a/crates/lanspread-peer/src/lib.rs +++ b/crates/lanspread-peer/src/lib.rs @@ -230,6 +230,7 @@ pub enum PeerCommand { id: String, file_descriptions: Vec, account_name: Option, + language: Option, }, /// Download game files with an explicit install policy. DownloadGameFilesWithOptions { @@ -237,11 +238,13 @@ pub enum PeerCommand { file_descriptions: Vec, install_after_download: bool, account_name: Option, + language: Option, }, /// Install already-downloaded archives into `local/`. InstallGame { id: String, account_name: Option, + language: Option, }, /// Remove only the `local/` install for a game. UninstallGame { id: String }, @@ -411,6 +414,7 @@ async fn handle_peer_commands( id, file_descriptions, account_name, + language, } => { handle_download_game_files_command( ctx, @@ -419,6 +423,7 @@ async fn handle_peer_commands( file_descriptions, true, account_name, + language, ) .await; } @@ -427,6 +432,7 @@ async fn handle_peer_commands( file_descriptions, install_after_download, account_name, + language, } => { handle_download_game_files_command( ctx, @@ -435,11 +441,16 @@ async fn handle_peer_commands( file_descriptions, install_after_download, account_name, + language, ) .await; } - PeerCommand::InstallGame { id, account_name } => { - handle_install_game_command(ctx, tx_notify_ui, id, account_name).await; + PeerCommand::InstallGame { + id, + account_name, + language, + } => { + handle_install_game_command(ctx, tx_notify_ui, id, account_name, language).await; } PeerCommand::UninstallGame { id } => { handle_uninstall_game_command(ctx, tx_notify_ui, id).await; diff --git a/crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs b/crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs index d398994..48c0194 100644 --- a/crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs +++ b/crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs @@ -42,10 +42,16 @@ struct LanSpreadState { peer_game_db: Arc>, catalog: Arc>>, unpack_logs: Arc>>, - pending_install_account_names: Arc>>, + pending_install_settings: Arc>>, state_dir: OnceLock, } +#[derive(Clone, Debug, PartialEq, Eq)] +struct InstallSettings { + account_name: String, + language: String, +} + struct PeerEventTx(UnboundedSender); #[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)] @@ -145,6 +151,7 @@ async fn request_games(state: tauri::State<'_, LanSpreadState>) -> tauri::Result #[tauri::command] async fn install_game( id: String, + language: String, username: String, state: tauri::State<'_, LanSpreadState>, ) -> tauri::Result { @@ -173,20 +180,21 @@ async fn install_game( return Ok(false); }; - let account_name = sanitize_username(&username); + let install_settings = install_settings(&language, &username); let handled = if let Some(peer_ctrl) = peer_ctrl { let command = if !downloaded { state .inner() - .pending_install_account_names + .pending_install_settings .write() .await - .insert(id.clone(), account_name); + .insert(id.clone(), install_settings); PeerCommand::GetGame(id.clone()) } else if !installed { PeerCommand::InstallGame { id: id.clone(), - account_name: Some(account_name), + account_name: Some(install_settings.account_name), + language: Some(install_settings.language), } } else { log::info!("Game is already installed: {id}"); @@ -197,7 +205,7 @@ async fn install_game( log::error!("Failed to send message to peer: {e:?}"); state .inner() - .pending_install_account_names + .pending_install_settings .write() .await .remove(&id); @@ -215,6 +223,7 @@ async fn install_game( #[tauri::command] async fn update_game( id: String, + language: String, username: String, state: tauri::State<'_, LanSpreadState>, ) -> tauri::Result { @@ -236,15 +245,15 @@ async fn update_game( if let Some(peer_ctrl) = peer_ctrl { state .inner() - .pending_install_account_names + .pending_install_settings .write() .await - .insert(id.clone(), sanitize_username(&username)); + .insert(id.clone(), install_settings(&language, &username)); if let Err(e) = peer_ctrl.send(PeerCommand::FetchLatestFromPeers { id: id.clone() }) { log::error!("Failed to send message to peer: {e:?}"); state .inner() - .pending_install_account_names + .pending_install_settings .write() .await .remove(&id); @@ -342,7 +351,7 @@ async fn cancel_download( ) -> tauri::Result { state .inner() - .pending_install_account_names + .pending_install_settings .write() .await .remove(&id); @@ -459,6 +468,20 @@ fn launch_settings(language: &str, username: &str) -> LaunchSettings { } } +fn install_settings(language: &str, username: &str) -> InstallSettings { + InstallSettings { + account_name: sanitize_username(username), + language: install_language(language), + } +} + +fn install_language(language: &str) -> String { + match sanitize_language(language).as_str() { + "de" => "german".to_string(), + _ => "english".to_string(), + } +} + #[cfg_attr(not(target_os = "windows"), allow(dead_code))] fn sanitize_language(language: &str) -> String { match language.trim().to_ascii_lowercase().as_str() { @@ -1481,7 +1504,7 @@ async fn handle_peer_event(app_handle: &AppHandle, event: PeerEvent) { } PeerEvent::NoPeersHaveGame { id } => { log::warn!("PeerEvent::NoPeersHaveGame received for {id}"); - clear_pending_install_account_name(app_handle, &id).await; + clear_pending_install_settings(app_handle, &id).await; emit_game_id_event( app_handle, "game-no-peers", @@ -1520,7 +1543,7 @@ async fn handle_peer_event(app_handle: &AppHandle, event: PeerEvent) { } PeerEvent::DownloadGameFilesFailed { id } => { log::warn!("PeerEvent::DownloadGameFilesFailed received"); - clear_pending_install_account_name(app_handle, &id).await; + clear_pending_install_settings(app_handle, &id).await; emit_game_id_event( app_handle, "game-download-failed", @@ -1530,7 +1553,7 @@ async fn handle_peer_event(app_handle: &AppHandle, event: PeerEvent) { } PeerEvent::DownloadGameFilesAllPeersGone { id } => { log::warn!("PeerEvent::DownloadGameFilesAllPeersGone received for {id}"); - clear_pending_install_account_name(app_handle, &id).await; + clear_pending_install_settings(app_handle, &id).await; emit_game_id_event( app_handle, "game-download-peers-gone", @@ -1655,9 +1678,9 @@ async fn handle_peer_event(app_handle: &AppHandle, event: PeerEvent) { } } -async fn clear_pending_install_account_name(app_handle: &AppHandle, id: &str) { +async fn clear_pending_install_settings(app_handle: &AppHandle, id: &str) { let state = app_handle.state::(); - state.pending_install_account_names.write().await.remove(id); + state.pending_install_settings.write().await.remove(id); } async fn handle_got_game_files( @@ -1674,17 +1697,17 @@ async fn handle_got_game_files( ); let state = app_handle.state::(); - let account_name = state - .pending_install_account_names - .write() - .await - .remove(&id); + let install_settings = state.pending_install_settings.write().await.remove(&id); + let (account_name, language) = install_settings.map_or((None, None), |settings| { + (Some(settings.account_name), Some(settings.language)) + }); let peer_ctrl = state.peer_ctrl.read().await.clone(); if let Some(peer_ctrl) = peer_ctrl && let Err(e) = peer_ctrl.send(PeerCommand::DownloadGameFiles { id, file_descriptions, account_name, + language, }) { log::error!("Failed to send PeerCommand::DownloadGameFiles: {e}"); @@ -1921,6 +1944,24 @@ mod tests { ); } + #[test] + fn install_settings_use_language_file_values() { + assert_eq!( + install_settings("de", " Alice \"Ace\"%PATH%\n "), + InstallSettings { + account_name: "Alice AcePATH".to_string(), + language: "german".to_string(), + } + ); + assert_eq!( + install_settings("fr", ""), + InstallSettings { + account_name: DEFAULT_USERNAME.to_string(), + language: "english".to_string(), + } + ); + } + #[test] fn script_params_use_common_argument_shape() { let start_params = script_params( diff --git a/crates/lanspread-tauri-deno-ts/src/hooks/useGameActions.ts b/crates/lanspread-tauri-deno-ts/src/hooks/useGameActions.ts index 51fcdfd..5c7fe54 100644 --- a/crates/lanspread-tauri-deno-ts/src/hooks/useGameActions.ts +++ b/crates/lanspread-tauri-deno-ts/src/hooks/useGameActions.ts @@ -53,6 +53,7 @@ export const useGameActions = ( try { const success = await invoke('install_game', { id, + language: settings.language, username: settings.username, }); if (!success) return; @@ -64,19 +65,20 @@ export const useGameActions = ( } catch (err) { console.error('install_game failed:', err); } - }, [games, settings.username]); + }, [games, settings.language, settings.username]); const update = useCallback(async (id: string) => { try { const success = await invoke('update_game', { id, + language: settings.language, username: settings.username, }); if (success) games.markChecking(id); } catch (err) { console.error('update_game failed:', err); } - }, [games, settings.username]); + }, [games, settings.language, settings.username]); const uninstall = useCallback(async (id: string) => { try {