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:
2026-05-21 22:24:59 +02:00
parent e06a887da1
commit 9bafd981d7
7 changed files with 162 additions and 56 deletions
+19 -10
View File
@@ -200,6 +200,7 @@ pub async fn handle_download_game_files_command(
file_descriptions: Vec<GameFileDescription>,
install_after_download: bool,
account_name: Option<String>,
language: Option<String>,
) {
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<PeerEvent>,
id: String,
account_name: Option<String>,
language: Option<String>,
) {
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<PeerEvent>,
id: String,
account_name: Option<String>,
language: Option<String>,
) {
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<PeerEvent>,
id: String,
account_name: Option<String>,
language: Option<String>,
) {
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<String>,
language: Option<String>,
) {
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),