~quf/no-cutscene-kiseki

6a0f30aa1158a85b1f1ce4ba8079d787f7e2ceca — Lukas Himbert 10 months ago b78bfba
prep jp support
5 files changed, 121 insertions(+), 11 deletions(-)

M Cargo.lock
M Cargo.toml
M README.md
M src/lib.rs
M x.gdb
M Cargo.lock => Cargo.lock +1 -1
@@ 209,7 209,7 @@ checksum = "3304a64d199bb964be99741b7a14d26972741915b3649639149b2479bb46f4b5"

[[package]]
name = "no-cutscene-kiseki"
version = "0.0.2"
version = "0.0.3"
dependencies = [
 "anyhow",
 "atty",

M Cargo.toml => Cargo.toml +1 -1
@@ 1,6 1,6 @@
[package]
name = "no-cutscene-kiseki"
version = "0.0.2"
version = "0.0.3"
edition = "2021"

[lib]

M README.md => README.md +7 -7
@@ 6,12 6,9 @@ This is a mod for the PC version of Trails of Cold Steel III to automatically sk
Every cutscene that can be skipped via the "skip cutscene? yes/no" menu in the original game will be skipped automatically with no user input.
Cutscenes that cannot be skipped this way in the original game are not changed: They will play out as normal with no input, but can still be fast-forwarded.

TODO: demonstration with button inputs
[Video demo (on youtube)](https://www.youtube.com/watch?v=2BkCd9lfyQQ)

The only currently supported version is the version 1.05 of the NISA PC port, English text language only.
French language should work but has not been extensively tested.
If there is interest, I can look into support for Japanese text (which requires patching a different exe).
The other games in the series don't benefit from auto-skipped cutscenes nearly as much as CS3, so I don't plan to support them any time soon if at all.
The only currently supported version is the version 1.05 of the NISA PC port.

Usage
-----


@@ 20,9 17,10 @@ Usage

- Download and run the mod .exe
- If the game folder hasn't been detected automatically, select it yourself.
  The game folder you need to choose contains (among others) the file `Sen3Launcher.exe`, as well as folders `bin` and `data`.
- Click either "Launch Game" or "Launch Game & Exit"

If your game is installed to a nonstandard location, you can copy the mod .exe to that location and it should recognize the game automatically.
If your game is installed to a nonstandard location and you want to avoid manually choosing the game folder every time, you can copy the mod .exe to that game folder and it should be recognized automatically.

Credits
-------


@@ 37,4 35,6 @@ Thanks to:
Releases
--------

None yet!
## [version 1](https://git.sr.ht/~quf/no-cutscene-kiseki/refs/download/no-cutscene-kiseki-v1/no-cutscene-kiseki-v1-launcher.exe)

- initial public release

M src/lib.rs => src/lib.rs +87 -1
@@ 8,7 8,22 @@ pub struct InstallFolder {
    pub exe: std::path::PathBuf,
}

pub fn exe_hash_matches_1_05<T: AsRef<[u8]>>(data: T) -> bool {
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ExeVersion {
    NisaV1_05En, // NISA version 1.05, english/french language
    NisaV1_05Jp, // NISA version 1.05, english/french language
}

impl ExeVersion {
    pub fn relative_path(&self) -> &'static str {
        match self {
            ExeVersion::NisaV1_05En => "bin\\x64\\ed8_3_PC.exe",
            ExeVersion::NisaV1_05Jp => "bin\\x64\\ed8_3_PC_JP.exe",
        }
    }
}

pub fn exe_hash_matches_1_05<T: AsRef<[u8]>>(_data: T) -> bool {
    &blake2::Blake2b512::digest(data.as_ref())[..]
        == hex_literal::hex!("bbce84df472cdeefd60464715aadfd7e35ca6a9107da320ea137b9424fc3009a4ecf55090f7a439a1ec4969df02bd190e46dd102c1f9bfbfa18d0b8babc989ff")
}


@@ 17,6 32,77 @@ pub fn install_folder_seems_valid(installation: &InstallFolder) -> bool {
    installation.dir.is_dir() && installation.exe.is_file()
}

enum Language {
    En,
    Fr,
    Jp,
}

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()))?;
    // Yes this is ugly
    // No I'm not adding an XML lib as a dependency just for this
    if content.contains("<Language>English</Language>") {
        Ok(Language::En)
    } else if content.contains("<Language>Japanese</Language>") {
        Ok(Language::Jp)
    } else if content.contains("<Language>French</Language>") {
        Ok(Language::Fr)
    } else {
        Err(anyhow::Error::msg(format!("settings file {} doesn't seem to contain a <Language> element", path.display())))
    }
}

pub fn identify_version(base_path: &std::path::Path) -> anyhow::Result<ExeVersion> {
    let language = identify_language().context("couldn't identify game language")?;

    let (exe_path, known_hashes) = match language {
        Language::En | Language::Fr => (
            "bin\\x64\\ed8_3_PC.exe",
            &[(
                ExeVersion::NisaV1_05En,
                hex_literal::hex!("bbce84df472cdeefd60464715aadfd7e35ca6a9107da320ea137b9424fc3009a4ecf55090f7a439a1ec4969df02bd190e46dd102c1f9bfbfa18d0b8babc989ff"),
            )],
        ),
        Language::Jp => (
            "bin\\x64\\ed8_3_PC_JP.exe",
            &[(
                ExeVersion::NisaV1_05Jp,
                hex_literal::hex!("2e846b7f10bda3d3ae191d4391b35b879683f73628e9bcb8a93b10448e65ecf22a496e3fe90c06aa140bb75bdfc09965367d4209c6311272e67d9a208104da7d"),
            )],
        ),
    };

    let path = base_path.join(exe_path);
    let data = std::fs::read(&path).with_context(|| format!("couldn't read executable file {}", path.display()))?;
    let hash = blake2::Blake2b512::digest(&data);
    for (exe_version, expected_hash) in known_hashes.iter().copied() {
        if &hash[..] == expected_hash {
            return Ok(exe_version);
        }
    }

    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> {
    let paths = [
        "GOG Games\\ToCS3",

M x.gdb => x.gdb +25 -1
@@ 8,6 8,8 @@ display $rax
display $rbx

handle SIGUSR1 pass nostop noprint

## EN
#rwatch *(unsigned char *) 0x1416c535d
#awatch *(unsigned char *) 0x1416c535d
#condition 1 ($rip != 0x00000001402fd952) && ($rip != 0x000000014032a5b1) && ($rip != 0x000000014032a5b8) && ($rip != 0x00000001402fcf77) && ($rip != 0x00000001402fd222) && ($rip != 0x00000001402fd22a) && ($rip != 0x00000001402fd3e6) && ($rip != 0x00000001403dfdaa) && ($rip != 0x00000001402fc96e)


@@ 15,6 17,28 @@ handle SIGUSR1 pass nostop noprint
#watch *(unsigned char *) 0x1416c535d
#condition 1 ((*(unsigned char *) 0x1416c535d) == 0)

watch *0x14032a5b1 if $rbx != 0x1416c1620
#watch *0x14032a5b1 if $rbx != 0x1416c1620
#watch *0x1403dfda3 if $rax != 0x1416c1620

## JP

# cutscene skip flag
set *(unsigned char *) 0x1416af22d = 1

# cutscene skip flag setters: set to 1 instead of 0
set *(unsigned char *) 0x1403210a7 = 1
set *(unsigned char *) (0x14030acda + 8) = 1
set *(unsigned char *) 0x1403d5949 = 1

# watch cutscene skip flag changes
watch *(unsigned char *) 0x1416af22d
# except from "known" locations
condition 1 ($rip != 0x1402f5a36) && ($rip != 0x1403210a8) && ($rip != 0x14030ace5) && ($rip != 0x1403d594a)

# watch cutscene skip flag setters
#watch *0x1403210a1 if $rbx != 0x1416ab4f0
#watch *0x14030acda if $rbx != 0x1416ab4f0
#watch *0x1403d5943 if $rax != 0x1416ab4f0


continue