diff --git a/CHANGELOG.md b/CHANGELOG.md index 83640d4c..b47642cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,10 @@ - Before: every time the launcher started, it put Link Play back on Same network, so players who only play over the internet had to pick Internet again each time - Now: Internet through the public relay or a friend's relay stays chosen. A direct address, a relay on this PC (off until you turn it on again) and old room codes are still cleared at start - Thanks to Sakamoto on Discord for the report +- **Set up says why it failed when the game folder has an accent in its path** (#630) + - Before: with the game in a folder such as `OneDrive\Área de Trabalho`, a failed Set up showed only "The game build failed (exit code: 1)" and setup.log had no reason, because the reason named that folder in a form the launcher could not read + - Now: the game build writes its messages as UTF-8 and the launcher reads every line of a tool's output, so the real error reaches the screen and setup.log + - Still not: this shows the error, it does not fix it. A game folder inside OneDrive can still fail while OneDrive syncs the build files; a folder outside OneDrive (such as `C:\Games`) avoids that ### Gameplay - **Camera aim keeps the block you aimed at while a move plays** (#535) diff --git a/game-build/release_b/pkp_build.py b/game-build/release_b/pkp_build.py index c63a447b..eabe3031 100644 --- a/game-build/release_b/pkp_build.py +++ b/game-build/release_b/pkp_build.py @@ -79,6 +79,12 @@ import time from concurrent.futures import ThreadPoolExecutor, FIRST_COMPLETED, wait +# The launcher reads this script's stdout and stderr as UTF-8, and lift() reads its --lift-one children's the same way. Under -I +# Python writes a pipe in the ANSI code page (cp1252 on a Portuguese Windows), so an @error naming a folder such as +# "OneDrive\Área de Trabalho" reached the launcher as bytes it could not read, and Set up failed with no reason (#630). +for _stream in (sys.stdout, sys.stderr): + if hasattr(_stream, 'reconfigure'): + _stream.reconfigure(encoding='utf-8', errors='replace') HERE = os.path.dirname(os.path.abspath(__file__)) RECOMP = os.path.join(HERE, 'recomp') SDK = os.path.join(HERE, 'sdk') diff --git a/launcher/src-tauri/examples/first_run.rs b/launcher/src-tauri/examples/first_run.rs index 3bcf3310..6430be3c 100644 --- a/launcher/src-tauri/examples/first_run.rs +++ b/launcher/src-tauri/examples/first_run.rs @@ -5,6 +5,8 @@ //! it fetches the Linux build tools first (host.rs). Prints each phase's time. // host.rs reaches the launcher's own modules, so an example carries the same set the binary declares (main.rs), by path. +#[path = "../src/child_lines.rs"] +mod child_lines; #[allow(dead_code, clippy::all)] #[path = "../src/config.rs"] mod config; diff --git a/launcher/src-tauri/examples/shader_setup.rs b/launcher/src-tauri/examples/shader_setup.rs index 44a9b479..9968923f 100644 --- a/launcher/src-tauri/examples/shader_setup.rs +++ b/launcher/src-tauri/examples/shader_setup.rs @@ -6,6 +6,8 @@ //! cargo run --example shader_setup -- --native [--gentle] // host.rs reaches the launcher's own modules, so an example carries the same set the binary declares (main.rs), by path. +#[path = "../src/child_lines.rs"] +mod child_lines; #[allow(dead_code, clippy::all)] #[path = "../src/config.rs"] mod config; diff --git a/launcher/src-tauri/src/child_lines.rs b/launcher/src-tauri/src/child_lines.rs new file mode 100644 index 00000000..6e734b93 --- /dev/null +++ b/launcher/src-tauri/src/child_lines.rs @@ -0,0 +1,40 @@ +//! A child process's output (the game build, the shader tools, the DLSS download) read line by line. +//! +//! `BufRead::lines()` ends at the first line that is not UTF-8, and `map_while(Result::ok)` then drops the rest of the stream. +//! A Windows tool writes in the ANSI code page, so a line naming a folder such as "OneDrive\Área de Trabalho" ended the +//! reader: the game build's `@error` line never arrived and Set up failed with no reason in setup.log (#630). Here such a line +//! is read lossily and the stream goes on. + +use std::io::{BufRead, BufReader, Read}; + +/// The lines `r` gives until it ends or fails, without their line ending ("\n" or "\r\n"). Bytes that are not UTF-8 read as +/// U+FFFD. +pub fn lines(r: R) -> impl Iterator { + let mut r = BufReader::new(r); + std::iter::from_fn(move || { + let mut buf = Vec::new(); + match r.read_until(b'\n', &mut buf) { + Ok(0) | Err(_) => None, + Ok(_) => { + if buf.last() == Some(&b'\n') { + buf.pop(); + if buf.last() == Some(&b'\r') { + buf.pop(); + } + } + Some(String::from_utf8_lossy(&buf).into_owned()) + } + } + }) +} + +#[cfg(test)] +mod tests { + #[test] + fn a_line_that_is_not_utf8_does_not_end_the_stream() { + // "@error C:\...\Área\r\n" as cp1252 writes it (0xC1), then a line after it + let out: &[u8] = b"@progress lift 0 4 x\r\n@error C:\\\xC1rea\r\nlast"; + let got: Vec = super::lines(out).collect(); + assert_eq!(got, ["@progress lift 0 4 x", "@error C:\\\u{FFFD}rea", "last"]); + } +} diff --git a/launcher/src-tauri/src/dlss.rs b/launcher/src-tauri/src/dlss.rs index e99a385f..7b70b57a 100644 --- a/launcher/src-tauri/src/dlss.rs +++ b/launcher/src-tauri/src/dlss.rs @@ -4,7 +4,6 @@ //! downloads the file from NVIDIA's own GitHub repository at a fixed tag and checks its sha256, into `/upscalers/streamline`. use serde::Serialize; -use std::io::{BufRead, BufReader}; use std::path::{Path, PathBuf}; use std::process::Stdio; @@ -76,9 +75,9 @@ pub fn fetch(game_dir: &Path, tools_dir: &Path, mut report: impl FnMut(Progress) } let mut child = cmd.stdout(Stdio::piped()).stderr(Stdio::piped()).spawn().map_err(|e| format!("could not start the download ({}): {}", py.display(), e))?; let err = child.stderr.take().unwrap(); - let err_tail = std::thread::spawn(move || BufReader::new(err).lines().map_while(Result::ok).last().unwrap_or_default()); + let err_tail = std::thread::spawn(move || crate::child_lines::lines(err).last().unwrap_or_default()); let mut error: Option = None; - for l in BufReader::new(child.stdout.take().unwrap()).lines().map_while(Result::ok) { + for l in crate::child_lines::lines(child.stdout.take().unwrap()) { if let Some(rest) = l.strip_prefix("@progress ") { let mut it = rest.splitn(4, ' '); let _phase = it.next(); diff --git a/launcher/src-tauri/src/gamebuild.rs b/launcher/src-tauri/src/gamebuild.rs index b0a7360b..f04df188 100644 --- a/launcher/src-tauri/src/gamebuild.rs +++ b/launcher/src-tauri/src/gamebuild.rs @@ -18,7 +18,6 @@ //! Its crate-local imports are host.rs (with download.rs) and runtime_api.rs, so `examples/first_run.rs` can build the files alone. use std::fs; -use std::io::{BufRead, BufReader}; use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; use std::sync::atomic::{AtomicBool, Ordering}; @@ -293,11 +292,11 @@ pub fn run(job: &Job, tools: &Tools, cancel: &AtomicBool, mut report: impl FnMut let out = child.stdout.take().unwrap(); let err = child.stderr.take().unwrap(); let (tx, rx) = std::sync::mpsc::channel::(); - std::thread::spawn(move || BufReader::new(out).lines().map_while(Result::ok).for_each(|l| drop(tx.send(l)))); + std::thread::spawn(move || crate::child_lines::lines(out).for_each(|l| drop(tx.send(l)))); let elog = job.log.clone(); let err_tail = std::thread::spawn(move || { let mut tail: Vec = Vec::new(); - for l in BufReader::new(err).lines().map_while(Result::ok) { + for l in crate::child_lines::lines(err) { if let Some(f) = &elog { f(&format!("stderr: {}", l)); } diff --git a/launcher/src-tauri/src/harness.rs b/launcher/src-tauri/src/harness.rs index 4e1d1333..0733bb09 100644 --- a/launcher/src-tauri/src/harness.rs +++ b/launcher/src-tauri/src/harness.rs @@ -1,7 +1,7 @@ //! Running the shader translator (tools/shaders/harness, `spirv_harness.exe`) for `shaders`: a run, a worker that runs chunk after //! chunk in one process, and why a failed run failed. use std::fs; -use std::io::{BufRead, BufReader, Write}; +use std::io::Write; use std::path::{Path, PathBuf}; use std::process::{Child, ChildStdin, Command, Stdio}; use std::sync::atomic::{AtomicBool, Ordering}; @@ -164,12 +164,12 @@ impl Worker { .map_err(|e| format!("could not start {}: {}", name, e))?; let out = child.stdout.take().unwrap(); let (tx, lines) = mpsc::channel::(); - std::thread::spawn(move || BufReader::new(out).lines().map_while(Result::ok).for_each(|l| drop(tx.send(l)))); + std::thread::spawn(move || crate::child_lines::lines(out).for_each(|l| drop(tx.send(l)))); let err = child.stderr.take().unwrap(); let (etx, stderr) = mpsc::channel::(); std::thread::spawn(move || { let mut tail: Vec = Vec::new(); - for l in BufReader::new(err).lines().map_while(Result::ok) { + for l in crate::child_lines::lines(err) { tail.push(l); if tail.len() > 12 { tail.remove(0); diff --git a/launcher/src-tauri/src/main.rs b/launcher/src-tauri/src/main.rs index 3805b522..48f7f3e5 100644 --- a/launcher/src-tauri/src/main.rs +++ b/launcher/src-tauri/src/main.rs @@ -4,6 +4,7 @@ mod backup_cmds; mod branding; mod branding_cmds; +mod child_lines; mod cli; mod config; mod dialogs; diff --git a/launcher/src-tauri/src/shaders.rs b/launcher/src-tauri/src/shaders.rs index 4096c3af..f021b84b 100644 --- a/launcher/src-tauri/src/shaders.rs +++ b/launcher/src-tauri/src/shaders.rs @@ -22,7 +22,7 @@ use serde::{Deserialize, Serialize}; use std::collections::HashSet; use std::fs; -use std::io::{BufRead, BufReader, Write}; +use std::io::Write; use std::path::{Path, PathBuf}; use std::process::{Child, Command, Stdio}; use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering}; @@ -455,13 +455,13 @@ pub(crate) fn run_tool(mut cmd: Command, cancel: &AtomicBool, idle: bool, log: O let (tx, rx) = mpsc::channel::(); let out = child.stdout.take().unwrap(); let tx2 = tx.clone(); - std::thread::spawn(move || BufReader::new(out).lines().map_while(Result::ok).for_each(|l| drop(tx2.send(l)))); + std::thread::spawn(move || crate::child_lines::lines(out).for_each(|l| drop(tx2.send(l)))); let err = child.stderr.take().unwrap(); let (etx, erx) = mpsc::channel::(); let elog = log.cloned(); std::thread::spawn(move || { let mut tail: Vec = Vec::new(); - for l in BufReader::new(err).lines().map_while(Result::ok) { + for l in crate::child_lines::lines(err) { if let Some(f) = &elog { f(&format!("stderr: {}", l)); }