M .builds/build-for-windows.yml => .builds/build-for-windows.yml +3 -1
@@ 6,7 6,7 @@ packages:
sources:
- https://git.sr.ht/~quf/no-cutscene-kiseki
artifacts:
- - no-cutscene-kiseki/target/x86_64-pc-windows-gnu/release/launch.exe
+ - no-cutscene-kiseki.exe
tasks:
- install-install-toolchain: |
rustup install stable
@@ 15,3 15,5 @@ tasks:
- build: |
cd no-cutscene-kiseki
cargo build --release --target x86_64-pc-windows-gnu
+ - rename-exe: |
+ mv no-cutscene-kiseki/target/x86_64-pc-windows-gnu/release/no-cutscene-kiseki.exe .
M Cargo.toml => Cargo.toml +1 -8
@@ 3,13 3,6 @@ name = "no-cutscene-kiseki"
version = "0.0.3"
edition = "2021"
-[lib]
-name = "common"
-path = "src/lib.rs"
-
-[[bin]]
-name = "launch"
-
[profile.release]
# runtime performance is basically irrelevant, so we optimize for binary size as far as possible.
opt-level = "z"
@@ 24,4 17,4 @@ clap = { version = "4.1", default-features = false, features = ["std", "derive"]
fltk = "1.4.0"
generic-array = "0.14.7"
hex-literal = "0.3.4"
-windows = { version = "0.48.0", features = [ "Win32_Foundation", "Win32_Security", "Win32_Storage_FileSystem", "Win32_System_SystemInformation", "Win32_System_Diagnostics_Debug", "Win32_System_Memory", "Win32_System_Threading" ]}
+windows = { version = "0.48.0", features = [ "Win32_Foundation", "Win32_Security", "Win32_Storage_FileSystem", "Win32_System_Com", "Win32_System_SystemInformation", "Win32_System_Diagnostics_Debug", "Win32_System_Memory", "Win32_System_Threading", "Win32_UI_Shell" ]}
R src/bin/launch.rs => src/main.rs +29 -20
@@ 1,9 1,13 @@
#![windows_subsystem = "windows"]
-use common::{find_install_folder, install_folder_seems_valid, run_and_patch_game, InstallFolder};
+mod patch;
+mod win;
+
+use patch::{find_install_path, identify_version, run_and_patch_new};
use clap::Parser;
+use anyhow::Context as _;
use fltk::prelude::GroupExt as _;
use fltk::prelude::InputExt as _;
use fltk::prelude::WidgetBase as _;
@@ 22,22 26,24 @@ enum Msg {
LaunchGame { exit_after: bool },
}
-fn main() {
+fn main() -> anyhow::Result<()> {
let Cmd { just_do_it } = Cmd::parse();
if just_do_it {
just_do_it_main()
} else {
- gui_main()
+ gui_main();
+ Ok(())
}
}
-fn just_do_it_main() {
- let installation = find_install_folder().unwrap();
- run_and_patch_game(&installation.exe).unwrap();
+fn just_do_it_main() -> anyhow::Result<()> {
+ let path = find_install_path().ok_or_else(|| anyhow::Error::msg("couldn't find install path"))?;
+ let version = identify_version(&path).context("couldn't determine game version")?;
+ run_and_patch_new(&path, version).context("couldn't launch patched game")
}
fn gui_main() {
- let mut installation = find_install_folder();
+ let mut install_path = find_install_path();
let (sender, receiver) = fltk::app::channel();
@@ 54,10 60,11 @@ fn gui_main() {
let mut folder_output = fltk::output::Output::default();
folder_output.set_value(
- &installation
+ install_path
.as_ref()
- .map(|i| std::borrow::Cow::Owned(format!("{}", i.dir.display())))
- .unwrap_or(std::borrow::Cow::Borrowed("select game install path")),
+ .map(|dir| std::borrow::Cow::Owned(format!("{}", dir.display())))
+ .unwrap_or(std::borrow::Cow::Borrowed("select game install path"))
+ .as_ref(),
);
let mut folder_select_button = fltk::button::Button::default().with_label("Choose game folder");
@@ 76,7 83,7 @@ fn gui_main() {
let mut launch_game_and_exit_button = fltk::button::Button::default().with_label("Launch Game && Exit");
launch_game_and_exit_button.emit(sender.clone(), Msg::LaunchGame { exit_after: true });
flex_row.end();
- if installation.is_some() {
+ if install_path.is_some() {
launch_game_button.activate();
launch_game_and_exit_button.activate();
} else {
@@ 101,21 108,23 @@ fn gui_main() {
diag.show();
let dir = diag.filename();
if !dir.as_os_str().is_empty() {
- let exe = dir.join("bin/x64/ed8_3_PC.exe");
- let ins = InstallFolder { dir, exe };
- if install_folder_seems_valid(&ins) {
- folder_output.set_label(&format!("{}", ins.dir.display()));
- installation = Some(ins);
+ if dir.is_dir() {
+ folder_output.set_label(&format!("{}", dir.display()));
+ install_path = Some(dir);
launch_game_button.activate();
launch_game_and_exit_button.activate();
} else {
- fltk::dialog::alert_default(&format!("'{}' doesn't look a valid installation path", ins.dir.display()));
+ fltk::dialog::alert_default(&format!("Error: '{}' doesn't look a valid installation path", dir.display()));
}
}
}
- Some(Msg::LaunchGame { exit_after }) => match (&installation).as_ref() {
- Some(InstallFolder { dir: _, exe }) => {
- match run_and_patch_game(&exe) {
+ Some(Msg::LaunchGame { exit_after }) => match (&install_path).as_ref() {
+ Some(path) => {
+ match identify_version(&path)
+ .context("couldn't identify game version")
+ .and_then(|version| run_and_patch_new(&path, version))
+ .context("couldn't launch game")
+ {
Ok(()) => {}
Err(err) => {
fltk::dialog::alert_default(&format!("Error:\n{:?}", err));
R src/lib.rs => src/patch.rs +18 -42
@@ 1,13 1,8 @@
-pub mod win;
+use crate::win;
use anyhow::Context as _;
use blake2::Digest as _;
-pub struct InstallFolder {
- pub dir: std::path::PathBuf,
- pub exe: std::path::PathBuf,
-}
-
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ExeVersion {
NisaV1_05En, // NISA version 1.05, english/french language
@@ 23,15 18,11 @@ impl ExeVersion {
}
}
-pub fn exe_hash_matches_1_05<T: AsRef<[u8]>>(_data: T) -> bool {
+pub fn exe_hash_matches_1_05<T: AsRef<[u8]>>(data: T) -> bool {
&blake2::Blake2b512::digest(data.as_ref())[..]
== hex_literal::hex!("bbce84df472cdeefd60464715aadfd7e35ca6a9107da320ea137b9424fc3009a4ecf55090f7a439a1ec4969df02bd190e46dd102c1f9bfbfa18d0b8babc989ff")
}
-pub fn install_folder_seems_valid(installation: &InstallFolder) -> bool {
- installation.dir.is_dir() && installation.exe.is_file()
-}
-
enum Language {
En,
Fr,
@@ 39,8 30,11 @@ enum Language {
}
fn identify_language() -> anyhow::Result<Language> {
- let path = &std::path::Path::new("%LocalAppData%\\TrailsOfColdSteel3\\settings.xml");
- let content = std::fs::read_to_string(path).with_context(|| format!("couldn't open settings file {:?}", path.display()))?;
+ let local_app_data = win::get_local_app_data().context("couldn't find %LocalAppData%")?;
+ println!("{}", local_app_data.display());
+ let path = local_app_data.join("TrailsOfColdSteel3\\settings.xml");
+ println!("{}", path.display());
+ let content = std::fs::read_to_string(&path).with_context(|| format!("couldn't open settings file {:?}", path.display()))?;
// Yes this is ugly
// No I'm not adding an XML lib as a dependency just for this
if content.contains("<Language>English</Language>") {
@@ 86,24 80,7 @@ pub fn identify_version(base_path: &std::path::Path) -> anyhow::Result<ExeVersio
Err(anyhow::Error::msg(format!("Executable file {} hash doesn't match any known one!", path.display())))
}
-pub fn find_base_path() -> Option<std::path::PathBuf> {
- let paths = [
- "GOG Games\\ToCS3",
- "ToCS3",
- "Program Files (x86)\\Steam\\steamapps\\common\\The Legend of Heroes Trails of Cold Steel III",
- "SteamLibrary\\steamapps\\common\\The Legend of Heroes Trails of Cold Steel III",
- ];
- let absolute_folders = win::get_drives_with_fallback()
- .map(|drv| {
- let drv = std::path::PathBuf::from(format!("{drv}:\\"));
- paths.map(|path| drv.join(path))
- })
- .flatten();
- let relative_folders = ["."].into_iter().map(std::path::PathBuf::from);
- relative_folders.chain(absolute_folders).into_iter().find(|path| path.is_dir())
-}
-
-pub fn find_install_folder() -> Option<InstallFolder> {
+pub fn find_install_path() -> Option<std::path::PathBuf> {
let paths = [
"GOG Games\\ToCS3",
"ToCS3",
@@ 117,15 94,7 @@ pub fn find_install_folder() -> Option<InstallFolder> {
})
.flatten();
let relative_folders = ["."].into_iter().map(std::path::PathBuf::from);
- let all_folders = relative_folders.chain(absolute_folders);
- for dir in all_folders {
- let exe = dir.join("bin\\x64\\ed8_3_PC.exe");
- let i = InstallFolder { dir, exe };
- if install_folder_seems_valid(&i) {
- return Some(i);
- }
- }
- None
+ relative_folders.chain(absolute_folders).into_iter().find(|path| path.is_dir() && path.join("bin\\x64").is_dir())
}
unsafe fn apply_patch<N: generic_array::ArrayLength<u8>>(
@@ 148,7 117,7 @@ unsafe fn apply_patch<N: generic_array::ArrayLength<u8>>(
}
}
-pub unsafe fn apply_patches(handle: windows::Win32::Foundation::HANDLE) -> anyhow::Result<()> {
+unsafe fn apply_patches(handle: windows::Win32::Foundation::HANDLE) -> anyhow::Result<()> {
// check game version
apply_patch(handle, 0x140825d68, b"ToCSIII PC Version 1.05\0".into(), Some(b"ToCS3 1.05 autoskip mod\0".into())).context("couldn't update game version")?;
@@ 231,7 200,7 @@ impl Drop for SuspendedGameProcess {
}
}
-pub fn run_and_patch_game(exe_path: &std::path::Path) -> anyhow::Result<()> {
+fn run_and_patch_game(exe_path: &std::path::Path) -> anyhow::Result<()> {
// Check that we got the right exe
let exe_data = std::fs::read(exe_path).context("couldn't check exe data")?;
if !exe_hash_matches_1_05(&exe_data) {
@@ 275,3 244,10 @@ pub fn run_and_patch_game(exe_path: &std::path::Path) -> anyhow::Result<()> {
Ok(())
}
+
+pub fn run_and_patch_new(path: &std::path::Path, version: ExeVersion) -> anyhow::Result<()> {
+ match version {
+ ExeVersion::NisaV1_05En => run_and_patch_game(&path.join(version.relative_path())),
+ ExeVersion::NisaV1_05Jp => todo!(),
+ }
+}
M src/win.rs => src/win.rs +14 -0
@@ 132,6 132,20 @@ pub fn get_drives_with_fallback() -> impl Iterator<Item = char> {
})
}
+pub fn get_local_app_data() -> std::io::Result<std::path::PathBuf> {
+ unsafe {
+ let ptr = windows::Win32::UI::Shell::SHGetKnownFolderPath(
+ &windows::Win32::UI::Shell::FOLDERID_LocalAppData,
+ windows::Win32::UI::Shell::KNOWN_FOLDER_FLAG::default(),
+ windows::Win32::Foundation::HANDLE::default(),
+ )?;
+ assert!(!ptr.is_null());
+ let path = std::path::PathBuf::from(ptr.to_hstring()?.to_os_string());
+ windows::Win32::System::Com::CoTaskMemFree(Some(ptr.0 as *const std::ffi::c_void));
+ Ok(path)
+ }
+}
+
// process handle rights
pub use windows::Win32::System::Threading::PROCESS_ALL_ACCESS;
pub use windows::Win32::System::Threading::PROCESS_QUERY_INFORMATION;