commit 46cda692fcb4c65a687b5f0aee643ae7c82951b9 Author: sneedmaster Date: Tue Apr 28 12:34:20 2026 +0200 Něco diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5b670ed --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +/target +/repos +/dist +.DS_Store +.07build +*.dxvk-cache +higu01 +higu_script_compile_status.txt diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..0e285ea --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,200 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "zeronanabuild" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "fs_extra", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..9390a95 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "zeronanabuild" +version = "0.1.0" +edition = "2024" + +[dependencies] +anyhow = "1.0.102" +clap = { version = "4.6.0", features = ["derive"] } +fs_extra = "1.3.0" diff --git a/README.md b/README.md new file mode 100644 index 0000000..c1e2311 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# 07build + +Custom tool for building and installing the 07th-Mod Higurashi patches. diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..49fe59b --- /dev/null +++ b/src/main.rs @@ -0,0 +1,585 @@ +use anyhow::bail; +use clap::{Parser, Subcommand}; +use fs_extra::{ + copy_items, + dir::CopyOptions as DirCopyOptions, + file::{CopyOptions as FileCopyOptions, move_file}, +}; +use std::{ + fs::{copy, create_dir_all, remove_dir_all}, + path::PathBuf, + process::Command, +}; + +/// 07th-Mod build tool +#[derive(Parser, Debug)] +#[command(version, about, long_about = None)] +struct Args { + /// The subcommand to run + #[command(subcommand)] + command: CLISubcommand, +} + +#[derive(Subcommand, Debug)] +enum CLISubcommand { + /// Prepares tools for building and installing the patch + InitPatch { + /// ID of the chapter ("onikakushi", ...) + #[arg(short, long)] + chapter: String, + /// The working directory to initialize + #[arg(short, long, default_value = ".07build")] + working: PathBuf, + /// Don't pull base (for build only) + #[arg(long, default_value = "false")] + no_base: bool, + /// Don't pull build dependencies (for installation only) + #[arg(long, default_value = "false")] + no_build_deps: bool, + }, + /// Prepares tools for building the UI patch + InitUI { + /// The working directory to initialize + #[arg(short, long, default_value = ".07build")] + working: PathBuf, + /// Path to the UI directory to initialize + #[arg(short, long, default_value = ".")] + ui: PathBuf, + }, + /// Builds the patch + BuildPatch { + /// ID of the chapter ("onikakushi", ...) + #[arg(short, long)] + chapter: String, + /// The initialized working directory + #[arg(short, long, default_value = ".07build")] + working: PathBuf, + /// Directory to output the built patch + #[arg(short, long, default_value = "dist")] + dist: PathBuf, + /// Directory of the patch + #[arg(short, long, default_value = ".")] + patch: PathBuf, + /// Zip the built patch + #[arg(short, long, default_value = "false")] + zip: bool, + }, + /// Builds the UI patch + BuildUI { + /// ID of the chapter ("onikakushi", ...) + #[arg(short, long)] + chapter: String, + /// Directory of the built patch + #[arg(short, long, default_value = "dist")] + dist: PathBuf, + /// Path to the UI directory + #[arg(short, long, default_value = ".")] + ui: PathBuf, + }, + /// Installs patches + Install { + /// ID of the chapter ("onikakushi", ...) + #[arg(short, long)] + chapter: String, + /// The initialized working directory + #[arg(short, long, default_value = ".07build")] + working: PathBuf, + /// The patch + #[arg(short, long)] + patch: PathBuf, + /// The UI patch + #[arg(short, long)] + ui: PathBuf, + /// The game directory (with the exe) + #[arg(short, long, default_value = ".")] + game: PathBuf, + }, +} + +fn main() -> anyhow::Result<()> { + let args = Args::parse(); + + match args.command { + CLISubcommand::InitPatch { + chapter, + working, + no_base, + no_build_deps, + } => init_patch(chapter, working, no_base, no_build_deps)?, + CLISubcommand::InitUI { working, ui } => init_ui(working, ui)?, + CLISubcommand::BuildPatch { + chapter, + working, + patch, + dist, + zip, + } => build_patch(chapter, working, patch, dist, zip)?, + CLISubcommand::BuildUI { chapter, dist, ui } => build_ui(chapter, dist, ui)?, + CLISubcommand::Install { + chapter, + working, + patch, + ui, + game, + } => install(chapter, working, patch, ui, game)?, + } + + Ok(()) +} + +fn chapter_to_strings(chapter: &str) -> anyhow::Result<(&str, &str, &str, &str)> { + match chapter { + "onikakushi" => Ok(( + "https://github.com/07th-mod/patch-releases/releases/download/onikakushi-v1.0/onikakushi-base.7z", + "HigurashiEp01_Data", + "5.2.2f1", + "Onikakushi", + )), + _ => bail!("Chapter {chapter} isn't supported yet!"), + } +} + +fn init_patch( + chapter: String, + working: PathBuf, + no_base: bool, + no_build_deps: bool, +) -> anyhow::Result<()> { + create_dir_all(&working)?; + + // DOWNLOAD + + // Download base + + let (base_url, _, _, _) = chapter_to_strings(&chapter)?; + + let base_7z_path = working.join(format!("{chapter}-base.7z")); + let base_path = working.join(format!("{chapter}-base")); + + if !no_base && !base_7z_path.exists() { + println!("Downloading base..."); + + Command::new("wget") + .arg(base_url) + .arg("-O") + .arg(&base_7z_path) + .status()?; + } + + if !no_base && !base_path.exists() { + println!("Unzipping base..."); + + Command::new("7z") + .arg("x") + .arg(base_7z_path) + .arg("-y") + .arg(format!("-o{}", base_path.to_string_lossy().to_string())) + .status()?; + } + + if no_build_deps { + return Ok(()); + } + + // Download video DLL + + let avpv_path = working.join("AVProVideo.dll"); + + if !avpv_path.exists() { + println!("Downloading AVProVideo.dll..."); + + Command::new("wget") + .arg("https://github.com/07th-mod/patch-releases/releases/download/developer-v1.0/AVProVideo.dll") + .arg("-O") + .arg(avpv_path) + .status()?; + } + + // Download the assembly + + let assemply_path = working.join("assembly"); + + if !assemply_path.exists() { + println!("Downloading assembly..."); + + Command::new("git") + .arg("clone") + .arg("https://github.com/07th-mod/higurashi-assembly.git") + .arg(&assemply_path) + .status()?; + } + + // BUILD DEPENDENCIES + + // Common + let assembly_csproj_path = assemply_path.join("Assembly-CSharp.csproj"); + + // Build the assembly + + let assembly_dll_path = assemply_path + .join("bin") + .join("Release") + .join("Assembly-CSharp.dll"); + + if !assembly_dll_path.exists() { + println!("Building the assembly..."); + + Command::new("xbuild") + .arg("/p:Configuration=Release") + .arg(&assembly_csproj_path) + .status()?; + } + + // Build the script compiler + + let script_compiler_path = assemply_path + .join("bin") + .join("ScriptCompiler") + .join("HigurashiScriptCompiler.exe"); + + if !script_compiler_path.exists() { + println!("Building the script compiler..."); + + Command::new("xbuild") + .arg("/p:Configuration=ScriptCompiler") + .arg(assembly_csproj_path) + .status()?; + } + + Ok(()) +} + +fn init_ui(working: PathBuf, ui: PathBuf) -> anyhow::Result<()> { + create_dir_all(&working)?; + + // INIT + + // Initialize venv + let ui_venv_path = ui.join("venv"); + + if !ui_venv_path.exists() { + println!("Initializing UI venv..."); + + Command::new("python3") + .arg("-m") + .arg("venv") + .arg("venv") + .current_dir(&ui) + .status()?; + + println!("Installing UI requirements..."); + + Command::new("venv/bin/pip") + .arg("install") + .arg("-r") + .arg("requirements.txt") + .current_dir(&ui) + .status()?; + } + + // Download vanilla UI + + let vanilla_7z_path = working.join("vanilla.7z"); + let vanilla_path = ui.join("assets").join("vanilla"); + + if !vanilla_7z_path.exists() { + println!("Downloading vanilla UI..."); + + Command::new("wget") + .arg("https://github.com/07th-mod/patch-releases/releases/download/developer-v1.0/vanilla.7z") + .arg("-O") + .arg(&vanilla_7z_path) + .status()?; + } + + if !vanilla_path.exists() { + println!("Unzipping vanilla UI..."); + + Command::new("7z") + .arg("x") + .arg(vanilla_7z_path) + .arg("-y") + .arg(format!("-o{}", ui.to_string_lossy().to_string())) + .status()?; + } + + // Download UABE (into the UI directory) + + let uabe_7z_path = working.join("uabe.7z"); + let uabe_path = ui.join("64bit").join("AssetBundleExtractor.exe"); + + if !uabe_7z_path.exists() { + println!("Downloading UABE..."); + + Command::new("wget") + .arg("https://github.com/07th-mod/patch-releases/releases/download/developer-v1.0/AssetsBundleExtractor_2.2stabled_64bit_with_VC2010.zip") + .arg("-O") + .arg(&uabe_7z_path) + .status()?; + } + + if !uabe_path.exists() { + println!("Unzipping UABE..."); + + Command::new("7z") + .arg("x") + .arg(uabe_7z_path) + .arg("-y") + .arg(format!("-o{}", ui.to_string_lossy().to_string())) + .status()?; + } + + // BUILD + + // Build the UI compiler (bin) + let ui_compiler_path = ui.join("target").join("release").join("ui-compiler"); + + if !ui_compiler_path.exists() { + println!("Building the UI compiler..."); + + Command::new("cargo") + .arg("build") + .arg("--release") + .current_dir(ui) + .status()?; + } + + Ok(()) +} + +fn build_patch( + chapter: String, + working: PathBuf, + patch: PathBuf, + dist: PathBuf, + zip: bool, +) -> anyhow::Result<()> { + // BUILD PATCH + + let scripts_path = patch.join("Update"); + + // Compile scripts + + let compiled_scripts_path = patch.join("CompiledUpdateScripts"); + create_dir_all(&compiled_scripts_path)?; + + let script_compiler_path = working + .join("assembly") + .join("bin") + .join("ScriptCompiler") + .join("HigurashiScriptCompiler.exe"); + + println!("Compiling scripts..."); + + Command::new("wine") + .arg(script_compiler_path) + .arg(scripts_path) + .arg(compiled_scripts_path) + .status()?; + + // COPY + + let (_, data_subdir, _, _) = chapter_to_strings(&chapter)?; + + let data_path = dist.join(data_subdir); + let managed_path = data_path.join("Managed"); + let plugins_path = data_path.join("Plugins"); + let streaming_assets_path = data_path.join("StreamingAssets"); + + create_dir_all(&data_path)?; + create_dir_all(&managed_path)?; + create_dir_all(&plugins_path)?; + create_dir_all(&streaming_assets_path)?; + + // Copy tips.json + + let src_tips_path = patch.join("tips.json"); + let dist_tips_path = data_path.join("tips.json"); + + println!("Copying tips.json..."); + + copy(src_tips_path, dist_tips_path)?; + + // Copy assembly + + let src_assembly_path = working + .join("assembly") + .join("bin") + .join("Release") + .join("Assembly-CSharp.dll"); + + let dist_assembly_path = managed_path.join("Assembly-CSharp.dll"); + + println!("Copying assembly..."); + + copy(src_assembly_path, dist_assembly_path)?; + + // Copy AVProVideo.dll + + let src_avpv_path = working.join("AVProVideo.dll"); + let dist_avpv_path = plugins_path.join("AVProVideo.dll"); + + println!("Copying AVProVideo.dll..."); + + copy(src_avpv_path, dist_avpv_path)?; + + // Copy StreamingAssets + + let cp_dirs: Vec = [ + "CG", + "CGAlt", + "CompiledUpdateScripts", + "OGSprites", + "spectrum", + "Update", + "voice", + ] + .map(|dir| patch.join(dir)) + .into_iter() + .filter(|path| path.exists()) + .collect(); + + println!("Copying StreamingAssets..."); + + copy_items( + &cp_dirs, + &streaming_assets_path, + &DirCopyOptions::new().overwrite(true), + )?; + + // Zip the patch + + if zip { + let patch_7z_path = dist.join(format!("{chapter}-patch.7z")); + + Command::new("7z") + .arg("a") + .arg(patch_7z_path) + .arg(data_path) + .arg("-y") + .status()?; + } + + Ok(()) +} + +fn build_ui(chapter: String, dist: PathBuf, ui: PathBuf) -> anyhow::Result<()> { + let (_, _, unity, title) = chapter_to_strings(&chapter)?; + + // Compile for Windows + + println!("Compiling UI for Windows..."); + + Command::new("target/release/ui-compiler") + .arg(&chapter) + .arg(unity) + .arg("win") + .current_dir(&ui) + .status()?; + + // Compile for Unix + + println!("Compiling UI for Unix..."); + + Command::new("target/release/ui-compiler") + .arg(&chapter) + .arg(unity) + .arg("unix") + .current_dir(&ui) + .status()?; + + // Move to dist + + let win_patch_src = ui + .join("output") + .join(format!("{}-UI_{}_win.7z", title, unity)); + + let win_patch_dist = dist.join(format!("{chapter}-ui-win.7z")); + + let unix_patch_src = ui + .join("output") + .join(format!("{}-UI_{}_unix.7z", title, unity)); + + let unix_patch_dist = dist.join(format!("{chapter}-ui-unix.7z")); + + println!("Moving to dist..."); + + move_file( + win_patch_src, + win_patch_dist, + &FileCopyOptions::new().overwrite(true), + )?; + + move_file( + unix_patch_src, + unix_patch_dist, + &FileCopyOptions::new().overwrite(true), + )?; + + Ok(()) +} + +fn install( + chapter: String, + working: PathBuf, + patch: PathBuf, + ui: PathBuf, + game: PathBuf, +) -> anyhow::Result<()> { + let (_, data_dir, _, _) = chapter_to_strings(&chapter)?; + + let base_data_path = working.join(format!("{chapter}-base")).join(&data_dir); + let dist_data_path = game.join(&data_dir); + + // Remove CG and CGAlt + + println!("Removing CG and CGAlt..."); + + let cg_dirs: Vec = [ + dist_data_path.join("StreamingAssets").join("CG"), + dist_data_path.join("StreamingAssets").join("CGAlt"), + ] + .into_iter() + .filter(|path| path.exists()) + .collect(); + + for dir in cg_dirs { + remove_dir_all(dir)?; + } + + // Install base + + println!("Installing base..."); + + copy_items( + &[base_data_path], + &game, + &DirCopyOptions::new().overwrite(true), + )?; + + // Install patch + + println!("Installing patch..."); + + Command::new("7z") + .arg("x") + .arg(patch) + .arg("-y") + .arg("-aoa") + .arg(format!("-o{}", game.to_string_lossy().to_string())) + .status()?; + + // Install UI patch + + println!("Installing UI patch"); + + Command::new("7z") + .arg("x") + .arg(ui) + .arg("-y") + .arg("-aoa") + .arg(format!("-o{}", game.to_string_lossy().to_string())) + .status()?; + + Ok(()) +}