Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion launcher-rust/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,26 @@ fn main() {
};
std::process::exit(runner::run(run))
}
Route::Prepare { args } => {
let run = RunContext {
context: &context,
args,
out: &mut std::io::stdout(),
};
std::process::exit(runner::prepare(run))
}
Route::Play => {
// As the Python `main`: the start-up steps and the settings first.
startup::apply_to_process(&context);
let paths = &context.paths;
startup::prepare(paths);
let settings = load_app_settings(paths);
let mut settings = load_app_settings(paths);
if let Some(update) = startup::update_dir_arg(&args) {
settings.set(
"update_dir",
serde_json::Value::String(update.to_string_lossy().into_owned()),
);
}
// Without a usable game folder there is nothing to play yet: the window opens instead.
if Installation::read(
&settings.get_str("game_dir"),
Expand Down
42 changes: 42 additions & 0 deletions launcher-rust/src/runner/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,19 @@ pub fn run(context: RunContext<'_>) -> i32 {
run_with(context, &SystemHost)
}

/// `--prepare --game <folder>` (RecompLauncher's setup step): the image preparation and the content
/// profile of that folder into the data folder's `out/`, as a start does them, then stop: no probe,
/// no window and no game. Prints `progress <0 to 1> <text>` lines, and returns 0 when it worked, 1
/// with the reason printed when it did not.
pub fn prepare(context: RunContext<'_>) -> i32 {
let RunContext {
context: program,
args,
out,
} = context;
Runner::new(out, program.env.clone(), &SystemHost).prepare_only(&program.paths, &args)
}

/// `SPARTAN_RESTART_COMMAND`, what the in-game "Apply and restart" starts (`run.py`, from
/// `sys.executable`): on Windows `<exe> --run --after <pid> <args>` quoted as
/// `subprocess.list2cmdline` does (the probe starts it and ends, so the new launch waits for this
Expand Down Expand Up @@ -636,6 +649,35 @@ impl<'a> Runner<'a> {
code
}

/// [`prepare`]: steps 3, 8 and 9 of a start for `--game <folder>`, without the update and mods.
fn prepare_only(&mut self, paths: &Paths, args: &[OsString]) -> i32 {
let [flag, game] = args else {
return self.fail("--prepare needs --game <folder>");
};
if flag != "--game" {
return self.fail("--prepare needs --game <folder>");
}
let game = PathBuf::from(game);
let plan = RunPlan::read(paths, &self.env);
self.port.clone_from(&plan.port);
if let Err(error) = fs::create_dir_all(&plan.out) {
return self.fail(&format!("{}: {error}", python_path(&plan.out)));
}
if !game.join("eboot.bin").is_file() {
return self.fail(&format!("No eboot.bin in {}.", python_path(&game)));
}
self.say("progress 0.1 Reading the game");
if let Err(code) = self.prepare_image(&game, &plan.out) {
return code;
}
self.say("progress 0.8 Reading the game's content");
if let Err(code) = self.content_profile(&plan, &game) {
return code;
}
self.say("progress 1 Ready");
0
}

/// `--after <pid>` at the front of `args`: waits for that process (at most 30 s) and returns
/// the other arguments.
fn after(&mut self, mut args: Vec<OsString>) -> Result<Vec<OsString>, i32> {
Expand Down
15 changes: 14 additions & 1 deletion launcher-rust/src/startup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ pub enum Route {
Run { args: Vec<OsString> },
/// `--play`: the Rust runner's play role.
Play,
/// `--prepare`: the game image of a folder, without the window or the game, for RecompLauncher's
/// setup step; everything after it is the runner's (`--game <folder>`).
Prepare { args: Vec<OsString> },
/// `--install-update TARGET PID`: the downloaded version installs itself over the old one.
/// A `pid` that is not a number is 0.
InstallUpdate { target: PathBuf, pid: u32 },
Expand Down Expand Up @@ -54,6 +57,7 @@ pub fn route_on<S: AsRef<OsStr>>(args: &[S], windows: bool) -> Route {
};
match args.first() {
Some(first) if is(first, "--run") => return Route::Run { args: rest() },
Some(first) if is(first, "--prepare") => return Route::Prepare { args: rest() },
Some(first) if is(first, "--voice-swap") => return Route::VoiceSwap { args: rest() },
Some(first) if is(first, "--install-update") && windows && args.len() == 3 => {
return Route::InstallUpdate {
Expand All @@ -76,6 +80,15 @@ pub fn route_on<S: AsRef<OsStr>>(args: &[S], windows: bool) -> Route {
Route::Launcher
}

/// `--update-dir <folder>` of a `--play` start (RecompLauncher puts the update the player picked in
/// one): the folder, when it holds an `eboot.bin`. A folder without one (the pick was skipped) or no
/// such argument is `None`, and the saved update folder stays.
pub fn update_dir_arg(args: &[OsString]) -> Option<PathBuf> {
let at = args.iter().position(|arg| arg == "--update-dir")?;
let dir = std::path::absolute(args.get(at + 1)?).ok()?;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
dir.join("eboot.bin").is_file().then_some(dir)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

/// Python `install_update`: waits for the old launcher (`pid`) to close, copies this version
/// (`source`) over `target` and starts it. Returns the exit code.
pub fn install_update(source: &Path, target: &Path, pid: u32, lang: &str) -> i32 {
Expand Down Expand Up @@ -139,7 +152,7 @@ pub fn start(
})
};
let mut paths = read(&env, None);
if matches!(route, Route::Run { .. }) {
if matches!(route, Route::Run { .. } | Route::Prepare { .. }) {
if let Ok(resolved) = mods::resolve(&paths.port_dir) {
if resolved != paths.port_dir {
paths = read(&env, Some(&resolved));
Expand Down
57 changes: 57 additions & 0 deletions launcher-rust/tests/runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -635,6 +635,63 @@ fn a_relative_controller_file_is_the_ports() {
);
}

/// `LAUNCHER --prepare <args>` in `port`, with the probe left as the empty file it is.
fn prepare_in(port: &Port, args: &[&std::ffi::OsStr]) -> Ran {
let output = Command::new(LAUNCHER)
.arg("--prepare")
.args(args)
.env_clear()
.envs(port.environment(&[]))
.stdin(Stdio::null())
.output()
.unwrap();
Ran {
code: output.status.code(),
stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
}
}

#[test]
fn prepare_makes_the_image_and_the_profile_and_starts_nothing() {
let port = Port::new();
let ran = prepare_in(&port, &["--game".as_ref(), port.game().as_os_str()]);
assert_eq!(ran.code, Some(0), "{ran}");
in_order(
&ran.stdout,
&[
"progress 0.1 ",
"imported symbols",
"progress 0.8 ",
"AppContent profile",
"progress 1 ",
],
);
for file in ["boot-linked.bin", "content.bin"] {
assert!(port.out().join(file).is_file(), "{file}: {ran}");
}
}

#[test]
fn prepare_names_a_folder_without_eboot_and_a_missing_game() {
let port = Port::new();
let empty = port.root.join("not a game");
fs::create_dir_all(&empty).unwrap();
let ran = prepare_in(&port, &["--game".as_ref(), empty.as_os_str()]);
assert_eq!(ran.code, Some(1), "{ran}");
assert!(
ran.stdout
.contains(&format!("No eboot.bin in {}.", empty.display())),
"{ran}"
);
let ran = prepare_in(&port, &[]);
assert_eq!(ran.code, Some(1), "{ran}");
assert!(
ran.stdout.contains("--prepare needs --game <folder>"),
"{ran}"
);
}

#[test]
fn a_missing_eboot_fails_with_the_python_message() {
let port = Port::new();
Expand Down
34 changes: 34 additions & 0 deletions launcher-rust/tests/startup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,40 @@ fn run_carries_everything_after_it() {
);
}

#[test]
fn update_dir_is_used_only_when_it_holds_an_eboot() {
use spartanport_launcher::startup::update_dir_arg;
let args = |list: &[&std::ffi::OsStr]| list.iter().map(OsString::from).collect::<Vec<_>>();
let dir = tempfile::tempdir().unwrap();
let given = args(&[
"--play".as_ref(),
"--update-dir".as_ref(),
dir.path().as_os_str(),
]);
assert_eq!(
update_dir_arg(&given),
None,
"a skipped pick has no eboot.bin"
);
std::fs::write(dir.path().join("eboot.bin"), b"x").unwrap();
assert_eq!(update_dir_arg(&given), Some(dir.path().to_path_buf()));
assert_eq!(update_dir_arg(&args(&["--play".as_ref()])), None);
assert_eq!(update_dir_arg(&args(&["--update-dir".as_ref()])), None);
}

#[test]
fn prepare_carries_everything_after_it() {
let args = |list: &[&str]| list.iter().map(OsString::from).collect::<Vec<_>>();
assert_eq!(
route(&["--prepare", "--game", "C:/dump"]),
Route::Prepare {
args: args(&["--game", "C:/dump"])
}
);
// Only as the first argument, like `--run`: elsewhere it is ignored.
assert_eq!(route(&["--game", "x", "--prepare"]), Route::Launcher);
}

#[test]
fn run_and_play_are_their_own_routes() {
// `--play` counts anywhere, as in the Python `main`.
Expand Down
2 changes: 2 additions & 0 deletions packaging/linux/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ install -m644 launcher/OFL-BodoniModa.txt "$share/launcher/OFL-BodoniModa.txt"
stray=$(find "$appdir" \( -name '*.py' -o -name _internal \) -print)
[[ -z $stray ]] || { echo 'Python files in the AppDir:' >&2; echo "$stray" >&2; exit 1; }
install -m755 packaging/linux/AppRun "$appdir/AppRun"
# RecompLauncher reads this from the AppImage to set the game up and start it (no game files in it).
install -m644 recomplauncher.json "$appdir/recomplauncher.json"
install -m644 packaging/linux/spartanport.desktop "$appdir/spartanport.desktop"
install -m644 "$icon" "$appdir/spartanport.png"

Expand Down
2 changes: 2 additions & 0 deletions packaging/windows/package.sh
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ cp -r scripts/controller_diagrams "$dest/scripts/"
cp -r patches "$dest/"
cp docs/VOICE_REPLACEMENT.md "$dest/docs/"
cp LICENSE README.md packaging/windows/README-Windows.txt "$dest/"
# RecompLauncher reads this to set the game up and start it (no game files in it).
cp recomplauncher.json "$dest/"
# The upscalers' third-party files and their licenses (credits in README.md).
mkdir -p "$dest/LICENSES"
cp -r out/fsr4_shaders "$dest/fsr4_shaders"
Expand Down
Loading
Loading