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
This commit is contained in:
@@ -42,10 +42,16 @@ struct LanSpreadState {
|
||||
peer_game_db: Arc<RwLock<PeerGameDB>>,
|
||||
catalog: Arc<RwLock<HashSet<String>>>,
|
||||
unpack_logs: Arc<RwLock<Vec<UnpackLogEntry>>>,
|
||||
pending_install_account_names: Arc<RwLock<HashMap<String, String>>>,
|
||||
pending_install_settings: Arc<RwLock<HashMap<String, InstallSettings>>>,
|
||||
state_dir: OnceLock<PathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct InstallSettings {
|
||||
account_name: String,
|
||||
language: String,
|
||||
}
|
||||
|
||||
struct PeerEventTx(UnboundedSender<PeerEvent>);
|
||||
|
||||
#[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<bool> {
|
||||
@@ -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<bool> {
|
||||
@@ -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<bool> {
|
||||
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::<LanSpreadState>();
|
||||
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::<LanSpreadState>();
|
||||
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(
|
||||
|
||||
@@ -53,6 +53,7 @@ export const useGameActions = (
|
||||
try {
|
||||
const success = await invoke<boolean>('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<boolean>('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 {
|
||||
|
||||
Reference in New Issue
Block a user