~quf/no-cutscene-kiseki

a8d63ac5a927c051646126b295939a5aff80cb16 — Lukas Himbert 1 year, 7 months ago 6a0f30a
reorganize and fix some bugs
5 files changed, 65 insertions(+), 71 deletions(-)

M .builds/build-for-windows.yml
M Cargo.toml
R src/{bin/launch.rs => main.rs}
R src/{lib.rs => patch.rs}
M src/win.rs
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;