diff --git a/Cargo.lock b/Cargo.lock index cc2cb7c..05306f0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -491,7 +491,7 @@ dependencies = [ [[package]] name = "coa-core" -version = "0.6.9" +version = "0.6.10" dependencies = [ "base64 0.22.1", "chrono", @@ -520,7 +520,7 @@ dependencies = [ [[package]] name = "coa-release" -version = "0.6.9" +version = "0.6.10" dependencies = [ "chrono", "coa-core", @@ -529,7 +529,7 @@ dependencies = [ [[package]] name = "coa-server-manager" -version = "0.6.9" +version = "0.6.10" dependencies = [ "coa-core", "serde", diff --git a/Cargo.toml b/Cargo.toml index 108ecec..5c3422e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ resolver = "2" members = ["crates/coa-core", "crates/coa-release", "src-tauri"] [workspace.package] -version = "0.6.9" +version = "0.6.10" edition = "2021" license = "AGPL-3.0-only" diff --git a/changelog.d/20261010-custom-races-update.md b/changelog.d/20261010-custom-races-update.md new file mode 100644 index 0000000..0f51a5c --- /dev/null +++ b/changelog.d/20261010-custom-races-update.md @@ -0,0 +1,7 @@ +--- +area: manager +type: changed +audience: admins +title: Custom race client patches update to the latest races and appearances +--- +The module card offers an upgrade for older patches. Archive downloads are verified before installation. diff --git a/crates/coa-core/src/custom_races.rs b/crates/coa-core/src/custom_races.rs index 38b8945..c675e4d 100644 --- a/crates/coa-core/src/custom_races.rs +++ b/crates/coa-core/src/custom_races.rs @@ -14,19 +14,20 @@ use crate::download::Cancel; use crate::error::{Error, Result}; use crate::fsx; +pub const CLIENT_VERSION: &str = "1.5"; pub const MANIFEST_FILENAME: &str = "custom_races_patch_manifest.txt"; pub const ARCHIVES: &[(&str, &str)] = &[ ( - "CoA-Custom-1.4-main-20261007-b46a130e.zip", - "https://github.com/ilusixn/azerothcore-wotlk-coa/releases/download/coa-custom-1.4/CoA-Custom-1.4-main-20261007-b46a130e.zip", + "CoA-Custom-1.5-main-20261009-cb2a0ca4.zip", + "https://github.com/ilusixn/azerothcore-wotlk-coa/releases/download/coa-custom-1.5/CoA-Custom-1.5-main-20261009-cb2a0ca4.zip", ), ( - "CoA-Custom-1.4-client-part1.zip", - "https://github.com/ilusixn/azerothcore-wotlk-coa/releases/download/coa-custom-1.4/CoA-Custom-1.4-client-part1.zip", + "CoA-Custom-1.5-client-part1.zip", + "https://github.com/ilusixn/azerothcore-wotlk-coa/releases/download/coa-custom-1.5/CoA-Custom-1.5-client-part1.zip", ), ( - "CoA-Custom-1.4-client-part2.zip", - "https://github.com/ilusixn/azerothcore-wotlk-coa/releases/download/coa-custom-1.4/CoA-Custom-1.4-client-part2.zip", + "CoA-Custom-1.5-client-part2.zip", + "https://github.com/ilusixn/azerothcore-wotlk-coa/releases/download/coa-custom-1.5/CoA-Custom-1.5-client-part2.zip", ), ]; @@ -47,6 +48,10 @@ pub const RACE_MPQS: &[&str] = &[ "patch-ZHM.MPQ", "patch-ZM.MPQ", "patch-ZVU.MPQ", + "patch-ZES.MPQ", + "patch-ZJ.MPQ", + "patch-ZZE.MPQ", + "patch-ZZR.MPQ", ]; #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] @@ -87,9 +92,11 @@ pub fn client_patch_status(client_path: &Path) -> ClientPatchStatus { || client_path.join("dinput8.dll.disabled").is_file(); let total = RACE_MPQS.len(); - let state = if enabled_count == total && client_path.join("dinput8.dll").is_file() { + let current_version = fs::read_to_string(client_path.join(MANIFEST_FILENAME)).ok() + .is_some_and(|manifest| manifest.lines().any(|line| line == format!("# CoA Custom {CLIENT_VERSION}"))); + let state = if enabled_count == total && current_version && client_path.join("dinput8.dll").is_file() { ClientPatchState::Enabled - } else if disabled_count == total && client_path.join("dinput8.dll.disabled").is_file() { + } else if disabled_count == total && current_version && client_path.join("dinput8.dll.disabled").is_file() { ClientPatchState::Disabled } else if (enabled_count + disabled_count) > 0 { ClientPatchState::PartiallyInstalled @@ -121,6 +128,18 @@ pub fn set_client_patch_enabled(client_path: &Path, enabled: bool) -> Result<()> return Ok(()); } let mut paths: Vec = RACE_MPQS.iter().map(|name| client_path.join("Data").join(name)).collect(); + if let Ok(manifest) = fs::read_to_string(client_path.join(MANIFEST_FILENAME)) { + for line in manifest.lines() { + if let Some(name) = line.strip_prefix("MPQ:") { + if name.contains(['/', '\\']) || !name.to_ascii_lowercase().ends_with(".mpq") { + return Err(Error::Invalid("Invalid custom races patch manifest.".into())); + } + paths.push(fsx::safe_join(&client_path.join("Data"), name)?); + } + } + } + paths.sort(); + paths.dedup(); paths.push(client_path.join("dinput8.dll")); paths.push(client_path.join("Ascension.exe")); for active in paths { @@ -151,9 +170,9 @@ pub fn set_client_patch_enabled(client_path: &Path, enabled: bool) -> Result<()> fn verify_archive(path: &Path, filename: &str) -> Result<()> { let expected = match filename { - "CoA-Custom-1.4-main-20261007-b46a130e.zip" => "eaf934b5283476aa7b5d6eca7200ceae016e866ddbdab726bd47f39739c9c802", - "CoA-Custom-1.4-client-part1.zip" => "669f8396e82f396125e0be2070b2bad782f0104c695939a88924ec97f4fc4d21", - "CoA-Custom-1.4-client-part2.zip" => "088b3ddcf292abccbc84453c0ccf4cd74ff6b20c848d5902dc83854553aecc8b", + "CoA-Custom-1.5-main-20261009-cb2a0ca4.zip" => "117f5d99add18191676be6648fb0f2e99019a06c18705c71454e371e7df2afd2", + "CoA-Custom-1.5-client-part1.zip" => "19eb372805b3268653914da03377d67f056e5b50d818a8a06a5f260aad03211e", + "CoA-Custom-1.5-client-part2.zip" => "122f2882da255913e01610e33c6446ded897fecbe60b6d5ee026149becf8f278", _ => return Err(Error::Invalid("Unknown custom races archive.".into())), }; if fsx::sha256_file(path)? != expected { @@ -368,11 +387,15 @@ pub fn install_client_patches( let dest = data_dir.join(&base); let mut out = File::create(&dest)?; io::copy(&mut zfile, &mut out)?; + let disabled = data_dir.join(format!("{base}.disabled")); + if disabled.is_file() { fs::remove_file(disabled)?; } installed_mpqs.push(base); } else if base.eq_ignore_ascii_case("dinput8.dll") { let dest = client_path.join(&base); let mut out = File::create(&dest)?; io::copy(&mut zfile, &mut out)?; + let disabled = client_path.join(format!("{base}.disabled")); + if disabled.is_file() { fs::remove_file(disabled)?; } installed_roots.push(base); } } else if zname_norm.contains("files/client_root/") { @@ -385,12 +408,18 @@ pub fn install_client_patches( let dest = client_path.join(&base); let mut out = File::create(&dest)?; io::copy(&mut zfile, &mut out)?; + let disabled = client_path.join(format!("{base}.disabled")); + if disabled.is_file() { fs::remove_file(disabled)?; } installed_roots.push(base); } } } } + if !RACE_MPQS.iter().all(|name| installed_mpqs.iter().any(|installed| installed == name)) { + return Err(Error::Invalid("Custom races archives are missing required client patches.".into())); + } + // 4. Record manifest report("Finalizing manifest...", 0.98); let manifest_path = client_path.join(MANIFEST_FILENAME); @@ -399,7 +428,7 @@ pub fn install_client_patches( installed_roots.sort(); installed_roots.dedup(); - let mut manifest_body = String::from("# Custom Race MPQ files\n"); + let mut manifest_body = format!("# CoA Custom {CLIENT_VERSION}\n# Custom Race MPQ files\n"); for m in &installed_mpqs { manifest_body.push_str(&format!("MPQ:{m}\n")); } @@ -442,6 +471,16 @@ mod tests { assert_eq!(fs::read(temp.path().join("Ascension.exe")).unwrap(), b"custom"); } + #[test] + fn an_old_complete_installation_requires_an_update() { + let temp = tempfile::tempdir().unwrap(); + fs::create_dir_all(temp.path().join("Data")).unwrap(); + for name in RACE_MPQS { fs::write(temp.path().join("Data").join(name), b"old patch").unwrap(); } + fs::write(temp.path().join("dinput8.dll"), b"old runtime").unwrap(); + fs::write(temp.path().join(MANIFEST_FILENAME), "# CoA Custom 1.4\n").unwrap(); + assert_eq!(client_patch_status(temp.path()).state, ClientPatchState::PartiallyInstalled); + } + #[test] fn an_unpatched_client_is_left_untouched() { let temp = tempfile::tempdir().unwrap(); @@ -469,6 +508,7 @@ mod tests { let st = client_patch_status(client_path); assert_eq!(st.state, ClientPatchState::NotInstalled); + fs::write(client_path.join(MANIFEST_FILENAME), format!("# CoA Custom {CLIENT_VERSION}\n")).unwrap(); // 2. Create MPQs and root files for mpq in RACE_MPQS { fs::write(data_dir.join(mpq), b"test mpq content").unwrap(); diff --git a/package-lock.json b/package-lock.json index f08d81b..a8f15aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "coa-server-manager", - "version": "0.6.9", + "version": "0.6.10", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "coa-server-manager", - "version": "0.6.9", + "version": "0.6.10", "dependencies": { "@tauri-apps/api": "^2.12.0", "@tauri-apps/plugin-dialog": "^2.8.0", diff --git a/package.json b/package.json index a580101..17db782 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "coa-server-manager", "private": true, - "version": "0.6.9", + "version": "0.6.10", "license": "AGPL-3.0-only", "type": "module", "scripts": { diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 8116dd4..6660282 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "CoA Server Manager", - "version": "0.6.9", + "version": "0.6.10", "identifier": "com.coa.servermanager", "build": { "beforeDevCommand": "npm run dev", diff --git a/src/screens/ModulesPage.tsx b/src/screens/ModulesPage.tsx index 5431518..5843dbd 100644 --- a/src/screens/ModulesPage.tsx +++ b/src/screens/ModulesPage.tsx @@ -261,7 +261,7 @@ export function ModulesPage({ serverId, onChanged, onRegisterGuard }: { serverId onClick={() => void installClientPatch()} > {racesInstalling && } - {locale === "ru" ? "Установить патч клиента" : "Install Client Patch"} + {locale === "ru" ? "Установить патч клиента" : "Install Client Patch 1.5"} )}